mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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 +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +66 -49
- mcp_ticketer/adapters/github.py +192 -125
- mcp_ticketer/adapters/hybrid.py +99 -53
- mcp_ticketer/adapters/jira.py +161 -151
- mcp_ticketer/adapters/linear.py +396 -246
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +15 -16
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +283 -298
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +11 -13
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +121 -66
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +46 -39
- mcp_ticketer/core/config.py +128 -92
- mcp_ticketer/core/env_discovery.py +69 -37
- mcp_ticketer/core/http_client.py +57 -40
- mcp_ticketer/core/mappers.py +98 -54
- mcp_ticketer/core/models.py +38 -24
- mcp_ticketer/core/project_config.py +145 -80
- mcp_ticketer/core/registry.py +16 -16
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +199 -145
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +30 -26
- mcp_ticketer/queue/queue.py +147 -85
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +55 -40
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -3,25 +3,23 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional, List
|
|
8
6
|
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
|
+
from dotenv import load_dotenv
|
|
11
12
|
from rich.console import Console
|
|
12
13
|
from rich.table import Table
|
|
13
|
-
from rich import print as rprint
|
|
14
|
-
from dotenv import load_dotenv
|
|
15
14
|
|
|
16
|
-
from ..
|
|
15
|
+
from ..__version__ import __version__
|
|
16
|
+
from ..core import AdapterRegistry, Priority, TicketState
|
|
17
17
|
from ..core.models import SearchQuery
|
|
18
|
-
from ..adapters import AITrackdownAdapter
|
|
19
18
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
20
|
-
from .
|
|
21
|
-
from ..__version__ import __version__
|
|
22
|
-
from .configure import configure_wizard, show_current_config, set_adapter_config
|
|
23
|
-
from .migrate_config import migrate_config_command
|
|
19
|
+
from .configure import configure_wizard, set_adapter_config, show_current_config
|
|
24
20
|
from .discover import app as discover_app
|
|
21
|
+
from .migrate_config import migrate_config_command
|
|
22
|
+
from .queue_commands import app as queue_app
|
|
25
23
|
|
|
26
24
|
# Load environment variables from .env files
|
|
27
25
|
# Priority: .env.local (highest) > .env (base)
|
|
@@ -58,21 +56,20 @@ def main_callback(
|
|
|
58
56
|
"-v",
|
|
59
57
|
callback=version_callback,
|
|
60
58
|
is_eager=True,
|
|
61
|
-
help="Show version and exit"
|
|
59
|
+
help="Show version and exit",
|
|
62
60
|
),
|
|
63
61
|
):
|
|
64
|
-
"""
|
|
65
|
-
MCP Ticketer - Universal ticket management interface.
|
|
66
|
-
"""
|
|
62
|
+
"""MCP Ticketer - Universal ticket management interface."""
|
|
67
63
|
pass
|
|
68
64
|
|
|
69
65
|
|
|
70
|
-
# Configuration file management
|
|
71
|
-
CONFIG_FILE = Path.
|
|
66
|
+
# Configuration file management - PROJECT-LOCAL ONLY
|
|
67
|
+
CONFIG_FILE = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
72
68
|
|
|
73
69
|
|
|
74
70
|
class AdapterType(str, Enum):
|
|
75
71
|
"""Available adapter types."""
|
|
72
|
+
|
|
76
73
|
AITRACKDOWN = "aitrackdown"
|
|
77
74
|
LINEAR = "linear"
|
|
78
75
|
JIRA = "jira"
|
|
@@ -80,48 +77,82 @@ class AdapterType(str, Enum):
|
|
|
80
77
|
|
|
81
78
|
|
|
82
79
|
def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
83
|
-
"""Load configuration from file.
|
|
80
|
+
"""Load configuration from project-local config file ONLY.
|
|
81
|
+
|
|
82
|
+
SECURITY: This method ONLY reads from the current project directory
|
|
83
|
+
to prevent configuration leakage across projects. It will NEVER read
|
|
84
|
+
from user home directory or system-wide locations.
|
|
84
85
|
|
|
85
86
|
Args:
|
|
86
87
|
project_dir: Optional project directory to load config from
|
|
87
88
|
|
|
88
89
|
Resolution order:
|
|
89
90
|
1. Project-specific config (.mcp-ticketer/config.json in project_dir or cwd)
|
|
90
|
-
2.
|
|
91
|
+
2. Default to aitrackdown adapter
|
|
91
92
|
|
|
92
93
|
Returns:
|
|
93
|
-
Configuration dictionary
|
|
94
|
+
Configuration dictionary with adapter and config keys.
|
|
95
|
+
Defaults to aitrackdown if no local config exists.
|
|
96
|
+
|
|
94
97
|
"""
|
|
98
|
+
import logging
|
|
99
|
+
|
|
100
|
+
logger = logging.getLogger(__name__)
|
|
101
|
+
|
|
95
102
|
# Use provided project_dir or current working directory
|
|
96
103
|
base_dir = project_dir or Path.cwd()
|
|
97
104
|
|
|
98
|
-
#
|
|
105
|
+
# ONLY check project-specific config in project directory
|
|
99
106
|
project_config = base_dir / ".mcp-ticketer" / "config.json"
|
|
100
107
|
if project_config.exists():
|
|
108
|
+
# Validate that config file is actually in project directory
|
|
101
109
|
try:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.print(f"[yellow]Warning: Could not load global config: {e}[/yellow]")
|
|
110
|
+
if not project_config.resolve().is_relative_to(base_dir.resolve()):
|
|
111
|
+
logger.error(
|
|
112
|
+
f"Security violation: Config file {project_config} "
|
|
113
|
+
"is not within project directory"
|
|
114
|
+
)
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Security violation: Config file {project_config} "
|
|
117
|
+
"is not within project directory"
|
|
118
|
+
)
|
|
119
|
+
except (ValueError, RuntimeError):
|
|
120
|
+
# is_relative_to may raise ValueError in some cases
|
|
121
|
+
pass
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
try:
|
|
124
|
+
with open(project_config) as f:
|
|
125
|
+
config = json.load(f)
|
|
126
|
+
logger.info(
|
|
127
|
+
f"Loaded configuration from project-local: {project_config}"
|
|
128
|
+
)
|
|
129
|
+
return config
|
|
130
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
131
|
+
logger.warning(f"Could not load project config: {e}, using defaults")
|
|
132
|
+
console.print(
|
|
133
|
+
f"[yellow]Warning: Could not load project config: {e}[/yellow]"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Default to aitrackdown with local base path
|
|
137
|
+
logger.info("No project-local config found, defaulting to aitrackdown adapter")
|
|
117
138
|
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
118
139
|
|
|
119
140
|
|
|
120
141
|
def save_config(config: dict) -> None:
|
|
121
|
-
"""Save configuration to file.
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
"""Save configuration to project-local config file ONLY.
|
|
143
|
+
|
|
144
|
+
SECURITY: This method ONLY saves to the current project directory
|
|
145
|
+
to prevent configuration leakage across projects.
|
|
146
|
+
"""
|
|
147
|
+
import logging
|
|
148
|
+
|
|
149
|
+
logger = logging.getLogger(__name__)
|
|
150
|
+
|
|
151
|
+
project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
152
|
+
project_config.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
with open(project_config, "w") as f:
|
|
124
154
|
json.dump(config, f, indent=2)
|
|
155
|
+
logger.info(f"Saved configuration to project-local: {project_config}")
|
|
125
156
|
|
|
126
157
|
|
|
127
158
|
def merge_config(updates: dict) -> dict:
|
|
@@ -132,6 +163,7 @@ def merge_config(updates: dict) -> dict:
|
|
|
132
163
|
|
|
133
164
|
Returns:
|
|
134
165
|
Updated configuration
|
|
166
|
+
|
|
135
167
|
"""
|
|
136
168
|
config = load_config()
|
|
137
169
|
|
|
@@ -151,12 +183,15 @@ def merge_config(updates: dict) -> dict:
|
|
|
151
183
|
return config
|
|
152
184
|
|
|
153
185
|
|
|
154
|
-
def get_adapter(
|
|
186
|
+
def get_adapter(
|
|
187
|
+
override_adapter: Optional[str] = None, override_config: Optional[dict] = None
|
|
188
|
+
):
|
|
155
189
|
"""Get configured adapter instance.
|
|
156
190
|
|
|
157
191
|
Args:
|
|
158
192
|
override_adapter: Override the default adapter type
|
|
159
193
|
override_config: Override configuration for the adapter
|
|
194
|
+
|
|
160
195
|
"""
|
|
161
196
|
config = load_config()
|
|
162
197
|
|
|
@@ -182,6 +217,7 @@ def get_adapter(override_adapter: Optional[str] = None, override_config: Optiona
|
|
|
182
217
|
|
|
183
218
|
# Add environment variables for authentication
|
|
184
219
|
import os
|
|
220
|
+
|
|
185
221
|
if adapter_type == "linear":
|
|
186
222
|
if not adapter_config.get("api_key"):
|
|
187
223
|
adapter_config["api_key"] = os.getenv("LINEAR_API_KEY")
|
|
@@ -203,64 +239,48 @@ def init(
|
|
|
203
239
|
None,
|
|
204
240
|
"--adapter",
|
|
205
241
|
"-a",
|
|
206
|
-
help="Adapter type to use (auto-detected from .env if not specified)"
|
|
242
|
+
help="Adapter type to use (auto-detected from .env if not specified)",
|
|
207
243
|
),
|
|
208
244
|
project_path: Optional[str] = typer.Option(
|
|
209
|
-
None,
|
|
210
|
-
"--path",
|
|
211
|
-
help="Project path (default: current directory)"
|
|
245
|
+
None, "--path", help="Project path (default: current directory)"
|
|
212
246
|
),
|
|
213
247
|
global_config: bool = typer.Option(
|
|
214
248
|
False,
|
|
215
249
|
"--global",
|
|
216
250
|
"-g",
|
|
217
|
-
help="Save to global config instead of project-specific"
|
|
251
|
+
help="Save to global config instead of project-specific",
|
|
218
252
|
),
|
|
219
253
|
base_path: Optional[str] = typer.Option(
|
|
220
254
|
None,
|
|
221
255
|
"--base-path",
|
|
222
256
|
"-p",
|
|
223
|
-
help="Base path for ticket storage (AITrackdown only)"
|
|
257
|
+
help="Base path for ticket storage (AITrackdown only)",
|
|
224
258
|
),
|
|
225
259
|
api_key: Optional[str] = typer.Option(
|
|
226
|
-
None,
|
|
227
|
-
"--api-key",
|
|
228
|
-
help="API key for Linear or API token for JIRA"
|
|
260
|
+
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
229
261
|
),
|
|
230
262
|
team_id: Optional[str] = typer.Option(
|
|
231
|
-
None,
|
|
232
|
-
"--team-id",
|
|
233
|
-
help="Linear team ID (required for Linear adapter)"
|
|
263
|
+
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
234
264
|
),
|
|
235
265
|
jira_server: Optional[str] = typer.Option(
|
|
236
266
|
None,
|
|
237
267
|
"--jira-server",
|
|
238
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)"
|
|
268
|
+
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
239
269
|
),
|
|
240
270
|
jira_email: Optional[str] = typer.Option(
|
|
241
|
-
None,
|
|
242
|
-
"--jira-email",
|
|
243
|
-
help="JIRA user email for authentication"
|
|
271
|
+
None, "--jira-email", help="JIRA user email for authentication"
|
|
244
272
|
),
|
|
245
273
|
jira_project: Optional[str] = typer.Option(
|
|
246
|
-
None,
|
|
247
|
-
"--jira-project",
|
|
248
|
-
help="Default JIRA project key"
|
|
274
|
+
None, "--jira-project", help="Default JIRA project key"
|
|
249
275
|
),
|
|
250
276
|
github_owner: Optional[str] = typer.Option(
|
|
251
|
-
None,
|
|
252
|
-
"--github-owner",
|
|
253
|
-
help="GitHub repository owner"
|
|
277
|
+
None, "--github-owner", help="GitHub repository owner"
|
|
254
278
|
),
|
|
255
279
|
github_repo: Optional[str] = typer.Option(
|
|
256
|
-
None,
|
|
257
|
-
"--github-repo",
|
|
258
|
-
help="GitHub repository name"
|
|
280
|
+
None, "--github-repo", help="GitHub repository name"
|
|
259
281
|
),
|
|
260
282
|
github_token: Optional[str] = typer.Option(
|
|
261
|
-
None,
|
|
262
|
-
"--github-token",
|
|
263
|
-
help="GitHub Personal Access Token"
|
|
283
|
+
None, "--github-token", help="GitHub Personal Access Token"
|
|
264
284
|
),
|
|
265
285
|
) -> None:
|
|
266
286
|
"""Initialize mcp-ticketer for the current project.
|
|
@@ -280,10 +300,12 @@ def init(
|
|
|
280
300
|
|
|
281
301
|
# Save globally (not recommended)
|
|
282
302
|
mcp-ticketer init --global
|
|
303
|
+
|
|
283
304
|
"""
|
|
284
305
|
from pathlib import Path
|
|
285
|
-
|
|
306
|
+
|
|
286
307
|
from ..core.env_discovery import discover_config
|
|
308
|
+
from ..core.project_config import ConfigResolver
|
|
287
309
|
|
|
288
310
|
# Determine project path
|
|
289
311
|
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
@@ -295,7 +317,7 @@ def init(
|
|
|
295
317
|
if config_path.exists():
|
|
296
318
|
if not typer.confirm(
|
|
297
319
|
f"Configuration already exists at {config_path}. Overwrite?",
|
|
298
|
-
default=False
|
|
320
|
+
default=False,
|
|
299
321
|
):
|
|
300
322
|
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
301
323
|
raise typer.Exit(0)
|
|
@@ -305,30 +327,37 @@ def init(
|
|
|
305
327
|
adapter_type = adapter
|
|
306
328
|
|
|
307
329
|
if not adapter_type:
|
|
308
|
-
console.print(
|
|
330
|
+
console.print(
|
|
331
|
+
"[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
|
|
332
|
+
)
|
|
309
333
|
discovered = discover_config(proj_path)
|
|
310
334
|
|
|
311
335
|
if discovered and discovered.adapters:
|
|
312
336
|
primary = discovered.get_primary_adapter()
|
|
313
337
|
if primary:
|
|
314
338
|
adapter_type = primary.adapter_type
|
|
315
|
-
console.print(
|
|
339
|
+
console.print(
|
|
340
|
+
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
341
|
+
)
|
|
316
342
|
|
|
317
343
|
# Show what was discovered
|
|
318
|
-
console.print(
|
|
344
|
+
console.print(
|
|
345
|
+
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
346
|
+
)
|
|
319
347
|
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
320
348
|
else:
|
|
321
349
|
adapter_type = "aitrackdown" # Fallback
|
|
322
|
-
console.print(
|
|
350
|
+
console.print(
|
|
351
|
+
"[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]"
|
|
352
|
+
)
|
|
323
353
|
else:
|
|
324
354
|
adapter_type = "aitrackdown" # Fallback
|
|
325
|
-
console.print(
|
|
355
|
+
console.print(
|
|
356
|
+
"[yellow]⚠ No .env files found, defaulting to aitrackdown[/yellow]"
|
|
357
|
+
)
|
|
326
358
|
|
|
327
359
|
# 2. Create configuration based on adapter type
|
|
328
|
-
config = {
|
|
329
|
-
"default_adapter": adapter_type,
|
|
330
|
-
"adapters": {}
|
|
331
|
-
}
|
|
360
|
+
config = {"default_adapter": adapter_type, "adapters": {}}
|
|
332
361
|
|
|
333
362
|
# 3. If discovered and matches adapter_type, use discovered config
|
|
334
363
|
if discovered and adapter_type != "aitrackdown":
|
|
@@ -355,7 +384,9 @@ def init(
|
|
|
355
384
|
linear_config["api_key"] = linear_api_key
|
|
356
385
|
elif not discovered:
|
|
357
386
|
console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
|
|
358
|
-
console.print(
|
|
387
|
+
console.print(
|
|
388
|
+
"Set LINEAR_API_KEY environment variable or use --api-key option"
|
|
389
|
+
)
|
|
359
390
|
|
|
360
391
|
if linear_config:
|
|
361
392
|
config["adapters"]["linear"] = linear_config
|
|
@@ -370,7 +401,9 @@ def init(
|
|
|
370
401
|
|
|
371
402
|
if not server:
|
|
372
403
|
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
373
|
-
console.print(
|
|
404
|
+
console.print(
|
|
405
|
+
"Use --jira-server or set JIRA_SERVER environment variable"
|
|
406
|
+
)
|
|
374
407
|
raise typer.Exit(1)
|
|
375
408
|
|
|
376
409
|
if not email:
|
|
@@ -380,15 +413,15 @@ def init(
|
|
|
380
413
|
|
|
381
414
|
if not token:
|
|
382
415
|
console.print("[red]Error:[/red] JIRA API token is required")
|
|
383
|
-
console.print(
|
|
384
|
-
|
|
416
|
+
console.print(
|
|
417
|
+
"Use --api-key or set JIRA_API_TOKEN environment variable"
|
|
418
|
+
)
|
|
419
|
+
console.print(
|
|
420
|
+
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
421
|
+
)
|
|
385
422
|
raise typer.Exit(1)
|
|
386
423
|
|
|
387
|
-
jira_config = {
|
|
388
|
-
"server": server,
|
|
389
|
-
"email": email,
|
|
390
|
-
"api_token": token
|
|
391
|
-
}
|
|
424
|
+
jira_config = {"server": server, "email": email, "api_token": token}
|
|
392
425
|
|
|
393
426
|
if project:
|
|
394
427
|
jira_config["project_key"] = project
|
|
@@ -404,25 +437,37 @@ def init(
|
|
|
404
437
|
|
|
405
438
|
if not owner:
|
|
406
439
|
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
407
|
-
console.print(
|
|
440
|
+
console.print(
|
|
441
|
+
"Use --github-owner or set GITHUB_OWNER environment variable"
|
|
442
|
+
)
|
|
408
443
|
raise typer.Exit(1)
|
|
409
444
|
|
|
410
445
|
if not repo:
|
|
411
446
|
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
412
|
-
console.print(
|
|
447
|
+
console.print(
|
|
448
|
+
"Use --github-repo or set GITHUB_REPO environment variable"
|
|
449
|
+
)
|
|
413
450
|
raise typer.Exit(1)
|
|
414
451
|
|
|
415
452
|
if not token:
|
|
416
|
-
console.print(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
console.print(
|
|
453
|
+
console.print(
|
|
454
|
+
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
455
|
+
)
|
|
456
|
+
console.print(
|
|
457
|
+
"Use --github-token or set GITHUB_TOKEN environment variable"
|
|
458
|
+
)
|
|
459
|
+
console.print(
|
|
460
|
+
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
461
|
+
)
|
|
462
|
+
console.print(
|
|
463
|
+
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]"
|
|
464
|
+
)
|
|
420
465
|
raise typer.Exit(1)
|
|
421
466
|
|
|
422
467
|
config["adapters"]["github"] = {
|
|
423
468
|
"owner": owner,
|
|
424
469
|
"repo": repo,
|
|
425
|
-
"token": token
|
|
470
|
+
"token": token,
|
|
426
471
|
}
|
|
427
472
|
|
|
428
473
|
# 5. Save to appropriate location
|
|
@@ -432,7 +477,7 @@ def init(
|
|
|
432
477
|
config_file_path = resolver.GLOBAL_CONFIG_PATH
|
|
433
478
|
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
434
479
|
|
|
435
|
-
with open(config_file_path,
|
|
480
|
+
with open(config_file_path, "w") as f:
|
|
436
481
|
json.dump(config, f, indent=2)
|
|
437
482
|
|
|
438
483
|
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
@@ -442,7 +487,7 @@ def init(
|
|
|
442
487
|
config_file_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
443
488
|
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
444
489
|
|
|
445
|
-
with open(config_file_path,
|
|
490
|
+
with open(config_file_path, "w") as f:
|
|
446
491
|
json.dump(config, f, indent=2)
|
|
447
492
|
|
|
448
493
|
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
@@ -453,12 +498,12 @@ def init(
|
|
|
453
498
|
if gitignore_path.exists():
|
|
454
499
|
gitignore_content = gitignore_path.read_text()
|
|
455
500
|
if ".mcp-ticketer" not in gitignore_content:
|
|
456
|
-
with open(gitignore_path,
|
|
501
|
+
with open(gitignore_path, "a") as f:
|
|
457
502
|
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
458
503
|
console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
459
504
|
else:
|
|
460
505
|
# Create .gitignore if it doesn't exist
|
|
461
|
-
with open(gitignore_path,
|
|
506
|
+
with open(gitignore_path, "w") as f:
|
|
462
507
|
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
463
508
|
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
464
509
|
|
|
@@ -469,64 +514,48 @@ def install(
|
|
|
469
514
|
None,
|
|
470
515
|
"--adapter",
|
|
471
516
|
"-a",
|
|
472
|
-
help="Adapter type to use (auto-detected from .env if not specified)"
|
|
517
|
+
help="Adapter type to use (auto-detected from .env if not specified)",
|
|
473
518
|
),
|
|
474
519
|
project_path: Optional[str] = typer.Option(
|
|
475
|
-
None,
|
|
476
|
-
"--path",
|
|
477
|
-
help="Project path (default: current directory)"
|
|
520
|
+
None, "--path", help="Project path (default: current directory)"
|
|
478
521
|
),
|
|
479
522
|
global_config: bool = typer.Option(
|
|
480
523
|
False,
|
|
481
524
|
"--global",
|
|
482
525
|
"-g",
|
|
483
|
-
help="Save to global config instead of project-specific"
|
|
526
|
+
help="Save to global config instead of project-specific",
|
|
484
527
|
),
|
|
485
528
|
base_path: Optional[str] = typer.Option(
|
|
486
529
|
None,
|
|
487
530
|
"--base-path",
|
|
488
531
|
"-p",
|
|
489
|
-
help="Base path for ticket storage (AITrackdown only)"
|
|
532
|
+
help="Base path for ticket storage (AITrackdown only)",
|
|
490
533
|
),
|
|
491
534
|
api_key: Optional[str] = typer.Option(
|
|
492
|
-
None,
|
|
493
|
-
"--api-key",
|
|
494
|
-
help="API key for Linear or API token for JIRA"
|
|
535
|
+
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
495
536
|
),
|
|
496
537
|
team_id: Optional[str] = typer.Option(
|
|
497
|
-
None,
|
|
498
|
-
"--team-id",
|
|
499
|
-
help="Linear team ID (required for Linear adapter)"
|
|
538
|
+
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
500
539
|
),
|
|
501
540
|
jira_server: Optional[str] = typer.Option(
|
|
502
541
|
None,
|
|
503
542
|
"--jira-server",
|
|
504
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)"
|
|
543
|
+
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
505
544
|
),
|
|
506
545
|
jira_email: Optional[str] = typer.Option(
|
|
507
|
-
None,
|
|
508
|
-
"--jira-email",
|
|
509
|
-
help="JIRA user email for authentication"
|
|
546
|
+
None, "--jira-email", help="JIRA user email for authentication"
|
|
510
547
|
),
|
|
511
548
|
jira_project: Optional[str] = typer.Option(
|
|
512
|
-
None,
|
|
513
|
-
"--jira-project",
|
|
514
|
-
help="Default JIRA project key"
|
|
549
|
+
None, "--jira-project", help="Default JIRA project key"
|
|
515
550
|
),
|
|
516
551
|
github_owner: Optional[str] = typer.Option(
|
|
517
|
-
None,
|
|
518
|
-
"--github-owner",
|
|
519
|
-
help="GitHub repository owner"
|
|
552
|
+
None, "--github-owner", help="GitHub repository owner"
|
|
520
553
|
),
|
|
521
554
|
github_repo: Optional[str] = typer.Option(
|
|
522
|
-
None,
|
|
523
|
-
"--github-repo",
|
|
524
|
-
help="GitHub repository name"
|
|
555
|
+
None, "--github-repo", help="GitHub repository name"
|
|
525
556
|
),
|
|
526
557
|
github_token: Optional[str] = typer.Option(
|
|
527
|
-
None,
|
|
528
|
-
"--github-token",
|
|
529
|
-
help="GitHub Personal Access Token"
|
|
558
|
+
None, "--github-token", help="GitHub Personal Access Token"
|
|
530
559
|
),
|
|
531
560
|
) -> None:
|
|
532
561
|
"""Initialize mcp-ticketer for the current project (alias for init).
|
|
@@ -547,6 +576,7 @@ def install(
|
|
|
547
576
|
|
|
548
577
|
# Save globally (not recommended)
|
|
549
578
|
mcp-ticketer install --global
|
|
579
|
+
|
|
550
580
|
"""
|
|
551
581
|
# Call init with all parameters
|
|
552
582
|
init(
|
|
@@ -568,45 +598,20 @@ def install(
|
|
|
568
598
|
@app.command("set")
|
|
569
599
|
def set_config(
|
|
570
600
|
adapter: Optional[AdapterType] = typer.Option(
|
|
571
|
-
None,
|
|
572
|
-
"--adapter",
|
|
573
|
-
"-a",
|
|
574
|
-
help="Set default adapter"
|
|
601
|
+
None, "--adapter", "-a", help="Set default adapter"
|
|
575
602
|
),
|
|
576
603
|
team_key: Optional[str] = typer.Option(
|
|
577
|
-
None,
|
|
578
|
-
"--team-key",
|
|
579
|
-
help="Linear team key (e.g., BTA)"
|
|
580
|
-
),
|
|
581
|
-
team_id: Optional[str] = typer.Option(
|
|
582
|
-
None,
|
|
583
|
-
"--team-id",
|
|
584
|
-
help="Linear team ID"
|
|
604
|
+
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
585
605
|
),
|
|
606
|
+
team_id: Optional[str] = typer.Option(None, "--team-id", help="Linear team ID"),
|
|
586
607
|
owner: Optional[str] = typer.Option(
|
|
587
|
-
None,
|
|
588
|
-
"--owner",
|
|
589
|
-
help="GitHub repository owner"
|
|
590
|
-
),
|
|
591
|
-
repo: Optional[str] = typer.Option(
|
|
592
|
-
None,
|
|
593
|
-
"--repo",
|
|
594
|
-
help="GitHub repository name"
|
|
595
|
-
),
|
|
596
|
-
server: Optional[str] = typer.Option(
|
|
597
|
-
None,
|
|
598
|
-
"--server",
|
|
599
|
-
help="JIRA server URL"
|
|
600
|
-
),
|
|
601
|
-
project: Optional[str] = typer.Option(
|
|
602
|
-
None,
|
|
603
|
-
"--project",
|
|
604
|
-
help="JIRA project key"
|
|
608
|
+
None, "--owner", help="GitHub repository owner"
|
|
605
609
|
),
|
|
610
|
+
repo: Optional[str] = typer.Option(None, "--repo", help="GitHub repository name"),
|
|
611
|
+
server: Optional[str] = typer.Option(None, "--server", help="JIRA server URL"),
|
|
612
|
+
project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
|
|
606
613
|
base_path: Optional[str] = typer.Option(
|
|
607
|
-
None,
|
|
608
|
-
"--base-path",
|
|
609
|
-
help="AITrackdown base path"
|
|
614
|
+
None, "--base-path", help="AITrackdown base path"
|
|
610
615
|
),
|
|
611
616
|
) -> None:
|
|
612
617
|
"""Set default adapter and adapter-specific configuration.
|
|
@@ -617,7 +622,9 @@ def set_config(
|
|
|
617
622
|
# Show current configuration
|
|
618
623
|
config = load_config()
|
|
619
624
|
console.print("[bold]Current Configuration:[/bold]")
|
|
620
|
-
console.print(
|
|
625
|
+
console.print(
|
|
626
|
+
f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]"
|
|
627
|
+
)
|
|
621
628
|
|
|
622
629
|
adapters_config = config.get("adapters", {})
|
|
623
630
|
if adapters_config:
|
|
@@ -626,7 +633,11 @@ def set_config(
|
|
|
626
633
|
console.print(f"\n[cyan]{adapter_name}:[/cyan]")
|
|
627
634
|
for key, value in adapter_config.items():
|
|
628
635
|
# Don't display sensitive values like tokens
|
|
629
|
-
if
|
|
636
|
+
if (
|
|
637
|
+
"token" in key.lower()
|
|
638
|
+
or "key" in key.lower()
|
|
639
|
+
and "team" not in key.lower()
|
|
640
|
+
):
|
|
630
641
|
value = "***" if value else "not set"
|
|
631
642
|
console.print(f" {key}: {value}")
|
|
632
643
|
return
|
|
@@ -649,7 +660,7 @@ def set_config(
|
|
|
649
660
|
if team_id:
|
|
650
661
|
linear_config["team_id"] = team_id
|
|
651
662
|
adapter_configs["linear"] = linear_config
|
|
652
|
-
console.print(
|
|
663
|
+
console.print("[green]✓[/green] Linear settings updated")
|
|
653
664
|
|
|
654
665
|
# GitHub configuration
|
|
655
666
|
if owner or repo:
|
|
@@ -659,7 +670,7 @@ def set_config(
|
|
|
659
670
|
if repo:
|
|
660
671
|
github_config["repo"] = repo
|
|
661
672
|
adapter_configs["github"] = github_config
|
|
662
|
-
console.print(
|
|
673
|
+
console.print("[green]✓[/green] GitHub settings updated")
|
|
663
674
|
|
|
664
675
|
# JIRA configuration
|
|
665
676
|
if server or project:
|
|
@@ -669,12 +680,12 @@ def set_config(
|
|
|
669
680
|
if project:
|
|
670
681
|
jira_config["project_key"] = project
|
|
671
682
|
adapter_configs["jira"] = jira_config
|
|
672
|
-
console.print(
|
|
683
|
+
console.print("[green]✓[/green] JIRA settings updated")
|
|
673
684
|
|
|
674
685
|
# AITrackdown configuration
|
|
675
686
|
if base_path:
|
|
676
687
|
adapter_configs["aitrackdown"] = {"base_path": base_path}
|
|
677
|
-
console.print(
|
|
688
|
+
console.print("[green]✓[/green] AITrackdown settings updated")
|
|
678
689
|
|
|
679
690
|
if adapter_configs:
|
|
680
691
|
updates["adapters"] = adapter_configs
|
|
@@ -688,36 +699,22 @@ def set_config(
|
|
|
688
699
|
|
|
689
700
|
@app.command("configure")
|
|
690
701
|
def configure_command(
|
|
691
|
-
show: bool = typer.Option(
|
|
692
|
-
False,
|
|
693
|
-
"--show",
|
|
694
|
-
help="Show current configuration"
|
|
695
|
-
),
|
|
702
|
+
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
696
703
|
adapter: Optional[str] = typer.Option(
|
|
697
|
-
None,
|
|
698
|
-
"--adapter",
|
|
699
|
-
help="Set default adapter type"
|
|
700
|
-
),
|
|
701
|
-
api_key: Optional[str] = typer.Option(
|
|
702
|
-
None,
|
|
703
|
-
"--api-key",
|
|
704
|
-
help="Set API key/token"
|
|
704
|
+
None, "--adapter", help="Set default adapter type"
|
|
705
705
|
),
|
|
706
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
706
707
|
project_id: Optional[str] = typer.Option(
|
|
707
|
-
None,
|
|
708
|
-
"--project-id",
|
|
709
|
-
help="Set project ID"
|
|
708
|
+
None, "--project-id", help="Set project ID"
|
|
710
709
|
),
|
|
711
710
|
team_id: Optional[str] = typer.Option(
|
|
712
|
-
None,
|
|
713
|
-
"--team-id",
|
|
714
|
-
help="Set team ID (Linear)"
|
|
711
|
+
None, "--team-id", help="Set team ID (Linear)"
|
|
715
712
|
),
|
|
716
713
|
global_scope: bool = typer.Option(
|
|
717
714
|
False,
|
|
718
715
|
"--global",
|
|
719
716
|
"-g",
|
|
720
|
-
help="Save to global config instead of project-specific"
|
|
717
|
+
help="Save to global config instead of project-specific",
|
|
721
718
|
),
|
|
722
719
|
) -> None:
|
|
723
720
|
"""Configure MCP Ticketer integration.
|
|
@@ -738,7 +735,7 @@ def configure_command(
|
|
|
738
735
|
api_key=api_key,
|
|
739
736
|
project_id=project_id,
|
|
740
737
|
team_id=team_id,
|
|
741
|
-
global_scope=global_scope
|
|
738
|
+
global_scope=global_scope,
|
|
742
739
|
)
|
|
743
740
|
return
|
|
744
741
|
|
|
@@ -749,9 +746,7 @@ def configure_command(
|
|
|
749
746
|
@app.command("migrate-config")
|
|
750
747
|
def migrate_config(
|
|
751
748
|
dry_run: bool = typer.Option(
|
|
752
|
-
False,
|
|
753
|
-
"--dry-run",
|
|
754
|
-
help="Show what would be done without making changes"
|
|
749
|
+
False, "--dry-run", help="Show what would be done without making changes"
|
|
755
750
|
),
|
|
756
751
|
) -> None:
|
|
757
752
|
"""Migrate configuration from old format to new format.
|
|
@@ -785,50 +780,42 @@ def status_command():
|
|
|
785
780
|
# Show worker status
|
|
786
781
|
worker_status = manager.get_status()
|
|
787
782
|
if worker_status["running"]:
|
|
788
|
-
console.print(
|
|
783
|
+
console.print(
|
|
784
|
+
f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
|
|
785
|
+
)
|
|
789
786
|
else:
|
|
790
787
|
console.print("\n[red]○ Worker is not running[/red]")
|
|
791
788
|
if pending > 0:
|
|
792
|
-
console.print(
|
|
789
|
+
console.print(
|
|
790
|
+
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
|
|
791
|
+
)
|
|
793
792
|
|
|
794
793
|
|
|
795
794
|
@app.command()
|
|
796
795
|
def create(
|
|
797
796
|
title: str = typer.Argument(..., help="Ticket title"),
|
|
798
797
|
description: Optional[str] = typer.Option(
|
|
799
|
-
None,
|
|
800
|
-
"--description",
|
|
801
|
-
"-d",
|
|
802
|
-
help="Ticket description"
|
|
798
|
+
None, "--description", "-d", help="Ticket description"
|
|
803
799
|
),
|
|
804
800
|
priority: Priority = typer.Option(
|
|
805
|
-
Priority.MEDIUM,
|
|
806
|
-
"--priority",
|
|
807
|
-
"-p",
|
|
808
|
-
help="Priority level"
|
|
801
|
+
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
809
802
|
),
|
|
810
|
-
tags: Optional[
|
|
811
|
-
None,
|
|
812
|
-
"--tag",
|
|
813
|
-
"-t",
|
|
814
|
-
help="Tags (can be specified multiple times)"
|
|
803
|
+
tags: Optional[list[str]] = typer.Option(
|
|
804
|
+
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
815
805
|
),
|
|
816
806
|
assignee: Optional[str] = typer.Option(
|
|
817
|
-
None,
|
|
818
|
-
"--assignee",
|
|
819
|
-
"-a",
|
|
820
|
-
help="Assignee username"
|
|
807
|
+
None, "--assignee", "-a", help="Assignee username"
|
|
821
808
|
),
|
|
822
809
|
adapter: Optional[AdapterType] = typer.Option(
|
|
823
|
-
None,
|
|
824
|
-
"--adapter",
|
|
825
|
-
help="Override default adapter"
|
|
810
|
+
None, "--adapter", help="Override default adapter"
|
|
826
811
|
),
|
|
827
812
|
) -> None:
|
|
828
813
|
"""Create a new ticket."""
|
|
829
814
|
# Get the adapter name
|
|
830
815
|
config = load_config()
|
|
831
|
-
adapter_name =
|
|
816
|
+
adapter_name = (
|
|
817
|
+
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
818
|
+
)
|
|
832
819
|
|
|
833
820
|
# Create task data
|
|
834
821
|
task_data = {
|
|
@@ -842,9 +829,7 @@ def create(
|
|
|
842
829
|
# Add to queue
|
|
843
830
|
queue = Queue()
|
|
844
831
|
queue_id = queue.add(
|
|
845
|
-
ticket_data=task_data,
|
|
846
|
-
adapter=adapter_name,
|
|
847
|
-
operation="create"
|
|
832
|
+
ticket_data=task_data, adapter=adapter_name, operation="create"
|
|
848
833
|
)
|
|
849
834
|
|
|
850
835
|
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
@@ -861,32 +846,22 @@ def create(
|
|
|
861
846
|
@app.command("list")
|
|
862
847
|
def list_tickets(
|
|
863
848
|
state: Optional[TicketState] = typer.Option(
|
|
864
|
-
None,
|
|
865
|
-
"--state",
|
|
866
|
-
"-s",
|
|
867
|
-
help="Filter by state"
|
|
849
|
+
None, "--state", "-s", help="Filter by state"
|
|
868
850
|
),
|
|
869
851
|
priority: Optional[Priority] = typer.Option(
|
|
870
|
-
None,
|
|
871
|
-
"--priority",
|
|
872
|
-
"-p",
|
|
873
|
-
help="Filter by priority"
|
|
874
|
-
),
|
|
875
|
-
limit: int = typer.Option(
|
|
876
|
-
10,
|
|
877
|
-
"--limit",
|
|
878
|
-
"-l",
|
|
879
|
-
help="Maximum number of tickets"
|
|
852
|
+
None, "--priority", "-p", help="Filter by priority"
|
|
880
853
|
),
|
|
854
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
881
855
|
adapter: Optional[AdapterType] = typer.Option(
|
|
882
|
-
None,
|
|
883
|
-
"--adapter",
|
|
884
|
-
help="Override default adapter"
|
|
856
|
+
None, "--adapter", help="Override default adapter"
|
|
885
857
|
),
|
|
886
858
|
) -> None:
|
|
887
859
|
"""List tickets with optional filters."""
|
|
860
|
+
|
|
888
861
|
async def _list():
|
|
889
|
-
adapter_instance = get_adapter(
|
|
862
|
+
adapter_instance = get_adapter(
|
|
863
|
+
override_adapter=adapter.value if adapter else None
|
|
864
|
+
)
|
|
890
865
|
filters = {}
|
|
891
866
|
if state:
|
|
892
867
|
filters["state"] = state
|
|
@@ -923,21 +898,17 @@ def list_tickets(
|
|
|
923
898
|
@app.command()
|
|
924
899
|
def show(
|
|
925
900
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
926
|
-
comments: bool = typer.Option(
|
|
927
|
-
False,
|
|
928
|
-
"--comments",
|
|
929
|
-
"-c",
|
|
930
|
-
help="Show comments"
|
|
931
|
-
),
|
|
901
|
+
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
932
902
|
adapter: Optional[AdapterType] = typer.Option(
|
|
933
|
-
None,
|
|
934
|
-
"--adapter",
|
|
935
|
-
help="Override default adapter"
|
|
903
|
+
None, "--adapter", help="Override default adapter"
|
|
936
904
|
),
|
|
937
905
|
) -> None:
|
|
938
906
|
"""Show detailed ticket information."""
|
|
907
|
+
|
|
939
908
|
async def _show():
|
|
940
|
-
adapter_instance = get_adapter(
|
|
909
|
+
adapter_instance = get_adapter(
|
|
910
|
+
override_adapter=adapter.value if adapter else None
|
|
911
|
+
)
|
|
941
912
|
ticket = await adapter_instance.read(ticket_id)
|
|
942
913
|
ticket_comments = None
|
|
943
914
|
if comments and ticket:
|
|
@@ -957,7 +928,7 @@ def show(
|
|
|
957
928
|
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
958
929
|
|
|
959
930
|
if ticket.description:
|
|
960
|
-
console.print(
|
|
931
|
+
console.print("\n[dim]Description:[/dim]")
|
|
961
932
|
console.print(ticket.description)
|
|
962
933
|
|
|
963
934
|
if ticket.tags:
|
|
@@ -979,27 +950,16 @@ def update(
|
|
|
979
950
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
980
951
|
title: Optional[str] = typer.Option(None, "--title", help="New title"),
|
|
981
952
|
description: Optional[str] = typer.Option(
|
|
982
|
-
None,
|
|
983
|
-
"--description",
|
|
984
|
-
"-d",
|
|
985
|
-
help="New description"
|
|
953
|
+
None, "--description", "-d", help="New description"
|
|
986
954
|
),
|
|
987
955
|
priority: Optional[Priority] = typer.Option(
|
|
988
|
-
None,
|
|
989
|
-
"--priority",
|
|
990
|
-
"-p",
|
|
991
|
-
help="New priority"
|
|
956
|
+
None, "--priority", "-p", help="New priority"
|
|
992
957
|
),
|
|
993
958
|
assignee: Optional[str] = typer.Option(
|
|
994
|
-
None,
|
|
995
|
-
"--assignee",
|
|
996
|
-
"-a",
|
|
997
|
-
help="New assignee"
|
|
959
|
+
None, "--assignee", "-a", help="New assignee"
|
|
998
960
|
),
|
|
999
961
|
adapter: Optional[AdapterType] = typer.Option(
|
|
1000
|
-
None,
|
|
1001
|
-
"--adapter",
|
|
1002
|
-
help="Override default adapter"
|
|
962
|
+
None, "--adapter", help="Override default adapter"
|
|
1003
963
|
),
|
|
1004
964
|
) -> None:
|
|
1005
965
|
"""Update ticket fields."""
|
|
@@ -1009,7 +969,9 @@ def update(
|
|
|
1009
969
|
if description:
|
|
1010
970
|
updates["description"] = description
|
|
1011
971
|
if priority:
|
|
1012
|
-
updates["priority"] =
|
|
972
|
+
updates["priority"] = (
|
|
973
|
+
priority.value if isinstance(priority, Priority) else priority
|
|
974
|
+
)
|
|
1013
975
|
if assignee:
|
|
1014
976
|
updates["assignee"] = assignee
|
|
1015
977
|
|
|
@@ -1019,18 +981,16 @@ def update(
|
|
|
1019
981
|
|
|
1020
982
|
# Get the adapter name
|
|
1021
983
|
config = load_config()
|
|
1022
|
-
adapter_name =
|
|
984
|
+
adapter_name = (
|
|
985
|
+
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
986
|
+
)
|
|
1023
987
|
|
|
1024
988
|
# Add ticket_id to updates
|
|
1025
989
|
updates["ticket_id"] = ticket_id
|
|
1026
990
|
|
|
1027
991
|
# Add to queue
|
|
1028
992
|
queue = Queue()
|
|
1029
|
-
queue_id = queue.add(
|
|
1030
|
-
ticket_data=updates,
|
|
1031
|
-
adapter=adapter_name,
|
|
1032
|
-
operation="update"
|
|
1033
|
-
)
|
|
993
|
+
queue_id = queue.add(ticket_data=updates, adapter=adapter_name, operation="update")
|
|
1034
994
|
|
|
1035
995
|
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
1036
996
|
for key, value in updates.items():
|
|
@@ -1047,31 +1007,60 @@ def update(
|
|
|
1047
1007
|
@app.command()
|
|
1048
1008
|
def transition(
|
|
1049
1009
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1050
|
-
|
|
1010
|
+
state_positional: Optional[TicketState] = typer.Argument(
|
|
1011
|
+
None, help="Target state (positional - deprecated, use --state instead)"
|
|
1012
|
+
),
|
|
1013
|
+
state: Optional[TicketState] = typer.Option(
|
|
1014
|
+
None, "--state", "-s", help="Target state (recommended)"
|
|
1015
|
+
),
|
|
1051
1016
|
adapter: Optional[AdapterType] = typer.Option(
|
|
1052
|
-
None,
|
|
1053
|
-
"--adapter",
|
|
1054
|
-
help="Override default adapter"
|
|
1017
|
+
None, "--adapter", help="Override default adapter"
|
|
1055
1018
|
),
|
|
1056
1019
|
) -> None:
|
|
1057
|
-
"""Change ticket state with validation.
|
|
1020
|
+
"""Change ticket state with validation.
|
|
1021
|
+
|
|
1022
|
+
Examples:
|
|
1023
|
+
# Recommended syntax with flag:
|
|
1024
|
+
mcp-ticketer transition BTA-215 --state done
|
|
1025
|
+
mcp-ticketer transition BTA-215 -s in_progress
|
|
1026
|
+
|
|
1027
|
+
# Legacy positional syntax (still supported):
|
|
1028
|
+
mcp-ticketer transition BTA-215 done
|
|
1029
|
+
|
|
1030
|
+
"""
|
|
1031
|
+
# Determine which state to use (prefer flag over positional)
|
|
1032
|
+
target_state = state if state is not None else state_positional
|
|
1033
|
+
|
|
1034
|
+
if target_state is None:
|
|
1035
|
+
console.print("[red]Error: State is required[/red]")
|
|
1036
|
+
console.print(
|
|
1037
|
+
"Use either:\n"
|
|
1038
|
+
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
1039
|
+
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
1040
|
+
)
|
|
1041
|
+
raise typer.Exit(1)
|
|
1042
|
+
|
|
1058
1043
|
# Get the adapter name
|
|
1059
1044
|
config = load_config()
|
|
1060
|
-
adapter_name =
|
|
1045
|
+
adapter_name = (
|
|
1046
|
+
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
1047
|
+
)
|
|
1061
1048
|
|
|
1062
1049
|
# Add to queue
|
|
1063
1050
|
queue = Queue()
|
|
1064
1051
|
queue_id = queue.add(
|
|
1065
1052
|
ticket_data={
|
|
1066
1053
|
"ticket_id": ticket_id,
|
|
1067
|
-
"state":
|
|
1054
|
+
"state": (
|
|
1055
|
+
target_state.value if hasattr(target_state, "value") else target_state
|
|
1056
|
+
),
|
|
1068
1057
|
},
|
|
1069
1058
|
adapter=adapter_name,
|
|
1070
|
-
operation="transition"
|
|
1059
|
+
operation="transition",
|
|
1071
1060
|
)
|
|
1072
1061
|
|
|
1073
1062
|
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
1074
|
-
console.print(f" Ticket: {ticket_id} → {
|
|
1063
|
+
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
1075
1064
|
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
1076
1065
|
|
|
1077
1066
|
# Start worker if needed
|
|
@@ -1088,14 +1077,15 @@ def search(
|
|
|
1088
1077
|
assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
|
|
1089
1078
|
limit: int = typer.Option(10, "--limit", "-l"),
|
|
1090
1079
|
adapter: Optional[AdapterType] = typer.Option(
|
|
1091
|
-
None,
|
|
1092
|
-
"--adapter",
|
|
1093
|
-
help="Override default adapter"
|
|
1080
|
+
None, "--adapter", help="Override default adapter"
|
|
1094
1081
|
),
|
|
1095
1082
|
) -> None:
|
|
1096
1083
|
"""Search tickets with advanced query."""
|
|
1084
|
+
|
|
1097
1085
|
async def _search():
|
|
1098
|
-
adapter_instance = get_adapter(
|
|
1086
|
+
adapter_instance = get_adapter(
|
|
1087
|
+
override_adapter=adapter.value if adapter else None
|
|
1088
|
+
)
|
|
1099
1089
|
search_query = SearchQuery(
|
|
1100
1090
|
query=query,
|
|
1101
1091
|
state=state,
|
|
@@ -1130,9 +1120,7 @@ app.add_typer(discover_app, name="discover")
|
|
|
1130
1120
|
|
|
1131
1121
|
|
|
1132
1122
|
@app.command()
|
|
1133
|
-
def check(
|
|
1134
|
-
queue_id: str = typer.Argument(..., help="Queue ID to check")
|
|
1135
|
-
):
|
|
1123
|
+
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
1136
1124
|
"""Check status of a queued operation."""
|
|
1137
1125
|
queue = Queue()
|
|
1138
1126
|
item = queue.get_item(queue_id)
|
|
@@ -1165,7 +1153,7 @@ def check(
|
|
|
1165
1153
|
if item.error_message:
|
|
1166
1154
|
console.print(f"\n[red]Error:[/red] {item.error_message}")
|
|
1167
1155
|
elif item.result:
|
|
1168
|
-
console.print(
|
|
1156
|
+
console.print("\n[green]Result:[/green]")
|
|
1169
1157
|
for key, value in item.result.items():
|
|
1170
1158
|
console.print(f" {key}: {value}")
|
|
1171
1159
|
|
|
@@ -1176,15 +1164,10 @@ def check(
|
|
|
1176
1164
|
@app.command()
|
|
1177
1165
|
def serve(
|
|
1178
1166
|
adapter: Optional[AdapterType] = typer.Option(
|
|
1179
|
-
None,
|
|
1180
|
-
"--adapter",
|
|
1181
|
-
"-a",
|
|
1182
|
-
help="Override default adapter type"
|
|
1167
|
+
None, "--adapter", "-a", help="Override default adapter type"
|
|
1183
1168
|
),
|
|
1184
1169
|
base_path: Optional[str] = typer.Option(
|
|
1185
|
-
None,
|
|
1186
|
-
"--base-path",
|
|
1187
|
-
help="Base path for AITrackdown adapter"
|
|
1170
|
+
None, "--base-path", help="Base path for AITrackdown adapter"
|
|
1188
1171
|
),
|
|
1189
1172
|
):
|
|
1190
1173
|
"""Start MCP server for JSON-RPC communication over stdio.
|
|
@@ -1206,7 +1189,9 @@ def serve(
|
|
|
1206
1189
|
config = load_config()
|
|
1207
1190
|
|
|
1208
1191
|
# Determine adapter type
|
|
1209
|
-
adapter_type =
|
|
1192
|
+
adapter_type = (
|
|
1193
|
+
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
1194
|
+
)
|
|
1210
1195
|
|
|
1211
1196
|
# Get adapter configuration
|
|
1212
1197
|
adapters_config = config.get("adapters", {})
|
|
@@ -1223,11 +1208,14 @@ def serve(
|
|
|
1223
1208
|
# MCP server uses stdio for JSON-RPC, so we can't print to stdout
|
|
1224
1209
|
# Only print to stderr to avoid interfering with the protocol
|
|
1225
1210
|
import sys
|
|
1211
|
+
|
|
1226
1212
|
if sys.stderr.isatty():
|
|
1227
1213
|
# Only print if stderr is a terminal (not redirected)
|
|
1228
1214
|
console.file = sys.stderr
|
|
1229
1215
|
console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
|
|
1230
|
-
console.print(
|
|
1216
|
+
console.print(
|
|
1217
|
+
"[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
|
|
1218
|
+
)
|
|
1231
1219
|
|
|
1232
1220
|
# Create and run server
|
|
1233
1221
|
try:
|
|
@@ -1237,7 +1225,7 @@ def serve(
|
|
|
1237
1225
|
# Also send this to stderr
|
|
1238
1226
|
if sys.stderr.isatty():
|
|
1239
1227
|
console.print("\n[yellow]Server stopped by user[/yellow]")
|
|
1240
|
-
if
|
|
1228
|
+
if "server" in locals():
|
|
1241
1229
|
asyncio.run(server.stop())
|
|
1242
1230
|
except Exception as e:
|
|
1243
1231
|
# Log error to stderr
|
|
@@ -1251,13 +1239,10 @@ def mcp(
|
|
|
1251
1239
|
False,
|
|
1252
1240
|
"--global",
|
|
1253
1241
|
"-g",
|
|
1254
|
-
help="Configure Claude Desktop instead of project-level"
|
|
1242
|
+
help="Configure Claude Desktop instead of project-level",
|
|
1255
1243
|
),
|
|
1256
1244
|
force: bool = typer.Option(
|
|
1257
|
-
False,
|
|
1258
|
-
"--force",
|
|
1259
|
-
"-f",
|
|
1260
|
-
help="Overwrite existing configuration"
|
|
1245
|
+
False, "--force", "-f", help="Overwrite existing configuration"
|
|
1261
1246
|
),
|
|
1262
1247
|
):
|
|
1263
1248
|
"""Configure Claude Code to use mcp-ticketer MCP server.
|
|
@@ -1283,4 +1268,4 @@ def main():
|
|
|
1283
1268
|
|
|
1284
1269
|
|
|
1285
1270
|
if __name__ == "__main__":
|
|
1286
|
-
main()
|
|
1271
|
+
main()
|