mcp-ticketer 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +27 -0
- mcp_ticketer/__version__.py +40 -0
- mcp_ticketer/adapters/__init__.py +8 -0
- mcp_ticketer/adapters/aitrackdown.py +396 -0
- mcp_ticketer/adapters/github.py +974 -0
- mcp_ticketer/adapters/jira.py +831 -0
- mcp_ticketer/adapters/linear.py +1355 -0
- mcp_ticketer/cache/__init__.py +5 -0
- mcp_ticketer/cache/memory.py +193 -0
- mcp_ticketer/cli/__init__.py +5 -0
- mcp_ticketer/cli/main.py +812 -0
- mcp_ticketer/cli/queue_commands.py +285 -0
- mcp_ticketer/cli/utils.py +523 -0
- mcp_ticketer/core/__init__.py +15 -0
- mcp_ticketer/core/adapter.py +211 -0
- mcp_ticketer/core/config.py +403 -0
- mcp_ticketer/core/http_client.py +430 -0
- mcp_ticketer/core/mappers.py +492 -0
- mcp_ticketer/core/models.py +111 -0
- mcp_ticketer/core/registry.py +128 -0
- mcp_ticketer/mcp/__init__.py +5 -0
- mcp_ticketer/mcp/server.py +459 -0
- mcp_ticketer/py.typed +0 -0
- mcp_ticketer/queue/__init__.py +7 -0
- mcp_ticketer/queue/__main__.py +6 -0
- mcp_ticketer/queue/manager.py +261 -0
- mcp_ticketer/queue/queue.py +357 -0
- mcp_ticketer/queue/run_worker.py +38 -0
- mcp_ticketer/queue/worker.py +425 -0
- mcp_ticketer-0.1.1.dist-info/METADATA +362 -0
- mcp_ticketer-0.1.1.dist-info/RECORD +35 -0
- mcp_ticketer-0.1.1.dist-info/WHEEL +5 -0
- mcp_ticketer-0.1.1.dist-info/entry_points.txt +3 -0
- mcp_ticketer-0.1.1.dist-info/licenses/LICENSE +21 -0
- mcp_ticketer-0.1.1.dist-info/top_level.txt +1 -0
mcp_ticketer/cli/main.py
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
"""CLI implementation using Typer."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich import print as rprint
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
|
|
16
|
+
from ..core import Task, TicketState, Priority, AdapterRegistry
|
|
17
|
+
from ..core.models import SearchQuery
|
|
18
|
+
from ..adapters import AITrackdownAdapter
|
|
19
|
+
from ..queue import Queue, QueueStatus, WorkerManager
|
|
20
|
+
from .queue_commands import app as queue_app
|
|
21
|
+
|
|
22
|
+
# Load environment variables
|
|
23
|
+
load_dotenv()
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="mcp-ticketer",
|
|
27
|
+
help="Universal ticket management interface",
|
|
28
|
+
add_completion=False,
|
|
29
|
+
)
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
# Configuration file management
|
|
33
|
+
CONFIG_FILE = Path.home() / ".mcp-ticketer" / "config.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdapterType(str, Enum):
|
|
37
|
+
"""Available adapter types."""
|
|
38
|
+
AITRACKDOWN = "aitrackdown"
|
|
39
|
+
LINEAR = "linear"
|
|
40
|
+
JIRA = "jira"
|
|
41
|
+
GITHUB = "github"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_config() -> dict:
|
|
45
|
+
"""Load configuration from file."""
|
|
46
|
+
if CONFIG_FILE.exists():
|
|
47
|
+
with open(CONFIG_FILE, "r") as f:
|
|
48
|
+
return json.load(f)
|
|
49
|
+
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_config(config: dict) -> None:
|
|
53
|
+
"""Save configuration to file."""
|
|
54
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
with open(CONFIG_FILE, "w") as f:
|
|
56
|
+
json.dump(config, f, indent=2)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def merge_config(updates: dict) -> dict:
|
|
60
|
+
"""Merge updates into existing config.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
updates: Configuration updates to merge
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Updated configuration
|
|
67
|
+
"""
|
|
68
|
+
config = load_config()
|
|
69
|
+
|
|
70
|
+
# Handle default_adapter
|
|
71
|
+
if "default_adapter" in updates:
|
|
72
|
+
config["default_adapter"] = updates["default_adapter"]
|
|
73
|
+
|
|
74
|
+
# Handle adapter-specific configurations
|
|
75
|
+
if "adapters" in updates:
|
|
76
|
+
if "adapters" not in config:
|
|
77
|
+
config["adapters"] = {}
|
|
78
|
+
for adapter_name, adapter_config in updates["adapters"].items():
|
|
79
|
+
if adapter_name not in config["adapters"]:
|
|
80
|
+
config["adapters"][adapter_name] = {}
|
|
81
|
+
config["adapters"][adapter_name].update(adapter_config)
|
|
82
|
+
|
|
83
|
+
return config
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_adapter(override_adapter: Optional[str] = None, override_config: Optional[dict] = None):
|
|
87
|
+
"""Get configured adapter instance.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
override_adapter: Override the default adapter type
|
|
91
|
+
override_config: Override configuration for the adapter
|
|
92
|
+
"""
|
|
93
|
+
config = load_config()
|
|
94
|
+
|
|
95
|
+
# Use override adapter if provided, otherwise use default
|
|
96
|
+
if override_adapter:
|
|
97
|
+
adapter_type = override_adapter
|
|
98
|
+
# If we have a stored config for this adapter, use it
|
|
99
|
+
adapters_config = config.get("adapters", {})
|
|
100
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
101
|
+
# Override with provided config if any
|
|
102
|
+
if override_config:
|
|
103
|
+
adapter_config.update(override_config)
|
|
104
|
+
else:
|
|
105
|
+
# Use default adapter from config
|
|
106
|
+
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
107
|
+
# Get config for the default adapter
|
|
108
|
+
adapters_config = config.get("adapters", {})
|
|
109
|
+
adapter_config = adapters_config.get(adapter_type, {})
|
|
110
|
+
|
|
111
|
+
# Fallback to legacy config format for backward compatibility
|
|
112
|
+
if not adapter_config and "config" in config:
|
|
113
|
+
adapter_config = config["config"]
|
|
114
|
+
|
|
115
|
+
# Add environment variables for authentication
|
|
116
|
+
import os
|
|
117
|
+
if adapter_type == "linear":
|
|
118
|
+
if not adapter_config.get("api_key"):
|
|
119
|
+
adapter_config["api_key"] = os.getenv("LINEAR_API_KEY")
|
|
120
|
+
elif adapter_type == "github":
|
|
121
|
+
if not adapter_config.get("api_key") and not adapter_config.get("token"):
|
|
122
|
+
adapter_config["api_key"] = os.getenv("GITHUB_TOKEN")
|
|
123
|
+
elif adapter_type == "jira":
|
|
124
|
+
if not adapter_config.get("api_token"):
|
|
125
|
+
adapter_config["api_token"] = os.getenv("JIRA_ACCESS_TOKEN")
|
|
126
|
+
if not adapter_config.get("email"):
|
|
127
|
+
adapter_config["email"] = os.getenv("JIRA_ACCESS_USER")
|
|
128
|
+
|
|
129
|
+
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.command()
|
|
133
|
+
def init(
|
|
134
|
+
adapter: AdapterType = typer.Option(
|
|
135
|
+
AdapterType.AITRACKDOWN,
|
|
136
|
+
"--adapter",
|
|
137
|
+
"-a",
|
|
138
|
+
help="Adapter type to use"
|
|
139
|
+
),
|
|
140
|
+
base_path: Optional[str] = typer.Option(
|
|
141
|
+
None,
|
|
142
|
+
"--base-path",
|
|
143
|
+
"-p",
|
|
144
|
+
help="Base path for ticket storage (AITrackdown only)"
|
|
145
|
+
),
|
|
146
|
+
api_key: Optional[str] = typer.Option(
|
|
147
|
+
None,
|
|
148
|
+
"--api-key",
|
|
149
|
+
help="API key for Linear or API token for JIRA"
|
|
150
|
+
),
|
|
151
|
+
team_id: Optional[str] = typer.Option(
|
|
152
|
+
None,
|
|
153
|
+
"--team-id",
|
|
154
|
+
help="Linear team ID (required for Linear adapter)"
|
|
155
|
+
),
|
|
156
|
+
jira_server: Optional[str] = typer.Option(
|
|
157
|
+
None,
|
|
158
|
+
"--jira-server",
|
|
159
|
+
help="JIRA server URL (e.g., https://company.atlassian.net)"
|
|
160
|
+
),
|
|
161
|
+
jira_email: Optional[str] = typer.Option(
|
|
162
|
+
None,
|
|
163
|
+
"--jira-email",
|
|
164
|
+
help="JIRA user email for authentication"
|
|
165
|
+
),
|
|
166
|
+
jira_project: Optional[str] = typer.Option(
|
|
167
|
+
None,
|
|
168
|
+
"--jira-project",
|
|
169
|
+
help="Default JIRA project key"
|
|
170
|
+
),
|
|
171
|
+
github_owner: Optional[str] = typer.Option(
|
|
172
|
+
None,
|
|
173
|
+
"--github-owner",
|
|
174
|
+
help="GitHub repository owner"
|
|
175
|
+
),
|
|
176
|
+
github_repo: Optional[str] = typer.Option(
|
|
177
|
+
None,
|
|
178
|
+
"--github-repo",
|
|
179
|
+
help="GitHub repository name"
|
|
180
|
+
),
|
|
181
|
+
github_token: Optional[str] = typer.Option(
|
|
182
|
+
None,
|
|
183
|
+
"--github-token",
|
|
184
|
+
help="GitHub Personal Access Token"
|
|
185
|
+
),
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Initialize MCP Ticketer configuration."""
|
|
188
|
+
config = {
|
|
189
|
+
"default_adapter": adapter.value,
|
|
190
|
+
"adapters": {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if adapter == AdapterType.AITRACKDOWN:
|
|
194
|
+
config["adapters"]["aitrackdown"] = {"base_path": base_path or ".aitrackdown"}
|
|
195
|
+
elif adapter == AdapterType.LINEAR:
|
|
196
|
+
# For Linear, we need team_id and optionally api_key
|
|
197
|
+
if not team_id:
|
|
198
|
+
console.print("[red]Error:[/red] --team-id is required for Linear adapter")
|
|
199
|
+
raise typer.Exit(1)
|
|
200
|
+
|
|
201
|
+
config["adapters"]["linear"] = {"team_id": team_id}
|
|
202
|
+
|
|
203
|
+
# Check for API key in environment or parameter
|
|
204
|
+
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
205
|
+
if not linear_api_key:
|
|
206
|
+
console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
|
|
207
|
+
console.print("Set LINEAR_API_KEY environment variable or use --api-key option")
|
|
208
|
+
else:
|
|
209
|
+
config["adapters"]["linear"]["api_key"] = linear_api_key
|
|
210
|
+
|
|
211
|
+
elif adapter == AdapterType.JIRA:
|
|
212
|
+
# For JIRA, we need server, email, and API token
|
|
213
|
+
server = jira_server or os.getenv("JIRA_SERVER")
|
|
214
|
+
email = jira_email or os.getenv("JIRA_EMAIL")
|
|
215
|
+
token = api_key or os.getenv("JIRA_API_TOKEN")
|
|
216
|
+
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
217
|
+
|
|
218
|
+
if not server:
|
|
219
|
+
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
220
|
+
console.print("Use --jira-server or set JIRA_SERVER environment variable")
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
|
|
223
|
+
if not email:
|
|
224
|
+
console.print("[red]Error:[/red] JIRA email is required")
|
|
225
|
+
console.print("Use --jira-email or set JIRA_EMAIL environment variable")
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
if not token:
|
|
229
|
+
console.print("[red]Error:[/red] JIRA API token is required")
|
|
230
|
+
console.print("Use --api-key or set JIRA_API_TOKEN environment variable")
|
|
231
|
+
console.print("[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]")
|
|
232
|
+
raise typer.Exit(1)
|
|
233
|
+
|
|
234
|
+
config["adapters"]["jira"] = {
|
|
235
|
+
"server": server,
|
|
236
|
+
"email": email,
|
|
237
|
+
"api_token": token
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if project:
|
|
241
|
+
config["adapters"]["jira"]["project_key"] = project
|
|
242
|
+
else:
|
|
243
|
+
console.print("[yellow]Warning:[/yellow] No default project key specified")
|
|
244
|
+
console.print("You may need to specify project key for some operations")
|
|
245
|
+
|
|
246
|
+
elif adapter == AdapterType.GITHUB:
|
|
247
|
+
# For GitHub, we need owner, repo, and token
|
|
248
|
+
owner = github_owner or os.getenv("GITHUB_OWNER")
|
|
249
|
+
repo = github_repo or os.getenv("GITHUB_REPO")
|
|
250
|
+
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
251
|
+
|
|
252
|
+
if not owner:
|
|
253
|
+
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
254
|
+
console.print("Use --github-owner or set GITHUB_OWNER environment variable")
|
|
255
|
+
raise typer.Exit(1)
|
|
256
|
+
|
|
257
|
+
if not repo:
|
|
258
|
+
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
259
|
+
console.print("Use --github-repo or set GITHUB_REPO environment variable")
|
|
260
|
+
raise typer.Exit(1)
|
|
261
|
+
|
|
262
|
+
if not token:
|
|
263
|
+
console.print("[red]Error:[/red] GitHub Personal Access Token is required")
|
|
264
|
+
console.print("Use --github-token or set GITHUB_TOKEN environment variable")
|
|
265
|
+
console.print("[dim]Create token at: https://github.com/settings/tokens/new[/dim]")
|
|
266
|
+
console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]")
|
|
267
|
+
raise typer.Exit(1)
|
|
268
|
+
|
|
269
|
+
config["adapters"]["github"] = {
|
|
270
|
+
"owner": owner,
|
|
271
|
+
"repo": repo,
|
|
272
|
+
"token": token
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
save_config(config)
|
|
276
|
+
console.print(f"[green]✓[/green] Initialized with {adapter.value} adapter")
|
|
277
|
+
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@app.command("set")
|
|
281
|
+
def set_config(
|
|
282
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
283
|
+
None,
|
|
284
|
+
"--adapter",
|
|
285
|
+
"-a",
|
|
286
|
+
help="Set default adapter"
|
|
287
|
+
),
|
|
288
|
+
team_key: Optional[str] = typer.Option(
|
|
289
|
+
None,
|
|
290
|
+
"--team-key",
|
|
291
|
+
help="Linear team key (e.g., BTA)"
|
|
292
|
+
),
|
|
293
|
+
team_id: Optional[str] = typer.Option(
|
|
294
|
+
None,
|
|
295
|
+
"--team-id",
|
|
296
|
+
help="Linear team ID"
|
|
297
|
+
),
|
|
298
|
+
owner: Optional[str] = typer.Option(
|
|
299
|
+
None,
|
|
300
|
+
"--owner",
|
|
301
|
+
help="GitHub repository owner"
|
|
302
|
+
),
|
|
303
|
+
repo: Optional[str] = typer.Option(
|
|
304
|
+
None,
|
|
305
|
+
"--repo",
|
|
306
|
+
help="GitHub repository name"
|
|
307
|
+
),
|
|
308
|
+
server: Optional[str] = typer.Option(
|
|
309
|
+
None,
|
|
310
|
+
"--server",
|
|
311
|
+
help="JIRA server URL"
|
|
312
|
+
),
|
|
313
|
+
project: Optional[str] = typer.Option(
|
|
314
|
+
None,
|
|
315
|
+
"--project",
|
|
316
|
+
help="JIRA project key"
|
|
317
|
+
),
|
|
318
|
+
base_path: Optional[str] = typer.Option(
|
|
319
|
+
None,
|
|
320
|
+
"--base-path",
|
|
321
|
+
help="AITrackdown base path"
|
|
322
|
+
),
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Set default adapter and adapter-specific configuration.
|
|
325
|
+
|
|
326
|
+
When called without arguments, shows current configuration.
|
|
327
|
+
"""
|
|
328
|
+
if not any([adapter, team_key, team_id, owner, repo, server, project, base_path]):
|
|
329
|
+
# Show current configuration
|
|
330
|
+
config = load_config()
|
|
331
|
+
console.print("[bold]Current Configuration:[/bold]")
|
|
332
|
+
console.print(f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]")
|
|
333
|
+
|
|
334
|
+
adapters_config = config.get("adapters", {})
|
|
335
|
+
if adapters_config:
|
|
336
|
+
console.print("\n[bold]Adapter Settings:[/bold]")
|
|
337
|
+
for adapter_name, adapter_config in adapters_config.items():
|
|
338
|
+
console.print(f"\n[cyan]{adapter_name}:[/cyan]")
|
|
339
|
+
for key, value in adapter_config.items():
|
|
340
|
+
# Don't display sensitive values like tokens
|
|
341
|
+
if "token" in key.lower() or "key" in key.lower() and "team" not in key.lower():
|
|
342
|
+
value = "***" if value else "not set"
|
|
343
|
+
console.print(f" {key}: {value}")
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
updates = {}
|
|
347
|
+
|
|
348
|
+
# Set default adapter
|
|
349
|
+
if adapter:
|
|
350
|
+
updates["default_adapter"] = adapter.value
|
|
351
|
+
console.print(f"[green]✓[/green] Default adapter set to: {adapter.value}")
|
|
352
|
+
|
|
353
|
+
# Build adapter-specific configuration
|
|
354
|
+
adapter_configs = {}
|
|
355
|
+
|
|
356
|
+
# Linear configuration
|
|
357
|
+
if team_key or team_id:
|
|
358
|
+
linear_config = {}
|
|
359
|
+
if team_key:
|
|
360
|
+
linear_config["team_key"] = team_key
|
|
361
|
+
if team_id:
|
|
362
|
+
linear_config["team_id"] = team_id
|
|
363
|
+
adapter_configs["linear"] = linear_config
|
|
364
|
+
console.print(f"[green]✓[/green] Linear settings updated")
|
|
365
|
+
|
|
366
|
+
# GitHub configuration
|
|
367
|
+
if owner or repo:
|
|
368
|
+
github_config = {}
|
|
369
|
+
if owner:
|
|
370
|
+
github_config["owner"] = owner
|
|
371
|
+
if repo:
|
|
372
|
+
github_config["repo"] = repo
|
|
373
|
+
adapter_configs["github"] = github_config
|
|
374
|
+
console.print(f"[green]✓[/green] GitHub settings updated")
|
|
375
|
+
|
|
376
|
+
# JIRA configuration
|
|
377
|
+
if server or project:
|
|
378
|
+
jira_config = {}
|
|
379
|
+
if server:
|
|
380
|
+
jira_config["server"] = server
|
|
381
|
+
if project:
|
|
382
|
+
jira_config["project_key"] = project
|
|
383
|
+
adapter_configs["jira"] = jira_config
|
|
384
|
+
console.print(f"[green]✓[/green] JIRA settings updated")
|
|
385
|
+
|
|
386
|
+
# AITrackdown configuration
|
|
387
|
+
if base_path:
|
|
388
|
+
adapter_configs["aitrackdown"] = {"base_path": base_path}
|
|
389
|
+
console.print(f"[green]✓[/green] AITrackdown settings updated")
|
|
390
|
+
|
|
391
|
+
if adapter_configs:
|
|
392
|
+
updates["adapters"] = adapter_configs
|
|
393
|
+
|
|
394
|
+
# Merge and save configuration
|
|
395
|
+
if updates:
|
|
396
|
+
config = merge_config(updates)
|
|
397
|
+
save_config(config)
|
|
398
|
+
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@app.command("status")
|
|
402
|
+
def status_command():
|
|
403
|
+
"""Show queue and worker status."""
|
|
404
|
+
queue = Queue()
|
|
405
|
+
manager = WorkerManager()
|
|
406
|
+
|
|
407
|
+
# Get queue stats
|
|
408
|
+
stats = queue.get_stats()
|
|
409
|
+
pending = stats.get(QueueStatus.PENDING.value, 0)
|
|
410
|
+
|
|
411
|
+
# Show queue status
|
|
412
|
+
console.print("[bold]Queue Status:[/bold]")
|
|
413
|
+
console.print(f" Pending: {pending}")
|
|
414
|
+
console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
|
|
415
|
+
console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
|
|
416
|
+
console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
|
|
417
|
+
|
|
418
|
+
# Show worker status
|
|
419
|
+
worker_status = manager.get_status()
|
|
420
|
+
if worker_status["running"]:
|
|
421
|
+
console.print(f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})")
|
|
422
|
+
else:
|
|
423
|
+
console.print("\n[red]○ Worker is not running[/red]")
|
|
424
|
+
if pending > 0:
|
|
425
|
+
console.print("[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@app.command()
|
|
429
|
+
def create(
|
|
430
|
+
title: str = typer.Argument(..., help="Ticket title"),
|
|
431
|
+
description: Optional[str] = typer.Option(
|
|
432
|
+
None,
|
|
433
|
+
"--description",
|
|
434
|
+
"-d",
|
|
435
|
+
help="Ticket description"
|
|
436
|
+
),
|
|
437
|
+
priority: Priority = typer.Option(
|
|
438
|
+
Priority.MEDIUM,
|
|
439
|
+
"--priority",
|
|
440
|
+
"-p",
|
|
441
|
+
help="Priority level"
|
|
442
|
+
),
|
|
443
|
+
tags: Optional[List[str]] = typer.Option(
|
|
444
|
+
None,
|
|
445
|
+
"--tag",
|
|
446
|
+
"-t",
|
|
447
|
+
help="Tags (can be specified multiple times)"
|
|
448
|
+
),
|
|
449
|
+
assignee: Optional[str] = typer.Option(
|
|
450
|
+
None,
|
|
451
|
+
"--assignee",
|
|
452
|
+
"-a",
|
|
453
|
+
help="Assignee username"
|
|
454
|
+
),
|
|
455
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
456
|
+
None,
|
|
457
|
+
"--adapter",
|
|
458
|
+
help="Override default adapter"
|
|
459
|
+
),
|
|
460
|
+
) -> None:
|
|
461
|
+
"""Create a new ticket."""
|
|
462
|
+
# Get the adapter name
|
|
463
|
+
config = load_config()
|
|
464
|
+
adapter_name = adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
465
|
+
|
|
466
|
+
# Create task data
|
|
467
|
+
task_data = {
|
|
468
|
+
"title": title,
|
|
469
|
+
"description": description,
|
|
470
|
+
"priority": priority.value if isinstance(priority, Priority) else priority,
|
|
471
|
+
"tags": tags or [],
|
|
472
|
+
"assignee": assignee,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
# Add to queue
|
|
476
|
+
queue = Queue()
|
|
477
|
+
queue_id = queue.add(
|
|
478
|
+
ticket_data=task_data,
|
|
479
|
+
adapter=adapter_name,
|
|
480
|
+
operation="create"
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
484
|
+
console.print(f" Title: {title}")
|
|
485
|
+
console.print(f" Priority: {priority}")
|
|
486
|
+
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
487
|
+
|
|
488
|
+
# Start worker if needed
|
|
489
|
+
manager = WorkerManager()
|
|
490
|
+
if manager.start_if_needed():
|
|
491
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@app.command("list")
|
|
495
|
+
def list_tickets(
|
|
496
|
+
state: Optional[TicketState] = typer.Option(
|
|
497
|
+
None,
|
|
498
|
+
"--state",
|
|
499
|
+
"-s",
|
|
500
|
+
help="Filter by state"
|
|
501
|
+
),
|
|
502
|
+
priority: Optional[Priority] = typer.Option(
|
|
503
|
+
None,
|
|
504
|
+
"--priority",
|
|
505
|
+
"-p",
|
|
506
|
+
help="Filter by priority"
|
|
507
|
+
),
|
|
508
|
+
limit: int = typer.Option(
|
|
509
|
+
10,
|
|
510
|
+
"--limit",
|
|
511
|
+
"-l",
|
|
512
|
+
help="Maximum number of tickets"
|
|
513
|
+
),
|
|
514
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
515
|
+
None,
|
|
516
|
+
"--adapter",
|
|
517
|
+
help="Override default adapter"
|
|
518
|
+
),
|
|
519
|
+
) -> None:
|
|
520
|
+
"""List tickets with optional filters."""
|
|
521
|
+
async def _list():
|
|
522
|
+
adapter_instance = get_adapter(override_adapter=adapter.value if adapter else None)
|
|
523
|
+
filters = {}
|
|
524
|
+
if state:
|
|
525
|
+
filters["state"] = state
|
|
526
|
+
if priority:
|
|
527
|
+
filters["priority"] = priority
|
|
528
|
+
return await adapter_instance.list(limit=limit, filters=filters)
|
|
529
|
+
|
|
530
|
+
tickets = asyncio.run(_list())
|
|
531
|
+
|
|
532
|
+
if not tickets:
|
|
533
|
+
console.print("[yellow]No tickets found[/yellow]")
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
# Create table
|
|
537
|
+
table = Table(title="Tickets")
|
|
538
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
539
|
+
table.add_column("Title", style="white")
|
|
540
|
+
table.add_column("State", style="green")
|
|
541
|
+
table.add_column("Priority", style="yellow")
|
|
542
|
+
table.add_column("Assignee", style="blue")
|
|
543
|
+
|
|
544
|
+
for ticket in tickets:
|
|
545
|
+
table.add_row(
|
|
546
|
+
ticket.id or "N/A",
|
|
547
|
+
ticket.title,
|
|
548
|
+
ticket.state,
|
|
549
|
+
ticket.priority,
|
|
550
|
+
ticket.assignee or "-",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
console.print(table)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@app.command()
|
|
557
|
+
def show(
|
|
558
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
559
|
+
comments: bool = typer.Option(
|
|
560
|
+
False,
|
|
561
|
+
"--comments",
|
|
562
|
+
"-c",
|
|
563
|
+
help="Show comments"
|
|
564
|
+
),
|
|
565
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
566
|
+
None,
|
|
567
|
+
"--adapter",
|
|
568
|
+
help="Override default adapter"
|
|
569
|
+
),
|
|
570
|
+
) -> None:
|
|
571
|
+
"""Show detailed ticket information."""
|
|
572
|
+
async def _show():
|
|
573
|
+
adapter_instance = get_adapter(override_adapter=adapter.value if adapter else None)
|
|
574
|
+
ticket = await adapter_instance.read(ticket_id)
|
|
575
|
+
ticket_comments = None
|
|
576
|
+
if comments and ticket:
|
|
577
|
+
ticket_comments = await adapter_instance.get_comments(ticket_id)
|
|
578
|
+
return ticket, ticket_comments
|
|
579
|
+
|
|
580
|
+
ticket, ticket_comments = asyncio.run(_show())
|
|
581
|
+
|
|
582
|
+
if not ticket:
|
|
583
|
+
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
584
|
+
raise typer.Exit(1)
|
|
585
|
+
|
|
586
|
+
# Display ticket details
|
|
587
|
+
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
588
|
+
console.print(f"Title: {ticket.title}")
|
|
589
|
+
console.print(f"State: [green]{ticket.state}[/green]")
|
|
590
|
+
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
591
|
+
|
|
592
|
+
if ticket.description:
|
|
593
|
+
console.print(f"\n[dim]Description:[/dim]")
|
|
594
|
+
console.print(ticket.description)
|
|
595
|
+
|
|
596
|
+
if ticket.tags:
|
|
597
|
+
console.print(f"\nTags: {', '.join(ticket.tags)}")
|
|
598
|
+
|
|
599
|
+
if ticket.assignee:
|
|
600
|
+
console.print(f"Assignee: {ticket.assignee}")
|
|
601
|
+
|
|
602
|
+
# Display comments if requested
|
|
603
|
+
if ticket_comments:
|
|
604
|
+
console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
|
|
605
|
+
for comment in ticket_comments:
|
|
606
|
+
console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
|
|
607
|
+
console.print(comment.content)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@app.command()
|
|
611
|
+
def update(
|
|
612
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
613
|
+
title: Optional[str] = typer.Option(None, "--title", help="New title"),
|
|
614
|
+
description: Optional[str] = typer.Option(
|
|
615
|
+
None,
|
|
616
|
+
"--description",
|
|
617
|
+
"-d",
|
|
618
|
+
help="New description"
|
|
619
|
+
),
|
|
620
|
+
priority: Optional[Priority] = typer.Option(
|
|
621
|
+
None,
|
|
622
|
+
"--priority",
|
|
623
|
+
"-p",
|
|
624
|
+
help="New priority"
|
|
625
|
+
),
|
|
626
|
+
assignee: Optional[str] = typer.Option(
|
|
627
|
+
None,
|
|
628
|
+
"--assignee",
|
|
629
|
+
"-a",
|
|
630
|
+
help="New assignee"
|
|
631
|
+
),
|
|
632
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
633
|
+
None,
|
|
634
|
+
"--adapter",
|
|
635
|
+
help="Override default adapter"
|
|
636
|
+
),
|
|
637
|
+
) -> None:
|
|
638
|
+
"""Update ticket fields."""
|
|
639
|
+
updates = {}
|
|
640
|
+
if title:
|
|
641
|
+
updates["title"] = title
|
|
642
|
+
if description:
|
|
643
|
+
updates["description"] = description
|
|
644
|
+
if priority:
|
|
645
|
+
updates["priority"] = priority.value if isinstance(priority, Priority) else priority
|
|
646
|
+
if assignee:
|
|
647
|
+
updates["assignee"] = assignee
|
|
648
|
+
|
|
649
|
+
if not updates:
|
|
650
|
+
console.print("[yellow]No updates specified[/yellow]")
|
|
651
|
+
raise typer.Exit(1)
|
|
652
|
+
|
|
653
|
+
# Get the adapter name
|
|
654
|
+
config = load_config()
|
|
655
|
+
adapter_name = adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
656
|
+
|
|
657
|
+
# Add ticket_id to updates
|
|
658
|
+
updates["ticket_id"] = ticket_id
|
|
659
|
+
|
|
660
|
+
# Add to queue
|
|
661
|
+
queue = Queue()
|
|
662
|
+
queue_id = queue.add(
|
|
663
|
+
ticket_data=updates,
|
|
664
|
+
adapter=adapter_name,
|
|
665
|
+
operation="update"
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
669
|
+
for key, value in updates.items():
|
|
670
|
+
if key != "ticket_id":
|
|
671
|
+
console.print(f" {key}: {value}")
|
|
672
|
+
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
673
|
+
|
|
674
|
+
# Start worker if needed
|
|
675
|
+
manager = WorkerManager()
|
|
676
|
+
if manager.start_if_needed():
|
|
677
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@app.command()
|
|
681
|
+
def transition(
|
|
682
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
683
|
+
state: TicketState = typer.Argument(..., help="Target state"),
|
|
684
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
685
|
+
None,
|
|
686
|
+
"--adapter",
|
|
687
|
+
help="Override default adapter"
|
|
688
|
+
),
|
|
689
|
+
) -> None:
|
|
690
|
+
"""Change ticket state with validation."""
|
|
691
|
+
# Get the adapter name
|
|
692
|
+
config = load_config()
|
|
693
|
+
adapter_name = adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
694
|
+
|
|
695
|
+
# Add to queue
|
|
696
|
+
queue = Queue()
|
|
697
|
+
queue_id = queue.add(
|
|
698
|
+
ticket_data={
|
|
699
|
+
"ticket_id": ticket_id,
|
|
700
|
+
"state": state.value if hasattr(state, 'value') else state
|
|
701
|
+
},
|
|
702
|
+
adapter=adapter_name,
|
|
703
|
+
operation="transition"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
707
|
+
console.print(f" Ticket: {ticket_id} → {state}")
|
|
708
|
+
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
709
|
+
|
|
710
|
+
# Start worker if needed
|
|
711
|
+
manager = WorkerManager()
|
|
712
|
+
if manager.start_if_needed():
|
|
713
|
+
console.print("[dim]Worker started to process request[/dim]")
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@app.command()
|
|
717
|
+
def search(
|
|
718
|
+
query: Optional[str] = typer.Argument(None, help="Search query"),
|
|
719
|
+
state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
|
|
720
|
+
priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
|
|
721
|
+
assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
|
|
722
|
+
limit: int = typer.Option(10, "--limit", "-l"),
|
|
723
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
724
|
+
None,
|
|
725
|
+
"--adapter",
|
|
726
|
+
help="Override default adapter"
|
|
727
|
+
),
|
|
728
|
+
) -> None:
|
|
729
|
+
"""Search tickets with advanced query."""
|
|
730
|
+
async def _search():
|
|
731
|
+
adapter_instance = get_adapter(override_adapter=adapter.value if adapter else None)
|
|
732
|
+
search_query = SearchQuery(
|
|
733
|
+
query=query,
|
|
734
|
+
state=state,
|
|
735
|
+
priority=priority,
|
|
736
|
+
assignee=assignee,
|
|
737
|
+
limit=limit,
|
|
738
|
+
)
|
|
739
|
+
return await adapter_instance.search(search_query)
|
|
740
|
+
|
|
741
|
+
tickets = asyncio.run(_search())
|
|
742
|
+
|
|
743
|
+
if not tickets:
|
|
744
|
+
console.print("[yellow]No tickets found matching query[/yellow]")
|
|
745
|
+
return
|
|
746
|
+
|
|
747
|
+
# Display results
|
|
748
|
+
console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
|
|
749
|
+
|
|
750
|
+
for ticket in tickets:
|
|
751
|
+
console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
|
|
752
|
+
console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
|
|
753
|
+
if ticket.assignee:
|
|
754
|
+
console.print(f" Assignee: {ticket.assignee}")
|
|
755
|
+
console.print()
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
# Add queue command to main app
|
|
759
|
+
app.add_typer(queue_app, name="queue")
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@app.command()
|
|
763
|
+
def check(
|
|
764
|
+
queue_id: str = typer.Argument(..., help="Queue ID to check")
|
|
765
|
+
):
|
|
766
|
+
"""Check status of a queued operation."""
|
|
767
|
+
queue = Queue()
|
|
768
|
+
item = queue.get_item(queue_id)
|
|
769
|
+
|
|
770
|
+
if not item:
|
|
771
|
+
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
772
|
+
raise typer.Exit(1)
|
|
773
|
+
|
|
774
|
+
# Display status
|
|
775
|
+
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
776
|
+
console.print(f"Operation: {item.operation}")
|
|
777
|
+
console.print(f"Adapter: {item.adapter}")
|
|
778
|
+
|
|
779
|
+
# Status with color
|
|
780
|
+
if item.status == QueueStatus.COMPLETED:
|
|
781
|
+
console.print(f"Status: [green]{item.status}[/green]")
|
|
782
|
+
elif item.status == QueueStatus.FAILED:
|
|
783
|
+
console.print(f"Status: [red]{item.status}[/red]")
|
|
784
|
+
elif item.status == QueueStatus.PROCESSING:
|
|
785
|
+
console.print(f"Status: [yellow]{item.status}[/yellow]")
|
|
786
|
+
else:
|
|
787
|
+
console.print(f"Status: {item.status}")
|
|
788
|
+
|
|
789
|
+
# Timestamps
|
|
790
|
+
console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
791
|
+
if item.processed_at:
|
|
792
|
+
console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
793
|
+
|
|
794
|
+
# Error or result
|
|
795
|
+
if item.error_message:
|
|
796
|
+
console.print(f"\n[red]Error:[/red] {item.error_message}")
|
|
797
|
+
elif item.result:
|
|
798
|
+
console.print(f"\n[green]Result:[/green]")
|
|
799
|
+
for key, value in item.result.items():
|
|
800
|
+
console.print(f" {key}: {value}")
|
|
801
|
+
|
|
802
|
+
if item.retry_count > 0:
|
|
803
|
+
console.print(f"\nRetry Count: {item.retry_count}")
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def main():
|
|
807
|
+
"""Main entry point."""
|
|
808
|
+
app()
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
if __name__ == "__main__":
|
|
812
|
+
main()
|