mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -5,27 +5,29 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
from dotenv import load_dotenv
|
|
12
12
|
from rich.console import Console
|
|
13
|
-
from rich.table import Table
|
|
14
|
-
|
|
15
|
-
from ..__version__ import __version__
|
|
16
|
-
from ..core import AdapterRegistry, Priority, TicketState
|
|
17
|
-
from ..core.models import SearchQuery
|
|
18
|
-
from ..queue import Queue, QueueStatus, WorkerManager
|
|
19
|
-
from ..queue.health_monitor import QueueHealthMonitor, HealthStatus
|
|
20
|
-
from ..queue.ticket_registry import TicketRegistry
|
|
21
13
|
|
|
22
14
|
# Import adapters module to trigger registration
|
|
23
15
|
import mcp_ticketer.adapters # noqa: F401
|
|
16
|
+
|
|
17
|
+
from ..__version__ import __version__
|
|
18
|
+
from ..core import AdapterRegistry
|
|
24
19
|
from .configure import configure_wizard, set_adapter_config, show_current_config
|
|
25
20
|
from .diagnostics import run_diagnostics
|
|
26
21
|
from .discover import app as discover_app
|
|
22
|
+
from .init_command import init
|
|
23
|
+
from .instruction_commands import app as instruction_app
|
|
24
|
+
from .mcp_server_commands import mcp_app
|
|
27
25
|
from .migrate_config import migrate_config_command
|
|
26
|
+
from .platform_commands import app as platform_app
|
|
27
|
+
from .platform_installer import install, remove, uninstall
|
|
28
28
|
from .queue_commands import app as queue_app
|
|
29
|
+
from .setup_command import setup
|
|
30
|
+
from .ticket_commands import app as ticket_app
|
|
29
31
|
|
|
30
32
|
# Load environment variables from .env files
|
|
31
33
|
# Priority: .env.local (highest) > .env (base)
|
|
@@ -47,11 +49,11 @@ app = typer.Typer(
|
|
|
47
49
|
console = Console()
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
def version_callback(value: bool):
|
|
52
|
+
def version_callback(value: bool) -> None:
|
|
51
53
|
"""Print version and exit."""
|
|
52
54
|
if value:
|
|
53
55
|
console.print(f"mcp-ticketer version {__version__}")
|
|
54
|
-
raise typer.Exit()
|
|
56
|
+
raise typer.Exit() from None
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
@app.callback()
|
|
@@ -64,7 +66,7 @@ def main_callback(
|
|
|
64
66
|
is_eager=True,
|
|
65
67
|
help="Show version and exit",
|
|
66
68
|
),
|
|
67
|
-
):
|
|
69
|
+
) -> None:
|
|
68
70
|
"""MCP Ticketer - Universal ticket management interface."""
|
|
69
71
|
pass
|
|
70
72
|
|
|
@@ -82,7 +84,7 @@ class AdapterType(str, Enum):
|
|
|
82
84
|
GITHUB = "github"
|
|
83
85
|
|
|
84
86
|
|
|
85
|
-
def load_config(project_dir:
|
|
87
|
+
def load_config(project_dir: Path | None = None) -> dict:
|
|
86
88
|
"""Load configuration from project-local config file ONLY.
|
|
87
89
|
|
|
88
90
|
SECURITY: This method ONLY reads from the current project directory
|
|
@@ -90,6 +92,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
90
92
|
from user home directory or system-wide locations.
|
|
91
93
|
|
|
92
94
|
Args:
|
|
95
|
+
----
|
|
93
96
|
project_dir: Optional project directory to load config from
|
|
94
97
|
|
|
95
98
|
Resolution order:
|
|
@@ -97,6 +100,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
97
100
|
2. Default to aitrackdown adapter
|
|
98
101
|
|
|
99
102
|
Returns:
|
|
103
|
+
-------
|
|
100
104
|
Configuration dictionary with adapter and config keys.
|
|
101
105
|
Defaults to aitrackdown if no local config exists.
|
|
102
106
|
|
|
@@ -144,6 +148,86 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
144
148
|
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
145
149
|
|
|
146
150
|
|
|
151
|
+
def _discover_from_env_files() -> str | None:
|
|
152
|
+
"""Discover adapter configuration from .env or .env.local files.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
-------
|
|
156
|
+
Adapter name if discovered, None otherwise
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
import logging
|
|
160
|
+
from pathlib import Path
|
|
161
|
+
|
|
162
|
+
logger = logging.getLogger(__name__)
|
|
163
|
+
|
|
164
|
+
# Check .env.local first, then .env
|
|
165
|
+
env_files = [".env.local", ".env"]
|
|
166
|
+
|
|
167
|
+
for env_file in env_files:
|
|
168
|
+
env_path = Path.cwd() / env_file
|
|
169
|
+
if env_path.exists():
|
|
170
|
+
try:
|
|
171
|
+
# Simple .env parsing (key=value format)
|
|
172
|
+
env_vars = {}
|
|
173
|
+
with open(env_path) as f:
|
|
174
|
+
for line in f:
|
|
175
|
+
line = line.strip()
|
|
176
|
+
if line and not line.startswith("#") and "=" in line:
|
|
177
|
+
key, value = line.split("=", 1)
|
|
178
|
+
env_vars[key.strip()] = value.strip().strip("\"'")
|
|
179
|
+
|
|
180
|
+
# Check for adapter-specific variables
|
|
181
|
+
if env_vars.get("LINEAR_API_KEY"):
|
|
182
|
+
logger.info(f"Discovered Linear configuration in {env_file}")
|
|
183
|
+
return "linear"
|
|
184
|
+
elif env_vars.get("GITHUB_TOKEN"):
|
|
185
|
+
logger.info(f"Discovered GitHub configuration in {env_file}")
|
|
186
|
+
return "github"
|
|
187
|
+
elif env_vars.get("JIRA_SERVER"):
|
|
188
|
+
logger.info(f"Discovered JIRA configuration in {env_file}")
|
|
189
|
+
return "jira"
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(f"Could not read {env_file}: {e}")
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _save_adapter_to_config(adapter_name: str) -> None:
|
|
198
|
+
"""Save adapter configuration to config file.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
----
|
|
202
|
+
adapter_name: Name of the adapter to save as default
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
import logging
|
|
206
|
+
|
|
207
|
+
logger = logging.getLogger(__name__)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
config = load_config()
|
|
211
|
+
config["default_adapter"] = adapter_name
|
|
212
|
+
|
|
213
|
+
# Ensure adapters section exists
|
|
214
|
+
if "adapters" not in config:
|
|
215
|
+
config["adapters"] = {}
|
|
216
|
+
|
|
217
|
+
# Add basic adapter config if not exists
|
|
218
|
+
if adapter_name not in config["adapters"]:
|
|
219
|
+
if adapter_name == "aitrackdown":
|
|
220
|
+
config["adapters"][adapter_name] = {"base_path": ".aitrackdown"}
|
|
221
|
+
else:
|
|
222
|
+
config["adapters"][adapter_name] = {"type": adapter_name}
|
|
223
|
+
|
|
224
|
+
save_config(config)
|
|
225
|
+
logger.info(f"Saved {adapter_name} as default adapter")
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.warning(f"Could not save adapter configuration: {e}")
|
|
229
|
+
|
|
230
|
+
|
|
147
231
|
def save_config(config: dict) -> None:
|
|
148
232
|
"""Save configuration to project-local config file ONLY.
|
|
149
233
|
|
|
@@ -165,9 +249,11 @@ def merge_config(updates: dict) -> dict:
|
|
|
165
249
|
"""Merge updates into existing config.
|
|
166
250
|
|
|
167
251
|
Args:
|
|
252
|
+
----
|
|
168
253
|
updates: Configuration updates to merge
|
|
169
254
|
|
|
170
255
|
Returns:
|
|
256
|
+
-------
|
|
171
257
|
Updated configuration
|
|
172
258
|
|
|
173
259
|
"""
|
|
@@ -190,11 +276,12 @@ def merge_config(updates: dict) -> dict:
|
|
|
190
276
|
|
|
191
277
|
|
|
192
278
|
def get_adapter(
|
|
193
|
-
override_adapter:
|
|
194
|
-
):
|
|
279
|
+
override_adapter: str | None = None, override_config: dict | None = None
|
|
280
|
+
) -> Any:
|
|
195
281
|
"""Get configured adapter instance.
|
|
196
282
|
|
|
197
283
|
Args:
|
|
284
|
+
----
|
|
198
285
|
override_adapter: Override the default adapter type
|
|
199
286
|
override_config: Override configuration for the adapter
|
|
200
287
|
|
|
@@ -222,7 +309,6 @@ def get_adapter(
|
|
|
222
309
|
adapter_config = config["config"]
|
|
223
310
|
|
|
224
311
|
# Add environment variables for authentication
|
|
225
|
-
import os
|
|
226
312
|
|
|
227
313
|
if adapter_type == "linear":
|
|
228
314
|
if not adapter_config.get("api_key"):
|
|
@@ -239,384 +325,20 @@ def get_adapter(
|
|
|
239
325
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
240
326
|
|
|
241
327
|
|
|
242
|
-
@app.command()
|
|
243
|
-
def init(
|
|
244
|
-
adapter: Optional[str] = typer.Option(
|
|
245
|
-
None,
|
|
246
|
-
"--adapter",
|
|
247
|
-
"-a",
|
|
248
|
-
help="Adapter type to use (auto-detected from .env if not specified)",
|
|
249
|
-
),
|
|
250
|
-
project_path: Optional[str] = typer.Option(
|
|
251
|
-
None, "--path", help="Project path (default: current directory)"
|
|
252
|
-
),
|
|
253
|
-
global_config: bool = typer.Option(
|
|
254
|
-
False,
|
|
255
|
-
"--global",
|
|
256
|
-
"-g",
|
|
257
|
-
help="Save to global config instead of project-specific",
|
|
258
|
-
),
|
|
259
|
-
base_path: Optional[str] = typer.Option(
|
|
260
|
-
None,
|
|
261
|
-
"--base-path",
|
|
262
|
-
"-p",
|
|
263
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
264
|
-
),
|
|
265
|
-
api_key: Optional[str] = typer.Option(
|
|
266
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
267
|
-
),
|
|
268
|
-
team_id: Optional[str] = typer.Option(
|
|
269
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
270
|
-
),
|
|
271
|
-
jira_server: Optional[str] = typer.Option(
|
|
272
|
-
None,
|
|
273
|
-
"--jira-server",
|
|
274
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
275
|
-
),
|
|
276
|
-
jira_email: Optional[str] = typer.Option(
|
|
277
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
278
|
-
),
|
|
279
|
-
jira_project: Optional[str] = typer.Option(
|
|
280
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
281
|
-
),
|
|
282
|
-
github_owner: Optional[str] = typer.Option(
|
|
283
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
284
|
-
),
|
|
285
|
-
github_repo: Optional[str] = typer.Option(
|
|
286
|
-
None, "--github-repo", help="GitHub repository name"
|
|
287
|
-
),
|
|
288
|
-
github_token: Optional[str] = typer.Option(
|
|
289
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
290
|
-
),
|
|
291
|
-
) -> None:
|
|
292
|
-
"""Initialize mcp-ticketer for the current project.
|
|
293
|
-
|
|
294
|
-
Creates .mcp-ticketer/config.json in the current directory with
|
|
295
|
-
auto-detected or specified adapter configuration.
|
|
296
|
-
|
|
297
|
-
Examples:
|
|
298
|
-
# Auto-detect from .env.local
|
|
299
|
-
mcp-ticketer init
|
|
300
|
-
|
|
301
|
-
# Force specific adapter
|
|
302
|
-
mcp-ticketer init --adapter linear
|
|
303
|
-
|
|
304
|
-
# Initialize for different project
|
|
305
|
-
mcp-ticketer init --path /path/to/project
|
|
306
|
-
|
|
307
|
-
# Save globally (not recommended)
|
|
308
|
-
mcp-ticketer init --global
|
|
309
|
-
|
|
310
|
-
"""
|
|
311
|
-
from pathlib import Path
|
|
312
|
-
|
|
313
|
-
from ..core.env_discovery import discover_config
|
|
314
|
-
from ..core.project_config import ConfigResolver
|
|
315
|
-
|
|
316
|
-
# Determine project path
|
|
317
|
-
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
318
|
-
|
|
319
|
-
# Check if already initialized (unless using --global)
|
|
320
|
-
if not global_config:
|
|
321
|
-
config_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
322
|
-
|
|
323
|
-
if config_path.exists():
|
|
324
|
-
if not typer.confirm(
|
|
325
|
-
f"Configuration already exists at {config_path}. Overwrite?",
|
|
326
|
-
default=False,
|
|
327
|
-
):
|
|
328
|
-
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
329
|
-
raise typer.Exit(0)
|
|
330
|
-
|
|
331
|
-
# 1. Try auto-discovery if no adapter specified
|
|
332
|
-
discovered = None
|
|
333
|
-
adapter_type = adapter
|
|
334
|
-
|
|
335
|
-
if not adapter_type:
|
|
336
|
-
console.print(
|
|
337
|
-
"[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
|
|
338
|
-
)
|
|
339
|
-
discovered = discover_config(proj_path)
|
|
340
|
-
|
|
341
|
-
if discovered and discovered.adapters:
|
|
342
|
-
primary = discovered.get_primary_adapter()
|
|
343
|
-
if primary:
|
|
344
|
-
adapter_type = primary.adapter_type
|
|
345
|
-
console.print(
|
|
346
|
-
f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Show what was discovered
|
|
350
|
-
console.print(
|
|
351
|
-
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
352
|
-
)
|
|
353
|
-
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
354
|
-
else:
|
|
355
|
-
adapter_type = "aitrackdown" # Fallback
|
|
356
|
-
console.print(
|
|
357
|
-
"[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]"
|
|
358
|
-
)
|
|
359
|
-
else:
|
|
360
|
-
adapter_type = "aitrackdown" # Fallback
|
|
361
|
-
console.print(
|
|
362
|
-
"[yellow]⚠ No .env files found, defaulting to aitrackdown[/yellow]"
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
# 2. Create configuration based on adapter type
|
|
366
|
-
config = {"default_adapter": adapter_type, "adapters": {}}
|
|
367
|
-
|
|
368
|
-
# 3. If discovered and matches adapter_type, use discovered config
|
|
369
|
-
if discovered and adapter_type != "aitrackdown":
|
|
370
|
-
discovered_adapter = discovered.get_adapter_by_type(adapter_type)
|
|
371
|
-
if discovered_adapter:
|
|
372
|
-
config["adapters"][adapter_type] = discovered_adapter.config
|
|
373
|
-
|
|
374
|
-
# 4. Handle manual configuration for specific adapters
|
|
375
|
-
if adapter_type == "aitrackdown":
|
|
376
|
-
config["adapters"]["aitrackdown"] = {"base_path": base_path or ".aitrackdown"}
|
|
377
|
-
|
|
378
|
-
elif adapter_type == "linear":
|
|
379
|
-
# If not auto-discovered, build from CLI params
|
|
380
|
-
if adapter_type not in config["adapters"]:
|
|
381
|
-
linear_config = {}
|
|
382
|
-
|
|
383
|
-
# Team ID
|
|
384
|
-
if team_id:
|
|
385
|
-
linear_config["team_id"] = team_id
|
|
386
|
-
|
|
387
|
-
# API Key
|
|
388
|
-
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
389
|
-
if linear_api_key:
|
|
390
|
-
linear_config["api_key"] = linear_api_key
|
|
391
|
-
elif not discovered:
|
|
392
|
-
console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
|
|
393
|
-
console.print(
|
|
394
|
-
"Set LINEAR_API_KEY environment variable or use --api-key option"
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
if linear_config:
|
|
398
|
-
config["adapters"]["linear"] = linear_config
|
|
399
|
-
|
|
400
|
-
elif adapter_type == "jira":
|
|
401
|
-
# If not auto-discovered, build from CLI params
|
|
402
|
-
if adapter_type not in config["adapters"]:
|
|
403
|
-
server = jira_server or os.getenv("JIRA_SERVER")
|
|
404
|
-
email = jira_email or os.getenv("JIRA_EMAIL")
|
|
405
|
-
token = api_key or os.getenv("JIRA_API_TOKEN")
|
|
406
|
-
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
407
|
-
|
|
408
|
-
if not server:
|
|
409
|
-
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
410
|
-
console.print(
|
|
411
|
-
"Use --jira-server or set JIRA_SERVER environment variable"
|
|
412
|
-
)
|
|
413
|
-
raise typer.Exit(1)
|
|
414
|
-
|
|
415
|
-
if not email:
|
|
416
|
-
console.print("[red]Error:[/red] JIRA email is required")
|
|
417
|
-
console.print("Use --jira-email or set JIRA_EMAIL environment variable")
|
|
418
|
-
raise typer.Exit(1)
|
|
419
|
-
|
|
420
|
-
if not token:
|
|
421
|
-
console.print("[red]Error:[/red] JIRA API token is required")
|
|
422
|
-
console.print(
|
|
423
|
-
"Use --api-key or set JIRA_API_TOKEN environment variable"
|
|
424
|
-
)
|
|
425
|
-
console.print(
|
|
426
|
-
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
427
|
-
)
|
|
428
|
-
raise typer.Exit(1)
|
|
429
|
-
|
|
430
|
-
jira_config = {"server": server, "email": email, "api_token": token}
|
|
431
|
-
|
|
432
|
-
if project:
|
|
433
|
-
jira_config["project_key"] = project
|
|
434
|
-
|
|
435
|
-
config["adapters"]["jira"] = jira_config
|
|
436
|
-
|
|
437
|
-
elif adapter_type == "github":
|
|
438
|
-
# If not auto-discovered, build from CLI params
|
|
439
|
-
if adapter_type not in config["adapters"]:
|
|
440
|
-
owner = github_owner or os.getenv("GITHUB_OWNER")
|
|
441
|
-
repo = github_repo or os.getenv("GITHUB_REPO")
|
|
442
|
-
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
443
|
-
|
|
444
|
-
if not owner:
|
|
445
|
-
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
446
|
-
console.print(
|
|
447
|
-
"Use --github-owner or set GITHUB_OWNER environment variable"
|
|
448
|
-
)
|
|
449
|
-
raise typer.Exit(1)
|
|
450
|
-
|
|
451
|
-
if not repo:
|
|
452
|
-
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
453
|
-
console.print(
|
|
454
|
-
"Use --github-repo or set GITHUB_REPO environment variable"
|
|
455
|
-
)
|
|
456
|
-
raise typer.Exit(1)
|
|
457
|
-
|
|
458
|
-
if not token:
|
|
459
|
-
console.print(
|
|
460
|
-
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
461
|
-
)
|
|
462
|
-
console.print(
|
|
463
|
-
"Use --github-token or set GITHUB_TOKEN environment variable"
|
|
464
|
-
)
|
|
465
|
-
console.print(
|
|
466
|
-
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
467
|
-
)
|
|
468
|
-
console.print(
|
|
469
|
-
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]"
|
|
470
|
-
)
|
|
471
|
-
raise typer.Exit(1)
|
|
472
|
-
|
|
473
|
-
config["adapters"]["github"] = {
|
|
474
|
-
"owner": owner,
|
|
475
|
-
"repo": repo,
|
|
476
|
-
"token": token,
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
# 5. Save to appropriate location
|
|
480
|
-
if global_config:
|
|
481
|
-
# Save to ~/.mcp-ticketer/config.json
|
|
482
|
-
resolver = ConfigResolver(project_path=proj_path)
|
|
483
|
-
config_file_path = resolver.GLOBAL_CONFIG_PATH
|
|
484
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
485
|
-
|
|
486
|
-
with open(config_file_path, "w") as f:
|
|
487
|
-
json.dump(config, f, indent=2)
|
|
488
|
-
|
|
489
|
-
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
490
|
-
console.print(f"[dim]Global configuration saved to {config_file_path}[/dim]")
|
|
491
|
-
else:
|
|
492
|
-
# Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
|
|
493
|
-
config_file_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
494
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
495
|
-
|
|
496
|
-
with open(config_file_path, "w") as f:
|
|
497
|
-
json.dump(config, f, indent=2)
|
|
498
|
-
|
|
499
|
-
console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
|
|
500
|
-
console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
|
|
501
|
-
|
|
502
|
-
# Add .mcp-ticketer to .gitignore if not already there
|
|
503
|
-
gitignore_path = proj_path / ".gitignore"
|
|
504
|
-
if gitignore_path.exists():
|
|
505
|
-
gitignore_content = gitignore_path.read_text()
|
|
506
|
-
if ".mcp-ticketer" not in gitignore_content:
|
|
507
|
-
with open(gitignore_path, "a") as f:
|
|
508
|
-
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
509
|
-
console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
510
|
-
else:
|
|
511
|
-
# Create .gitignore if it doesn't exist
|
|
512
|
-
with open(gitignore_path, "w") as f:
|
|
513
|
-
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
514
|
-
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
@app.command()
|
|
518
|
-
def install(
|
|
519
|
-
adapter: Optional[str] = typer.Option(
|
|
520
|
-
None,
|
|
521
|
-
"--adapter",
|
|
522
|
-
"-a",
|
|
523
|
-
help="Adapter type to use (auto-detected from .env if not specified)",
|
|
524
|
-
),
|
|
525
|
-
project_path: Optional[str] = typer.Option(
|
|
526
|
-
None, "--path", help="Project path (default: current directory)"
|
|
527
|
-
),
|
|
528
|
-
global_config: bool = typer.Option(
|
|
529
|
-
False,
|
|
530
|
-
"--global",
|
|
531
|
-
"-g",
|
|
532
|
-
help="Save to global config instead of project-specific",
|
|
533
|
-
),
|
|
534
|
-
base_path: Optional[str] = typer.Option(
|
|
535
|
-
None,
|
|
536
|
-
"--base-path",
|
|
537
|
-
"-p",
|
|
538
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
539
|
-
),
|
|
540
|
-
api_key: Optional[str] = typer.Option(
|
|
541
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
542
|
-
),
|
|
543
|
-
team_id: Optional[str] = typer.Option(
|
|
544
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
545
|
-
),
|
|
546
|
-
jira_server: Optional[str] = typer.Option(
|
|
547
|
-
None,
|
|
548
|
-
"--jira-server",
|
|
549
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
550
|
-
),
|
|
551
|
-
jira_email: Optional[str] = typer.Option(
|
|
552
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
553
|
-
),
|
|
554
|
-
jira_project: Optional[str] = typer.Option(
|
|
555
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
556
|
-
),
|
|
557
|
-
github_owner: Optional[str] = typer.Option(
|
|
558
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
559
|
-
),
|
|
560
|
-
github_repo: Optional[str] = typer.Option(
|
|
561
|
-
None, "--github-repo", help="GitHub repository name"
|
|
562
|
-
),
|
|
563
|
-
github_token: Optional[str] = typer.Option(
|
|
564
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
565
|
-
),
|
|
566
|
-
) -> None:
|
|
567
|
-
"""Initialize mcp-ticketer for the current project (alias for init).
|
|
568
|
-
|
|
569
|
-
This command is synonymous with 'init' and provides the same functionality.
|
|
570
|
-
Creates .mcp-ticketer/config.json in the current directory with
|
|
571
|
-
auto-detected or specified adapter configuration.
|
|
572
|
-
|
|
573
|
-
Examples:
|
|
574
|
-
# Auto-detect from .env.local
|
|
575
|
-
mcp-ticketer install
|
|
576
|
-
|
|
577
|
-
# Force specific adapter
|
|
578
|
-
mcp-ticketer install --adapter linear
|
|
579
|
-
|
|
580
|
-
# Initialize for different project
|
|
581
|
-
mcp-ticketer install --path /path/to/project
|
|
582
|
-
|
|
583
|
-
# Save globally (not recommended)
|
|
584
|
-
mcp-ticketer install --global
|
|
585
|
-
|
|
586
|
-
"""
|
|
587
|
-
# Call init with all parameters
|
|
588
|
-
init(
|
|
589
|
-
adapter=adapter,
|
|
590
|
-
project_path=project_path,
|
|
591
|
-
global_config=global_config,
|
|
592
|
-
base_path=base_path,
|
|
593
|
-
api_key=api_key,
|
|
594
|
-
team_id=team_id,
|
|
595
|
-
jira_server=jira_server,
|
|
596
|
-
jira_email=jira_email,
|
|
597
|
-
jira_project=jira_project,
|
|
598
|
-
github_owner=github_owner,
|
|
599
|
-
github_repo=github_repo,
|
|
600
|
-
github_token=github_token,
|
|
601
|
-
)
|
|
602
|
-
|
|
603
|
-
|
|
604
328
|
@app.command("set")
|
|
605
329
|
def set_config(
|
|
606
|
-
adapter:
|
|
330
|
+
adapter: AdapterType | None = typer.Option(
|
|
607
331
|
None, "--adapter", "-a", help="Set default adapter"
|
|
608
332
|
),
|
|
609
|
-
team_key:
|
|
333
|
+
team_key: str | None = typer.Option(
|
|
610
334
|
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
611
335
|
),
|
|
612
|
-
team_id:
|
|
613
|
-
owner:
|
|
614
|
-
|
|
615
|
-
),
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
|
|
619
|
-
base_path: Optional[str] = typer.Option(
|
|
336
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
|
|
337
|
+
owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
|
|
338
|
+
repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
|
|
339
|
+
server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
|
|
340
|
+
project: str | None = typer.Option(None, "--project", help="JIRA project key"),
|
|
341
|
+
base_path: str | None = typer.Option(
|
|
620
342
|
None, "--base-path", help="AITrackdown base path"
|
|
621
343
|
),
|
|
622
344
|
) -> None:
|
|
@@ -706,16 +428,12 @@ def set_config(
|
|
|
706
428
|
@app.command("configure")
|
|
707
429
|
def configure_command(
|
|
708
430
|
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
709
|
-
adapter:
|
|
431
|
+
adapter: str | None = typer.Option(
|
|
710
432
|
None, "--adapter", help="Set default adapter type"
|
|
711
433
|
),
|
|
712
|
-
api_key:
|
|
713
|
-
project_id:
|
|
714
|
-
|
|
715
|
-
),
|
|
716
|
-
team_id: Optional[str] = typer.Option(
|
|
717
|
-
None, "--team-id", help="Set team ID (Linear)"
|
|
718
|
-
),
|
|
434
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
435
|
+
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
436
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
719
437
|
global_scope: bool = typer.Option(
|
|
720
438
|
False,
|
|
721
439
|
"--global",
|
|
@@ -749,6 +467,26 @@ def configure_command(
|
|
|
749
467
|
configure_wizard()
|
|
750
468
|
|
|
751
469
|
|
|
470
|
+
@app.command("config")
|
|
471
|
+
def config_alias(
|
|
472
|
+
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
473
|
+
adapter: str | None = typer.Option(
|
|
474
|
+
None, "--adapter", help="Set default adapter type"
|
|
475
|
+
),
|
|
476
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
477
|
+
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
478
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
479
|
+
global_scope: bool = typer.Option(
|
|
480
|
+
False,
|
|
481
|
+
"--global",
|
|
482
|
+
"-g",
|
|
483
|
+
help="Save to global config instead of project-specific",
|
|
484
|
+
),
|
|
485
|
+
) -> None:
|
|
486
|
+
"""Alias for configure command - shorter syntax."""
|
|
487
|
+
configure_command(show, adapter, api_key, project_id, team_id, global_scope)
|
|
488
|
+
|
|
489
|
+
|
|
752
490
|
@app.command("migrate-config")
|
|
753
491
|
def migrate_config(
|
|
754
492
|
dry_run: bool = typer.Option(
|
|
@@ -766,822 +504,122 @@ def migrate_config(
|
|
|
766
504
|
migrate_config_command(dry_run=dry_run)
|
|
767
505
|
|
|
768
506
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
"""Show queue and worker status."""
|
|
772
|
-
queue = Queue()
|
|
773
|
-
manager = WorkerManager()
|
|
774
|
-
|
|
775
|
-
# Get queue stats
|
|
776
|
-
stats = queue.get_stats()
|
|
777
|
-
pending = stats.get(QueueStatus.PENDING.value, 0)
|
|
778
|
-
|
|
779
|
-
# Show queue status
|
|
780
|
-
console.print("[bold]Queue Status:[/bold]")
|
|
781
|
-
console.print(f" Pending: {pending}")
|
|
782
|
-
console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
|
|
783
|
-
console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
|
|
784
|
-
console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
|
|
785
|
-
|
|
786
|
-
# Show worker status
|
|
787
|
-
worker_status = manager.get_status()
|
|
788
|
-
if worker_status["running"]:
|
|
789
|
-
console.print(
|
|
790
|
-
f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
|
|
791
|
-
)
|
|
792
|
-
else:
|
|
793
|
-
console.print("\n[red]○ Worker is not running[/red]")
|
|
794
|
-
if pending > 0:
|
|
795
|
-
console.print(
|
|
796
|
-
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
|
|
797
|
-
)
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
@app.command()
|
|
801
|
-
def health(
|
|
802
|
-
auto_repair: bool = typer.Option(False, "--auto-repair", help="Attempt automatic repair of issues"),
|
|
803
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed health information")
|
|
804
|
-
) -> None:
|
|
805
|
-
"""Check queue system health and detect issues immediately."""
|
|
806
|
-
|
|
807
|
-
health_monitor = QueueHealthMonitor()
|
|
808
|
-
health = health_monitor.check_health()
|
|
809
|
-
|
|
810
|
-
# Display overall status
|
|
811
|
-
status_color = {
|
|
812
|
-
HealthStatus.HEALTHY: "green",
|
|
813
|
-
HealthStatus.WARNING: "yellow",
|
|
814
|
-
HealthStatus.CRITICAL: "red",
|
|
815
|
-
HealthStatus.FAILED: "red"
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
status_icon = {
|
|
819
|
-
HealthStatus.HEALTHY: "✓",
|
|
820
|
-
HealthStatus.WARNING: "⚠️",
|
|
821
|
-
HealthStatus.CRITICAL: "🚨",
|
|
822
|
-
HealthStatus.FAILED: "❌"
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
color = status_color.get(health["status"], "white")
|
|
826
|
-
icon = status_icon.get(health["status"], "?")
|
|
827
|
-
|
|
828
|
-
console.print(f"[{color}]{icon} Queue Health: {health['status'].upper()}[/{color}]")
|
|
829
|
-
console.print(f"Last checked: {health['timestamp']}")
|
|
830
|
-
|
|
831
|
-
# Display alerts
|
|
832
|
-
if health["alerts"]:
|
|
833
|
-
console.print("\n[bold]Issues Found:[/bold]")
|
|
834
|
-
for alert in health["alerts"]:
|
|
835
|
-
alert_color = status_color.get(alert["level"], "white")
|
|
836
|
-
console.print(f"[{alert_color}] • {alert['message']}[/{alert_color}]")
|
|
837
|
-
|
|
838
|
-
if verbose and alert.get("details"):
|
|
839
|
-
for key, value in alert["details"].items():
|
|
840
|
-
console.print(f" {key}: {value}")
|
|
841
|
-
else:
|
|
842
|
-
console.print("\n[green]✓ No issues detected[/green]")
|
|
843
|
-
|
|
844
|
-
# Auto-repair if requested
|
|
845
|
-
if auto_repair and health["status"] in [HealthStatus.CRITICAL, HealthStatus.WARNING]:
|
|
846
|
-
console.print("\n[yellow]Attempting automatic repair...[/yellow]")
|
|
847
|
-
repair_result = health_monitor.auto_repair()
|
|
848
|
-
|
|
849
|
-
if repair_result["actions_taken"]:
|
|
850
|
-
console.print("[green]Repair actions taken:[/green]")
|
|
851
|
-
for action in repair_result["actions_taken"]:
|
|
852
|
-
console.print(f"[green] ✓ {action}[/green]")
|
|
853
|
-
|
|
854
|
-
# Re-check health
|
|
855
|
-
console.print("\n[yellow]Re-checking health after repair...[/yellow]")
|
|
856
|
-
new_health = health_monitor.check_health()
|
|
857
|
-
new_color = status_color.get(new_health["status"], "white")
|
|
858
|
-
new_icon = status_icon.get(new_health["status"], "?")
|
|
859
|
-
console.print(f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]")
|
|
860
|
-
else:
|
|
861
|
-
console.print("[yellow]No repair actions available[/yellow]")
|
|
862
|
-
|
|
863
|
-
# Exit with appropriate code
|
|
864
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
865
|
-
raise typer.Exit(1)
|
|
866
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
867
|
-
raise typer.Exit(2)
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
@app.command()
|
|
871
|
-
def create(
|
|
872
|
-
title: str = typer.Argument(..., help="Ticket title"),
|
|
873
|
-
description: Optional[str] = typer.Option(
|
|
874
|
-
None, "--description", "-d", help="Ticket description"
|
|
875
|
-
),
|
|
876
|
-
priority: Priority = typer.Option(
|
|
877
|
-
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
878
|
-
),
|
|
879
|
-
tags: Optional[list[str]] = typer.Option(
|
|
880
|
-
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
881
|
-
),
|
|
882
|
-
assignee: Optional[str] = typer.Option(
|
|
883
|
-
None, "--assignee", "-a", help="Assignee username"
|
|
884
|
-
),
|
|
885
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
886
|
-
None, "--adapter", help="Override default adapter"
|
|
887
|
-
),
|
|
888
|
-
) -> None:
|
|
889
|
-
"""Create a new ticket with comprehensive health checks."""
|
|
890
|
-
|
|
891
|
-
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
892
|
-
health_monitor = QueueHealthMonitor()
|
|
893
|
-
health = health_monitor.check_health()
|
|
894
|
-
|
|
895
|
-
# Display health status
|
|
896
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
897
|
-
console.print("[red]🚨 CRITICAL: Queue system has serious issues![/red]")
|
|
898
|
-
for alert in health["alerts"]:
|
|
899
|
-
if alert["level"] == "critical":
|
|
900
|
-
console.print(f"[red] • {alert['message']}[/red]")
|
|
901
|
-
|
|
902
|
-
# Attempt auto-repair
|
|
903
|
-
console.print("[yellow]Attempting automatic repair...[/yellow]")
|
|
904
|
-
repair_result = health_monitor.auto_repair()
|
|
905
|
-
|
|
906
|
-
if repair_result["actions_taken"]:
|
|
907
|
-
for action in repair_result["actions_taken"]:
|
|
908
|
-
console.print(f"[yellow] ✓ {action}[/yellow]")
|
|
909
|
-
|
|
910
|
-
# Re-check health after repair
|
|
911
|
-
health = health_monitor.check_health()
|
|
912
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
913
|
-
console.print("[red]❌ Auto-repair failed. Manual intervention required.[/red]")
|
|
914
|
-
console.print("[red]Cannot safely create ticket. Please check system status.[/red]")
|
|
915
|
-
raise typer.Exit(1)
|
|
916
|
-
else:
|
|
917
|
-
console.print("[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]")
|
|
918
|
-
else:
|
|
919
|
-
console.print("[red]❌ No repair actions available. Manual intervention required.[/red]")
|
|
920
|
-
raise typer.Exit(1)
|
|
921
|
-
|
|
922
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
923
|
-
console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
|
|
924
|
-
for alert in health["alerts"]:
|
|
925
|
-
if alert["level"] == "warning":
|
|
926
|
-
console.print(f"[yellow] • {alert['message']}[/yellow]")
|
|
927
|
-
console.print("[yellow]Proceeding with ticket creation...[/yellow]")
|
|
928
|
-
|
|
929
|
-
# Get the adapter name
|
|
930
|
-
config = load_config()
|
|
931
|
-
adapter_name = (
|
|
932
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
933
|
-
)
|
|
934
|
-
|
|
935
|
-
# Create task data
|
|
936
|
-
task_data = {
|
|
937
|
-
"title": title,
|
|
938
|
-
"description": description,
|
|
939
|
-
"priority": priority.value if isinstance(priority, Priority) else priority,
|
|
940
|
-
"tags": tags or [],
|
|
941
|
-
"assignee": assignee,
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
# Add to queue
|
|
945
|
-
queue = Queue()
|
|
946
|
-
queue_id = queue.add(
|
|
947
|
-
ticket_data=task_data, adapter=adapter_name, operation="create"
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
# Register in ticket registry for tracking
|
|
951
|
-
registry = TicketRegistry()
|
|
952
|
-
registry.register_ticket_operation(queue_id, adapter_name, "create", title, task_data)
|
|
953
|
-
|
|
954
|
-
console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
|
|
955
|
-
console.print(f" Title: {title}")
|
|
956
|
-
console.print(f" Priority: {priority}")
|
|
957
|
-
console.print(f" Adapter: {adapter_name}")
|
|
958
|
-
console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
|
|
959
|
-
|
|
960
|
-
# Start worker if needed with immediate feedback
|
|
961
|
-
manager = WorkerManager()
|
|
962
|
-
worker_started = manager.start_if_needed()
|
|
963
|
-
|
|
964
|
-
if worker_started:
|
|
965
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
966
|
-
|
|
967
|
-
# Give immediate feedback on processing
|
|
968
|
-
import time
|
|
969
|
-
time.sleep(1) # Brief pause to let worker start
|
|
970
|
-
|
|
971
|
-
# Check if item is being processed
|
|
972
|
-
item = queue.get_item(queue_id)
|
|
973
|
-
if item and item.status == QueueStatus.PROCESSING:
|
|
974
|
-
console.print("[green]✓ Item is being processed by worker[/green]")
|
|
975
|
-
elif item and item.status == QueueStatus.PENDING:
|
|
976
|
-
console.print("[yellow]⏳ Item is queued for processing[/yellow]")
|
|
977
|
-
else:
|
|
978
|
-
console.print("[red]⚠️ Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]")
|
|
979
|
-
else:
|
|
980
|
-
# Worker didn't start - this is a problem
|
|
981
|
-
pending_count = queue.get_pending_count()
|
|
982
|
-
if pending_count > 1: # More than just this item
|
|
983
|
-
console.print(f"[red]❌ Worker failed to start with {pending_count} pending items![/red]")
|
|
984
|
-
console.print("[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]")
|
|
985
|
-
else:
|
|
986
|
-
console.print("[yellow]Worker not started (no other pending items)[/yellow]")
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
@app.command("list")
|
|
990
|
-
def list_tickets(
|
|
991
|
-
state: Optional[TicketState] = typer.Option(
|
|
992
|
-
None, "--state", "-s", help="Filter by state"
|
|
993
|
-
),
|
|
994
|
-
priority: Optional[Priority] = typer.Option(
|
|
995
|
-
None, "--priority", "-p", help="Filter by priority"
|
|
996
|
-
),
|
|
997
|
-
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
998
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
999
|
-
None, "--adapter", help="Override default adapter"
|
|
1000
|
-
),
|
|
1001
|
-
) -> None:
|
|
1002
|
-
"""List tickets with optional filters."""
|
|
1003
|
-
|
|
1004
|
-
async def _list():
|
|
1005
|
-
adapter_instance = get_adapter(
|
|
1006
|
-
override_adapter=adapter.value if adapter else None
|
|
1007
|
-
)
|
|
1008
|
-
filters = {}
|
|
1009
|
-
if state:
|
|
1010
|
-
filters["state"] = state
|
|
1011
|
-
if priority:
|
|
1012
|
-
filters["priority"] = priority
|
|
1013
|
-
return await adapter_instance.list(limit=limit, filters=filters)
|
|
1014
|
-
|
|
1015
|
-
tickets = asyncio.run(_list())
|
|
507
|
+
# Add ticket command group to main app
|
|
508
|
+
app.add_typer(ticket_app, name="ticket")
|
|
1016
509
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
return
|
|
510
|
+
# Add platform command group to main app
|
|
511
|
+
app.add_typer(platform_app, name="platform")
|
|
1020
512
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
table.add_column("ID", style="cyan", no_wrap=True)
|
|
1024
|
-
table.add_column("Title", style="white")
|
|
1025
|
-
table.add_column("State", style="green")
|
|
1026
|
-
table.add_column("Priority", style="yellow")
|
|
1027
|
-
table.add_column("Assignee", style="blue")
|
|
1028
|
-
|
|
1029
|
-
for ticket in tickets:
|
|
1030
|
-
table.add_row(
|
|
1031
|
-
ticket.id or "N/A",
|
|
1032
|
-
ticket.title,
|
|
1033
|
-
ticket.state,
|
|
1034
|
-
ticket.priority,
|
|
1035
|
-
ticket.assignee or "-",
|
|
1036
|
-
)
|
|
513
|
+
# Add queue command to main app
|
|
514
|
+
app.add_typer(queue_app, name="queue")
|
|
1037
515
|
|
|
1038
|
-
|
|
516
|
+
# Add discover command to main app
|
|
517
|
+
app.add_typer(discover_app, name="discover")
|
|
1039
518
|
|
|
519
|
+
# Add instructions command to main app
|
|
520
|
+
app.add_typer(instruction_app, name="instructions")
|
|
1040
521
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
1045
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1046
|
-
None, "--adapter", help="Override default adapter"
|
|
1047
|
-
),
|
|
1048
|
-
) -> None:
|
|
1049
|
-
"""Show detailed ticket information."""
|
|
522
|
+
# Add setup and init commands to main app
|
|
523
|
+
app.command()(setup)
|
|
524
|
+
app.command()(init)
|
|
1050
525
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
ticket = await adapter_instance.read(ticket_id)
|
|
1056
|
-
ticket_comments = None
|
|
1057
|
-
if comments and ticket:
|
|
1058
|
-
ticket_comments = await adapter_instance.get_comments(ticket_id)
|
|
1059
|
-
return ticket, ticket_comments
|
|
1060
|
-
|
|
1061
|
-
ticket, ticket_comments = asyncio.run(_show())
|
|
1062
|
-
|
|
1063
|
-
if not ticket:
|
|
1064
|
-
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
1065
|
-
raise typer.Exit(1)
|
|
1066
|
-
|
|
1067
|
-
# Display ticket details
|
|
1068
|
-
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
1069
|
-
console.print(f"Title: {ticket.title}")
|
|
1070
|
-
console.print(f"State: [green]{ticket.state}[/green]")
|
|
1071
|
-
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
1072
|
-
|
|
1073
|
-
if ticket.description:
|
|
1074
|
-
console.print("\n[dim]Description:[/dim]")
|
|
1075
|
-
console.print(ticket.description)
|
|
1076
|
-
|
|
1077
|
-
if ticket.tags:
|
|
1078
|
-
console.print(f"\nTags: {', '.join(ticket.tags)}")
|
|
1079
|
-
|
|
1080
|
-
if ticket.assignee:
|
|
1081
|
-
console.print(f"Assignee: {ticket.assignee}")
|
|
1082
|
-
|
|
1083
|
-
# Display comments if requested
|
|
1084
|
-
if ticket_comments:
|
|
1085
|
-
console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
|
|
1086
|
-
for comment in ticket_comments:
|
|
1087
|
-
console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
|
|
1088
|
-
console.print(comment.content)
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
@app.command()
|
|
1092
|
-
def update(
|
|
1093
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1094
|
-
title: Optional[str] = typer.Option(None, "--title", help="New title"),
|
|
1095
|
-
description: Optional[str] = typer.Option(
|
|
1096
|
-
None, "--description", "-d", help="New description"
|
|
1097
|
-
),
|
|
1098
|
-
priority: Optional[Priority] = typer.Option(
|
|
1099
|
-
None, "--priority", "-p", help="New priority"
|
|
1100
|
-
),
|
|
1101
|
-
assignee: Optional[str] = typer.Option(
|
|
1102
|
-
None, "--assignee", "-a", help="New assignee"
|
|
1103
|
-
),
|
|
1104
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1105
|
-
None, "--adapter", help="Override default adapter"
|
|
1106
|
-
),
|
|
1107
|
-
) -> None:
|
|
1108
|
-
"""Update ticket fields."""
|
|
1109
|
-
updates = {}
|
|
1110
|
-
if title:
|
|
1111
|
-
updates["title"] = title
|
|
1112
|
-
if description:
|
|
1113
|
-
updates["description"] = description
|
|
1114
|
-
if priority:
|
|
1115
|
-
updates["priority"] = (
|
|
1116
|
-
priority.value if isinstance(priority, Priority) else priority
|
|
1117
|
-
)
|
|
1118
|
-
if assignee:
|
|
1119
|
-
updates["assignee"] = assignee
|
|
526
|
+
# Add platform installer commands to main app
|
|
527
|
+
app.command()(install)
|
|
528
|
+
app.command()(remove)
|
|
529
|
+
app.command()(uninstall)
|
|
1120
530
|
|
|
1121
|
-
if not updates:
|
|
1122
|
-
console.print("[yellow]No updates specified[/yellow]")
|
|
1123
|
-
raise typer.Exit(1)
|
|
1124
531
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
# Add ticket_id to updates
|
|
1132
|
-
updates["ticket_id"] = ticket_id
|
|
1133
|
-
|
|
1134
|
-
# Add to queue
|
|
1135
|
-
queue = Queue()
|
|
1136
|
-
queue_id = queue.add(ticket_data=updates, adapter=adapter_name, operation="update")
|
|
1137
|
-
|
|
1138
|
-
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
1139
|
-
for key, value in updates.items():
|
|
1140
|
-
if key != "ticket_id":
|
|
1141
|
-
console.print(f" {key}: {value}")
|
|
1142
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
1143
|
-
|
|
1144
|
-
# Start worker if needed
|
|
1145
|
-
manager = WorkerManager()
|
|
1146
|
-
if manager.start_if_needed():
|
|
1147
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
@app.command()
|
|
1151
|
-
def transition(
|
|
1152
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1153
|
-
state_positional: Optional[TicketState] = typer.Argument(
|
|
1154
|
-
None, help="Target state (positional - deprecated, use --state instead)"
|
|
532
|
+
# Add diagnostics command
|
|
533
|
+
@app.command("doctor")
|
|
534
|
+
def doctor_command(
|
|
535
|
+
output_file: str | None = typer.Option(
|
|
536
|
+
None, "--output", "-o", help="Save full report to file"
|
|
1155
537
|
),
|
|
1156
|
-
|
|
1157
|
-
|
|
538
|
+
json_output: bool = typer.Option(
|
|
539
|
+
False, "--json", help="Output report in JSON format"
|
|
1158
540
|
),
|
|
1159
|
-
|
|
1160
|
-
|
|
541
|
+
simple: bool = typer.Option(
|
|
542
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1161
543
|
),
|
|
1162
544
|
) -> None:
|
|
1163
|
-
"""
|
|
1164
|
-
|
|
1165
|
-
Examples:
|
|
1166
|
-
# Recommended syntax with flag:
|
|
1167
|
-
mcp-ticketer transition BTA-215 --state done
|
|
1168
|
-
mcp-ticketer transition BTA-215 -s in_progress
|
|
1169
|
-
|
|
1170
|
-
# Legacy positional syntax (still supported):
|
|
1171
|
-
mcp-ticketer transition BTA-215 done
|
|
1172
|
-
|
|
1173
|
-
"""
|
|
1174
|
-
# Determine which state to use (prefer flag over positional)
|
|
1175
|
-
target_state = state if state is not None else state_positional
|
|
1176
|
-
|
|
1177
|
-
if target_state is None:
|
|
1178
|
-
console.print("[red]Error: State is required[/red]")
|
|
1179
|
-
console.print(
|
|
1180
|
-
"Use either:\n"
|
|
1181
|
-
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
1182
|
-
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
1183
|
-
)
|
|
1184
|
-
raise typer.Exit(1)
|
|
1185
|
-
|
|
1186
|
-
# Get the adapter name
|
|
1187
|
-
config = load_config()
|
|
1188
|
-
adapter_name = (
|
|
1189
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
1190
|
-
)
|
|
1191
|
-
|
|
1192
|
-
# Add to queue
|
|
1193
|
-
queue = Queue()
|
|
1194
|
-
queue_id = queue.add(
|
|
1195
|
-
ticket_data={
|
|
1196
|
-
"ticket_id": ticket_id,
|
|
1197
|
-
"state": (
|
|
1198
|
-
target_state.value if hasattr(target_state, "value") else target_state
|
|
1199
|
-
),
|
|
1200
|
-
},
|
|
1201
|
-
adapter=adapter_name,
|
|
1202
|
-
operation="transition",
|
|
1203
|
-
)
|
|
1204
|
-
|
|
1205
|
-
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
1206
|
-
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
1207
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
1208
|
-
|
|
1209
|
-
# Start worker if needed
|
|
1210
|
-
manager = WorkerManager()
|
|
1211
|
-
if manager.start_if_needed():
|
|
1212
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
@app.command()
|
|
1216
|
-
def search(
|
|
1217
|
-
query: Optional[str] = typer.Argument(None, help="Search query"),
|
|
1218
|
-
state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
|
|
1219
|
-
priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
|
|
1220
|
-
assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
|
|
1221
|
-
limit: int = typer.Option(10, "--limit", "-l"),
|
|
1222
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1223
|
-
None, "--adapter", help="Override default adapter"
|
|
1224
|
-
),
|
|
1225
|
-
) -> None:
|
|
1226
|
-
"""Search tickets with advanced query."""
|
|
1227
|
-
|
|
1228
|
-
async def _search():
|
|
1229
|
-
adapter_instance = get_adapter(
|
|
1230
|
-
override_adapter=adapter.value if adapter else None
|
|
1231
|
-
)
|
|
1232
|
-
search_query = SearchQuery(
|
|
1233
|
-
query=query,
|
|
1234
|
-
state=state,
|
|
1235
|
-
priority=priority,
|
|
1236
|
-
assignee=assignee,
|
|
1237
|
-
limit=limit,
|
|
1238
|
-
)
|
|
1239
|
-
return await adapter_instance.search(search_query)
|
|
1240
|
-
|
|
1241
|
-
tickets = asyncio.run(_search())
|
|
1242
|
-
|
|
1243
|
-
if not tickets:
|
|
1244
|
-
console.print("[yellow]No tickets found matching query[/yellow]")
|
|
1245
|
-
return
|
|
1246
|
-
|
|
1247
|
-
# Display results
|
|
1248
|
-
console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
|
|
1249
|
-
|
|
1250
|
-
for ticket in tickets:
|
|
1251
|
-
console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
|
|
1252
|
-
console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
|
|
1253
|
-
if ticket.assignee:
|
|
1254
|
-
console.print(f" Assignee: {ticket.assignee}")
|
|
1255
|
-
console.print()
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
# Add queue command to main app
|
|
1259
|
-
app.add_typer(queue_app, name="queue")
|
|
1260
|
-
|
|
1261
|
-
# Add discover command to main app
|
|
1262
|
-
app.add_typer(discover_app, name="discover")
|
|
1263
|
-
|
|
1264
|
-
# Add diagnostics command
|
|
1265
|
-
@app.command()
|
|
1266
|
-
def diagnose(
|
|
1267
|
-
output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Save full report to file"),
|
|
1268
|
-
json_output: bool = typer.Option(False, "--json", help="Output report in JSON format"),
|
|
1269
|
-
simple: bool = typer.Option(False, "--simple", help="Use simple diagnostics (no heavy dependencies)"),
|
|
1270
|
-
) -> None:
|
|
1271
|
-
"""Run comprehensive system diagnostics and health check."""
|
|
545
|
+
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
1272
546
|
if simple:
|
|
1273
547
|
from .simple_health import simple_diagnose
|
|
548
|
+
|
|
1274
549
|
report = simple_diagnose()
|
|
1275
550
|
if output_file:
|
|
1276
551
|
import json
|
|
1277
|
-
|
|
552
|
+
|
|
553
|
+
with open(output_file, "w") as f:
|
|
1278
554
|
json.dump(report, f, indent=2)
|
|
1279
555
|
console.print(f"\n📄 Report saved to: {output_file}")
|
|
1280
556
|
if json_output:
|
|
1281
557
|
import json
|
|
558
|
+
|
|
1282
559
|
console.print("\n" + json.dumps(report, indent=2))
|
|
1283
560
|
if report["issues"]:
|
|
1284
|
-
raise typer.Exit(1)
|
|
561
|
+
raise typer.Exit(1) from None
|
|
1285
562
|
else:
|
|
1286
563
|
try:
|
|
1287
|
-
asyncio.run(
|
|
564
|
+
asyncio.run(
|
|
565
|
+
run_diagnostics(output_file=output_file, json_output=json_output)
|
|
566
|
+
)
|
|
567
|
+
except typer.Exit:
|
|
568
|
+
# typer.Exit is expected - don't fall back to simple diagnostics
|
|
569
|
+
raise
|
|
1288
570
|
except Exception as e:
|
|
1289
571
|
console.print(f"⚠️ Full diagnostics failed: {e}")
|
|
1290
572
|
console.print("🔄 Falling back to simple diagnostics...")
|
|
1291
573
|
from .simple_health import simple_diagnose
|
|
574
|
+
|
|
1292
575
|
report = simple_diagnose()
|
|
1293
576
|
if report["issues"]:
|
|
1294
|
-
raise typer.Exit(1)
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
@app.command()
|
|
1298
|
-
def health() -> None:
|
|
1299
|
-
"""Quick health check - shows system status summary."""
|
|
1300
|
-
from .simple_health import simple_health_check
|
|
1301
|
-
|
|
1302
|
-
result = simple_health_check()
|
|
1303
|
-
if result != 0:
|
|
1304
|
-
raise typer.Exit(result)
|
|
1305
|
-
|
|
1306
|
-
# Create MCP configuration command group
|
|
1307
|
-
mcp_app = typer.Typer(
|
|
1308
|
-
name="mcp",
|
|
1309
|
-
help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
|
|
1310
|
-
add_completion=False,
|
|
1311
|
-
)
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
@app.command()
|
|
1315
|
-
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
1316
|
-
"""Check status of a queued operation."""
|
|
1317
|
-
queue = Queue()
|
|
1318
|
-
item = queue.get_item(queue_id)
|
|
1319
|
-
|
|
1320
|
-
if not item:
|
|
1321
|
-
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
1322
|
-
raise typer.Exit(1)
|
|
1323
|
-
|
|
1324
|
-
# Display status
|
|
1325
|
-
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
1326
|
-
console.print(f"Operation: {item.operation}")
|
|
1327
|
-
console.print(f"Adapter: {item.adapter}")
|
|
1328
|
-
|
|
1329
|
-
# Status with color
|
|
1330
|
-
if item.status == QueueStatus.COMPLETED:
|
|
1331
|
-
console.print(f"Status: [green]{item.status}[/green]")
|
|
1332
|
-
elif item.status == QueueStatus.FAILED:
|
|
1333
|
-
console.print(f"Status: [red]{item.status}[/red]")
|
|
1334
|
-
elif item.status == QueueStatus.PROCESSING:
|
|
1335
|
-
console.print(f"Status: [yellow]{item.status}[/yellow]")
|
|
1336
|
-
else:
|
|
1337
|
-
console.print(f"Status: {item.status}")
|
|
577
|
+
raise typer.Exit(1) from None
|
|
1338
578
|
|
|
1339
|
-
# Timestamps
|
|
1340
|
-
console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
1341
|
-
if item.processed_at:
|
|
1342
|
-
console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
1343
579
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
console.print("\n[green]Result:[/green]")
|
|
1349
|
-
for key, value in item.result.items():
|
|
1350
|
-
console.print(f" {key}: {value}")
|
|
1351
|
-
|
|
1352
|
-
if item.retry_count > 0:
|
|
1353
|
-
console.print(f"\nRetry Count: {item.retry_count}")
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
@app.command()
|
|
1357
|
-
def serve(
|
|
1358
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
1359
|
-
None, "--adapter", "-a", help="Override default adapter type"
|
|
1360
|
-
),
|
|
1361
|
-
base_path: Optional[str] = typer.Option(
|
|
1362
|
-
None, "--base-path", help="Base path for AITrackdown adapter"
|
|
1363
|
-
),
|
|
1364
|
-
):
|
|
1365
|
-
"""Start MCP server for JSON-RPC communication over stdio.
|
|
1366
|
-
|
|
1367
|
-
This command is used by Claude Code/Desktop when connecting to the MCP server.
|
|
1368
|
-
You typically don't need to run this manually - use 'mcp-ticketer mcp' to configure.
|
|
1369
|
-
|
|
1370
|
-
Configuration Resolution:
|
|
1371
|
-
- When MCP server starts, it uses the current working directory (cwd)
|
|
1372
|
-
- The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
|
|
1373
|
-
- Configuration is loaded with this priority:
|
|
1374
|
-
1. Project-specific: .mcp-ticketer/config.json in cwd
|
|
1375
|
-
2. Global: ~/.mcp-ticketer/config.json
|
|
1376
|
-
3. Default: aitrackdown adapter with .aitrackdown base path
|
|
1377
|
-
"""
|
|
1378
|
-
from ..mcp.server import MCPTicketServer
|
|
1379
|
-
|
|
1380
|
-
# Load configuration (respects project-specific config in cwd)
|
|
1381
|
-
config = load_config()
|
|
1382
|
-
|
|
1383
|
-
# Determine adapter type
|
|
1384
|
-
adapter_type = (
|
|
1385
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
1386
|
-
)
|
|
1387
|
-
|
|
1388
|
-
# Get adapter configuration
|
|
1389
|
-
adapters_config = config.get("adapters", {})
|
|
1390
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
1391
|
-
|
|
1392
|
-
# Override with command line options if provided
|
|
1393
|
-
if base_path and adapter_type == "aitrackdown":
|
|
1394
|
-
adapter_config["base_path"] = base_path
|
|
1395
|
-
|
|
1396
|
-
# Fallback to legacy config format
|
|
1397
|
-
if not adapter_config and "config" in config:
|
|
1398
|
-
adapter_config = config["config"]
|
|
1399
|
-
|
|
1400
|
-
# MCP server uses stdio for JSON-RPC, so we can't print to stdout
|
|
1401
|
-
# Only print to stderr to avoid interfering with the protocol
|
|
1402
|
-
import sys
|
|
1403
|
-
|
|
1404
|
-
if sys.stderr.isatty():
|
|
1405
|
-
# Only print if stderr is a terminal (not redirected)
|
|
1406
|
-
console.file = sys.stderr
|
|
1407
|
-
console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
|
|
1408
|
-
console.print(
|
|
1409
|
-
"[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
|
|
1410
|
-
)
|
|
1411
|
-
|
|
1412
|
-
# Create and run server
|
|
1413
|
-
try:
|
|
1414
|
-
server = MCPTicketServer(adapter_type, adapter_config)
|
|
1415
|
-
asyncio.run(server.run())
|
|
1416
|
-
except KeyboardInterrupt:
|
|
1417
|
-
# Also send this to stderr
|
|
1418
|
-
if sys.stderr.isatty():
|
|
1419
|
-
console.print("\n[yellow]Server stopped by user[/yellow]")
|
|
1420
|
-
if "server" in locals():
|
|
1421
|
-
asyncio.run(server.stop())
|
|
1422
|
-
except Exception as e:
|
|
1423
|
-
# Log error to stderr
|
|
1424
|
-
sys.stderr.write(f"MCP server error: {e}\n")
|
|
1425
|
-
sys.exit(1)
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
@mcp_app.command(name="claude")
|
|
1429
|
-
def mcp_claude(
|
|
1430
|
-
global_config: bool = typer.Option(
|
|
1431
|
-
False,
|
|
1432
|
-
"--global",
|
|
1433
|
-
"-g",
|
|
1434
|
-
help="Configure Claude Desktop instead of project-level",
|
|
1435
|
-
),
|
|
1436
|
-
force: bool = typer.Option(
|
|
1437
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
1438
|
-
),
|
|
1439
|
-
):
|
|
1440
|
-
"""Configure Claude Code to use mcp-ticketer MCP server.
|
|
1441
|
-
|
|
1442
|
-
Reads configuration from .mcp-ticketer/config.json and updates
|
|
1443
|
-
Claude Code's MCP settings accordingly.
|
|
1444
|
-
|
|
1445
|
-
By default, configures project-level (.mcp/config.json).
|
|
1446
|
-
Use --global to configure Claude Desktop instead.
|
|
1447
|
-
|
|
1448
|
-
Examples:
|
|
1449
|
-
# Configure for current project (default)
|
|
1450
|
-
mcp-ticketer mcp claude
|
|
1451
|
-
|
|
1452
|
-
# Configure Claude Desktop globally
|
|
1453
|
-
mcp-ticketer mcp claude --global
|
|
1454
|
-
|
|
1455
|
-
# Force overwrite existing configuration
|
|
1456
|
-
mcp-ticketer mcp claude --force
|
|
1457
|
-
|
|
1458
|
-
"""
|
|
1459
|
-
from ..cli.mcp_configure import configure_claude_mcp
|
|
1460
|
-
|
|
1461
|
-
try:
|
|
1462
|
-
configure_claude_mcp(global_config=global_config, force=force)
|
|
1463
|
-
except Exception as e:
|
|
1464
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
1465
|
-
raise typer.Exit(1)
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
@mcp_app.command(name="gemini")
|
|
1469
|
-
def mcp_gemini(
|
|
1470
|
-
scope: str = typer.Option(
|
|
1471
|
-
"project",
|
|
1472
|
-
"--scope",
|
|
1473
|
-
"-s",
|
|
1474
|
-
help="Configuration scope: 'project' (default) or 'user'",
|
|
580
|
+
@app.command("diagnose", hidden=True)
|
|
581
|
+
def diagnose_alias(
|
|
582
|
+
output_file: str | None = typer.Option(
|
|
583
|
+
None, "--output", "-o", help="Save full report to file"
|
|
1475
584
|
),
|
|
1476
|
-
|
|
1477
|
-
False, "--
|
|
585
|
+
json_output: bool = typer.Option(
|
|
586
|
+
False, "--json", help="Output report in JSON format"
|
|
1478
587
|
),
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
1483
|
-
Gemini CLI settings file with mcp-ticketer configuration.
|
|
1484
|
-
|
|
1485
|
-
By default, configures project-level (.gemini/settings.json).
|
|
1486
|
-
Use --scope user to configure user-level (~/.gemini/settings.json).
|
|
1487
|
-
|
|
1488
|
-
Examples:
|
|
1489
|
-
# Configure for current project (default)
|
|
1490
|
-
mcp-ticketer mcp gemini
|
|
1491
|
-
|
|
1492
|
-
# Configure at user level
|
|
1493
|
-
mcp-ticketer mcp gemini --scope user
|
|
1494
|
-
|
|
1495
|
-
# Force overwrite existing configuration
|
|
1496
|
-
mcp-ticketer mcp gemini --force
|
|
1497
|
-
|
|
1498
|
-
"""
|
|
1499
|
-
from ..cli.gemini_configure import configure_gemini_mcp
|
|
1500
|
-
|
|
1501
|
-
# Validate scope parameter
|
|
1502
|
-
if scope not in ["project", "user"]:
|
|
1503
|
-
console.print(
|
|
1504
|
-
f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
|
|
1505
|
-
)
|
|
1506
|
-
raise typer.Exit(1)
|
|
1507
|
-
|
|
1508
|
-
try:
|
|
1509
|
-
configure_gemini_mcp(scope=scope, force=force) # type: ignore
|
|
1510
|
-
except Exception as e:
|
|
1511
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
1512
|
-
raise typer.Exit(1)
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
@mcp_app.command(name="codex")
|
|
1516
|
-
def mcp_codex(
|
|
1517
|
-
force: bool = typer.Option(
|
|
1518
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
588
|
+
simple: bool = typer.Option(
|
|
589
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1519
590
|
),
|
|
1520
|
-
):
|
|
1521
|
-
"""
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
Codex CLI config.toml with mcp-ticketer configuration.
|
|
1525
|
-
|
|
1526
|
-
IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
|
|
1527
|
-
There is no project-level configuration support. After configuration,
|
|
1528
|
-
you must restart Codex CLI for changes to take effect.
|
|
1529
|
-
|
|
1530
|
-
Examples:
|
|
1531
|
-
# Configure Codex CLI globally
|
|
1532
|
-
mcp-ticketer mcp codex
|
|
1533
|
-
|
|
1534
|
-
# Force overwrite existing configuration
|
|
1535
|
-
mcp-ticketer mcp codex --force
|
|
1536
|
-
|
|
1537
|
-
"""
|
|
1538
|
-
from ..cli.codex_configure import configure_codex_mcp
|
|
1539
|
-
|
|
1540
|
-
try:
|
|
1541
|
-
configure_codex_mcp(force=force)
|
|
1542
|
-
except Exception as e:
|
|
1543
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
1544
|
-
raise typer.Exit(1)
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
@mcp_app.command(name="auggie")
|
|
1548
|
-
def mcp_auggie(
|
|
1549
|
-
force: bool = typer.Option(
|
|
1550
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
1551
|
-
),
|
|
1552
|
-
):
|
|
1553
|
-
"""Configure Auggie CLI to use mcp-ticketer MCP server.
|
|
591
|
+
) -> None:
|
|
592
|
+
"""Run comprehensive system diagnostics and health check (alias for doctor)."""
|
|
593
|
+
# Call the doctor_command function with the same parameters
|
|
594
|
+
doctor_command(output_file=output_file, json_output=json_output, simple=simple)
|
|
1554
595
|
|
|
1555
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
1556
|
-
Auggie CLI settings.json with mcp-ticketer configuration.
|
|
1557
596
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
597
|
+
@app.command("status")
|
|
598
|
+
def status_command() -> None:
|
|
599
|
+
"""Quick health check - shows system status summary (alias: health)."""
|
|
600
|
+
from .simple_health import simple_health_check
|
|
1561
601
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
602
|
+
result = simple_health_check()
|
|
603
|
+
if result != 0:
|
|
604
|
+
raise typer.Exit(result) from None
|
|
1565
605
|
|
|
1566
|
-
# Force overwrite existing configuration
|
|
1567
|
-
mcp-ticketer mcp auggie --force
|
|
1568
606
|
|
|
1569
|
-
|
|
1570
|
-
|
|
607
|
+
@app.command("health")
|
|
608
|
+
def health_alias() -> None:
|
|
609
|
+
"""Quick health check - shows system status summary (alias for status)."""
|
|
610
|
+
from .simple_health import simple_health_check
|
|
1571
611
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
1576
|
-
raise typer.Exit(1)
|
|
612
|
+
result = simple_health_check()
|
|
613
|
+
if result != 0:
|
|
614
|
+
raise typer.Exit(result) from None
|
|
1577
615
|
|
|
1578
616
|
|
|
1579
|
-
# Add
|
|
617
|
+
# Add command groups to main app (must be after all subcommands are defined)
|
|
1580
618
|
app.add_typer(mcp_app, name="mcp")
|
|
1581
619
|
|
|
1582
620
|
|
|
1583
|
-
def main():
|
|
1584
|
-
"""
|
|
621
|
+
def main() -> None:
|
|
622
|
+
"""Execute the main CLI application entry point."""
|
|
1585
623
|
app()
|
|
1586
624
|
|
|
1587
625
|
|