mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +263 -14
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +326 -109
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +271 -25
- mcp_ticketer/adapters/linear/adapter.py +693 -39
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/adapters/linear/queries.py +9 -7
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +104 -15
- mcp_ticketer/cli/codex_configure.py +188 -32
- mcp_ticketer/cli/configure.py +37 -48
- mcp_ticketer/cli/diagnostics.py +20 -18
- mcp_ticketer/cli/discover.py +292 -26
- mcp_ticketer/cli/gemini_configure.py +107 -26
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +105 -22
- mcp_ticketer/cli/main.py +1830 -435
- mcp_ticketer/cli/mcp_configure.py +296 -89
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +773 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +67 -62
- mcp_ticketer/core/__init__.py +14 -1
- mcp_ticketer/core/adapter.py +84 -15
- mcp_ticketer/core/config.py +44 -39
- mcp_ticketer/core/env_discovery.py +42 -12
- mcp_ticketer/core/env_loader.py +15 -14
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +11 -11
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +57 -35
- mcp_ticketer/core/registry.py +3 -3
- 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/{dto.py → server/dto.py} +32 -32
- mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
- mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
- mcp_ticketer/mcp/server/server_sdk.py +93 -0
- mcp_ticketer/mcp/server/tools/__init__.py +47 -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 +90 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +5 -4
- mcp_ticketer/queue/manager.py +15 -51
- mcp_ticketer/queue/queue.py +19 -19
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +14 -14
- mcp_ticketer/queue/worker.py +16 -14
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
- /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""AI client platform auto-detection for mcp-ticketer.
|
|
2
|
+
|
|
3
|
+
This module provides automatic detection of AI client frameworks that
|
|
4
|
+
support MCP servers. It detects installation status, configuration paths,
|
|
5
|
+
and scope (project/global) for each platform.
|
|
6
|
+
|
|
7
|
+
Supported platforms:
|
|
8
|
+
- Claude Code (project-level, ~/.claude.json)
|
|
9
|
+
- Claude Desktop (global, platform-specific paths)
|
|
10
|
+
- Auggie (CLI + ~/.augment/settings.json)
|
|
11
|
+
- Codex (CLI + ~/.codex/config.toml)
|
|
12
|
+
- Gemini (CLI + .gemini/settings.json or ~/.gemini/settings.json)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DetectedPlatform:
|
|
25
|
+
"""Represents a detected AI client platform.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
name: Platform identifier (e.g., "claude-code")
|
|
29
|
+
display_name: Human-readable name (e.g., "Claude Code")
|
|
30
|
+
config_path: Path to platform configuration file
|
|
31
|
+
is_installed: Whether platform is installed and usable
|
|
32
|
+
scope: Configuration scope - "project", "global", or "both"
|
|
33
|
+
executable_path: Path to CLI executable (if applicable)
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
display_name: str
|
|
39
|
+
config_path: Path
|
|
40
|
+
is_installed: bool
|
|
41
|
+
scope: str
|
|
42
|
+
executable_path: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PlatformDetector:
|
|
46
|
+
"""Detects installed AI client platforms that support MCP servers."""
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def detect_claude_code() -> DetectedPlatform | None:
|
|
50
|
+
"""Detect Claude Code installation.
|
|
51
|
+
|
|
52
|
+
Claude Code uses project-level configuration stored in ~/.claude.json
|
|
53
|
+
with a projects structure that maps project paths to MCP server configs.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
DetectedPlatform if Claude Code config exists, None otherwise
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
config_path = Path.home() / ".claude.json"
|
|
60
|
+
|
|
61
|
+
# Check if config file exists
|
|
62
|
+
if not config_path.exists():
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# Validate it's valid JSON (but don't require specific structure)
|
|
66
|
+
try:
|
|
67
|
+
with config_path.open() as f:
|
|
68
|
+
content = f.read().strip()
|
|
69
|
+
if content: # Only validate if not empty
|
|
70
|
+
json.loads(content)
|
|
71
|
+
|
|
72
|
+
return DetectedPlatform(
|
|
73
|
+
name="claude-code",
|
|
74
|
+
display_name="Claude Code",
|
|
75
|
+
config_path=config_path,
|
|
76
|
+
is_installed=True,
|
|
77
|
+
scope="project",
|
|
78
|
+
executable_path=None, # Claude Code doesn't have a CLI
|
|
79
|
+
)
|
|
80
|
+
except (json.JSONDecodeError, OSError):
|
|
81
|
+
# Config exists but is corrupted - still consider it "detected"
|
|
82
|
+
# but mark as not installed/usable
|
|
83
|
+
return DetectedPlatform(
|
|
84
|
+
name="claude-code",
|
|
85
|
+
display_name="Claude Code",
|
|
86
|
+
config_path=config_path,
|
|
87
|
+
is_installed=False,
|
|
88
|
+
scope="project",
|
|
89
|
+
executable_path=None,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def detect_claude_desktop() -> DetectedPlatform | None:
|
|
94
|
+
"""Detect Claude Desktop installation.
|
|
95
|
+
|
|
96
|
+
Claude Desktop uses global configuration with platform-specific paths:
|
|
97
|
+
- macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
98
|
+
- Linux: ~/.config/Claude/claude_desktop_config.json
|
|
99
|
+
- Windows: %APPDATA%/Claude/claude_desktop_config.json
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
DetectedPlatform if Claude Desktop config exists, None otherwise
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
# Determine platform-specific config path
|
|
106
|
+
if sys.platform == "darwin": # macOS
|
|
107
|
+
config_path = (
|
|
108
|
+
Path.home()
|
|
109
|
+
/ "Library"
|
|
110
|
+
/ "Application Support"
|
|
111
|
+
/ "Claude"
|
|
112
|
+
/ "claude_desktop_config.json"
|
|
113
|
+
)
|
|
114
|
+
elif sys.platform == "win32": # Windows
|
|
115
|
+
appdata = os.environ.get("APPDATA", "")
|
|
116
|
+
if not appdata:
|
|
117
|
+
return None
|
|
118
|
+
config_path = Path(appdata) / "Claude" / "claude_desktop_config.json"
|
|
119
|
+
else: # Linux
|
|
120
|
+
config_path = (
|
|
121
|
+
Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Check if config file exists
|
|
125
|
+
if not config_path.exists():
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# Validate it's valid JSON
|
|
129
|
+
try:
|
|
130
|
+
with config_path.open() as f:
|
|
131
|
+
content = f.read().strip()
|
|
132
|
+
if content: # Only validate if not empty
|
|
133
|
+
json.loads(content)
|
|
134
|
+
|
|
135
|
+
return DetectedPlatform(
|
|
136
|
+
name="claude-desktop",
|
|
137
|
+
display_name="Claude Desktop",
|
|
138
|
+
config_path=config_path,
|
|
139
|
+
is_installed=True,
|
|
140
|
+
scope="global",
|
|
141
|
+
executable_path=None, # Claude Desktop is a GUI app
|
|
142
|
+
)
|
|
143
|
+
except (json.JSONDecodeError, OSError):
|
|
144
|
+
# Config exists but is corrupted
|
|
145
|
+
return DetectedPlatform(
|
|
146
|
+
name="claude-desktop",
|
|
147
|
+
display_name="Claude Desktop",
|
|
148
|
+
config_path=config_path,
|
|
149
|
+
is_installed=False,
|
|
150
|
+
scope="global",
|
|
151
|
+
executable_path=None,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def detect_auggie() -> DetectedPlatform | None:
|
|
156
|
+
"""Detect Auggie installation.
|
|
157
|
+
|
|
158
|
+
Auggie requires both:
|
|
159
|
+
1. `auggie` CLI executable in PATH
|
|
160
|
+
2. Configuration file at ~/.augment/settings.json
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
DetectedPlatform if Auggie is installed, None otherwise
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
# Check for CLI executable
|
|
167
|
+
executable_path = shutil.which("auggie")
|
|
168
|
+
if not executable_path:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Check for config file
|
|
172
|
+
config_path = Path.home() / ".augment" / "settings.json"
|
|
173
|
+
|
|
174
|
+
# Auggie is installed if CLI exists, even without config
|
|
175
|
+
is_installed = True
|
|
176
|
+
|
|
177
|
+
# If config exists, validate it
|
|
178
|
+
if config_path.exists():
|
|
179
|
+
try:
|
|
180
|
+
with config_path.open() as f:
|
|
181
|
+
content = f.read().strip()
|
|
182
|
+
if content:
|
|
183
|
+
json.loads(content)
|
|
184
|
+
except (json.JSONDecodeError, OSError):
|
|
185
|
+
# Config exists but is corrupted
|
|
186
|
+
is_installed = False
|
|
187
|
+
|
|
188
|
+
return DetectedPlatform(
|
|
189
|
+
name="auggie",
|
|
190
|
+
display_name="Auggie",
|
|
191
|
+
config_path=config_path,
|
|
192
|
+
is_installed=is_installed,
|
|
193
|
+
scope="global",
|
|
194
|
+
executable_path=executable_path,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def detect_codex() -> DetectedPlatform | None:
|
|
199
|
+
"""Detect Codex installation.
|
|
200
|
+
|
|
201
|
+
Codex requires both:
|
|
202
|
+
1. `codex` CLI executable in PATH
|
|
203
|
+
2. Configuration file at ~/.codex/config.toml
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
DetectedPlatform if Codex is installed, None otherwise
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
# Check for CLI executable
|
|
210
|
+
executable_path = shutil.which("codex")
|
|
211
|
+
if not executable_path:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
# Check for config file
|
|
215
|
+
config_path = Path.home() / ".codex" / "config.toml"
|
|
216
|
+
|
|
217
|
+
# Codex is installed if CLI exists, even without config
|
|
218
|
+
is_installed = True
|
|
219
|
+
|
|
220
|
+
# If config exists, validate it exists and is readable
|
|
221
|
+
if config_path.exists():
|
|
222
|
+
try:
|
|
223
|
+
with config_path.open() as f:
|
|
224
|
+
f.read() # Just check if readable
|
|
225
|
+
except OSError:
|
|
226
|
+
is_installed = False
|
|
227
|
+
|
|
228
|
+
return DetectedPlatform(
|
|
229
|
+
name="codex",
|
|
230
|
+
display_name="Codex",
|
|
231
|
+
config_path=config_path,
|
|
232
|
+
is_installed=is_installed,
|
|
233
|
+
scope="global",
|
|
234
|
+
executable_path=executable_path,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def detect_gemini(project_path: Path | None = None) -> DetectedPlatform | None:
|
|
239
|
+
"""Detect Gemini installation.
|
|
240
|
+
|
|
241
|
+
Gemini supports both project-level and global configurations:
|
|
242
|
+
1. `gemini` CLI executable in PATH
|
|
243
|
+
2. Configuration at .gemini/settings.json (project) or
|
|
244
|
+
~/.gemini/settings.json (global)
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
project_path: Optional project directory to check for project-level config
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
DetectedPlatform if Gemini is installed, None otherwise
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
# Check for CLI executable
|
|
254
|
+
executable_path = shutil.which("gemini")
|
|
255
|
+
if not executable_path:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
# Check for config files (project-level first, then global)
|
|
259
|
+
project_config = None
|
|
260
|
+
global_config = Path.home() / ".gemini" / "settings.json"
|
|
261
|
+
|
|
262
|
+
if project_path:
|
|
263
|
+
project_config = project_path / ".gemini" / "settings.json"
|
|
264
|
+
|
|
265
|
+
# Determine which config exists
|
|
266
|
+
config_path = None
|
|
267
|
+
scope = "global"
|
|
268
|
+
|
|
269
|
+
if project_config and project_config.exists():
|
|
270
|
+
config_path = project_config
|
|
271
|
+
scope = "project"
|
|
272
|
+
elif global_config.exists():
|
|
273
|
+
config_path = global_config
|
|
274
|
+
scope = "global"
|
|
275
|
+
else:
|
|
276
|
+
# No config found, use global path as default
|
|
277
|
+
config_path = global_config
|
|
278
|
+
|
|
279
|
+
# Gemini is installed if CLI exists, even without config
|
|
280
|
+
is_installed = True
|
|
281
|
+
|
|
282
|
+
# If config exists, validate it
|
|
283
|
+
if config_path.exists():
|
|
284
|
+
try:
|
|
285
|
+
with config_path.open() as f:
|
|
286
|
+
content = f.read().strip()
|
|
287
|
+
if content:
|
|
288
|
+
json.loads(content)
|
|
289
|
+
except (json.JSONDecodeError, OSError):
|
|
290
|
+
# Config exists but is corrupted
|
|
291
|
+
is_installed = False
|
|
292
|
+
|
|
293
|
+
# Check if both configs exist
|
|
294
|
+
if project_config and project_config.exists() and global_config.exists():
|
|
295
|
+
scope = "both"
|
|
296
|
+
|
|
297
|
+
return DetectedPlatform(
|
|
298
|
+
name="gemini",
|
|
299
|
+
display_name="Gemini",
|
|
300
|
+
config_path=config_path,
|
|
301
|
+
is_installed=is_installed,
|
|
302
|
+
scope=scope,
|
|
303
|
+
executable_path=executable_path,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def detect_all(cls, project_path: Path | None = None) -> list[DetectedPlatform]:
|
|
308
|
+
"""Detect all installed AI client platforms.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
project_path: Optional project directory for project-level detection
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
List of detected platforms (empty if none found)
|
|
315
|
+
|
|
316
|
+
Examples:
|
|
317
|
+
>>> detector = PlatformDetector()
|
|
318
|
+
>>> platforms = detector.detect_all()
|
|
319
|
+
>>> for platform in platforms:
|
|
320
|
+
... print(f"{platform.display_name}: {platform.is_installed}")
|
|
321
|
+
Claude Code: True
|
|
322
|
+
Claude Desktop: False
|
|
323
|
+
|
|
324
|
+
>>> # With project path for Gemini detection
|
|
325
|
+
>>> platforms = detector.detect_all(Path("/home/user/project"))
|
|
326
|
+
>>> gemini = next(p for p in platforms if p.name == "gemini")
|
|
327
|
+
>>> print(gemini.scope) # "project" or "global" or "both"
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
detected = []
|
|
331
|
+
|
|
332
|
+
# Detect Claude Code
|
|
333
|
+
claude_code = cls.detect_claude_code()
|
|
334
|
+
if claude_code:
|
|
335
|
+
detected.append(claude_code)
|
|
336
|
+
|
|
337
|
+
# Detect Claude Desktop
|
|
338
|
+
claude_desktop = cls.detect_claude_desktop()
|
|
339
|
+
if claude_desktop:
|
|
340
|
+
detected.append(claude_desktop)
|
|
341
|
+
|
|
342
|
+
# Detect Auggie
|
|
343
|
+
auggie = cls.detect_auggie()
|
|
344
|
+
if auggie:
|
|
345
|
+
detected.append(auggie)
|
|
346
|
+
|
|
347
|
+
# Detect Codex
|
|
348
|
+
codex = cls.detect_codex()
|
|
349
|
+
if codex:
|
|
350
|
+
detected.append(codex)
|
|
351
|
+
|
|
352
|
+
# Detect Gemini (with project path support)
|
|
353
|
+
gemini = cls.detect_gemini(project_path=project_path)
|
|
354
|
+
if gemini:
|
|
355
|
+
detected.append(gemini)
|
|
356
|
+
|
|
357
|
+
return detected
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def get_platform_by_name(
|
|
361
|
+
platform_name: str, project_path: Path | None = None
|
|
362
|
+
) -> DetectedPlatform | None:
|
|
363
|
+
"""Get detection result for a specific platform by name.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
platform_name: Platform identifier (e.g., "claude-code", "auggie")
|
|
367
|
+
project_path: Optional project directory for project-level detection
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
DetectedPlatform if found, None if platform doesn't exist or isn't installed
|
|
371
|
+
|
|
372
|
+
Examples:
|
|
373
|
+
>>> platform = get_platform_by_name("claude-code")
|
|
374
|
+
>>> if platform and platform.is_installed:
|
|
375
|
+
... print(f"Config at: {platform.config_path}")
|
|
376
|
+
|
|
377
|
+
"""
|
|
378
|
+
detector = PlatformDetector()
|
|
379
|
+
|
|
380
|
+
# Map platform names to detection methods
|
|
381
|
+
detection_map = {
|
|
382
|
+
"claude-code": detector.detect_claude_code,
|
|
383
|
+
"claude-desktop": detector.detect_claude_desktop,
|
|
384
|
+
"auggie": detector.detect_auggie,
|
|
385
|
+
"codex": detector.detect_codex,
|
|
386
|
+
"gemini": lambda: detector.detect_gemini(project_path),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
detect_func = detection_map.get(platform_name)
|
|
390
|
+
if not detect_func:
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
return detect_func()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def is_platform_installed(platform_name: str, project_path: Path | None = None) -> bool:
|
|
397
|
+
"""Check if a specific platform is installed and usable.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
platform_name: Platform identifier (e.g., "claude-code", "auggie")
|
|
401
|
+
project_path: Optional project directory for project-level detection
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if platform is installed and has valid configuration
|
|
405
|
+
|
|
406
|
+
Examples:
|
|
407
|
+
>>> if is_platform_installed("claude-code"):
|
|
408
|
+
... print("Claude Code is installed and configured")
|
|
409
|
+
|
|
410
|
+
"""
|
|
411
|
+
platform = get_platform_by_name(platform_name, project_path)
|
|
412
|
+
return platform is not None and platform.is_installed
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Reliable Python executable detection for mcp-ticketer.
|
|
2
|
+
|
|
3
|
+
This module provides reliable detection of the Python executable for mcp-ticketer
|
|
4
|
+
across different installation methods (pipx, pip, uv, direct venv).
|
|
5
|
+
|
|
6
|
+
The module follows the proven pattern from mcp-vector-search:
|
|
7
|
+
- Detect venv Python path reliably
|
|
8
|
+
- Use `python -m mcp_ticketer.mcp.server` instead of binary paths
|
|
9
|
+
- Support multiple installation methods transparently
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_mcp_ticketer_python(project_path: Path | None = None) -> str:
|
|
19
|
+
"""Get the correct Python executable for mcp-ticketer MCP server.
|
|
20
|
+
|
|
21
|
+
This function follows the mcp-vector-search pattern of using project-specific
|
|
22
|
+
venv Python for proper project isolation and dependency management.
|
|
23
|
+
|
|
24
|
+
Detection priority:
|
|
25
|
+
1. Project-local venv (.venv/bin/python) if project_path provided
|
|
26
|
+
2. Current Python executable if in pipx venv
|
|
27
|
+
3. Python from mcp-ticketer binary shebang
|
|
28
|
+
4. Current Python executable (fallback)
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
project_path: Optional project directory path to check for local venv
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Path to Python executable
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> # With project venv
|
|
38
|
+
>>> python_path = get_mcp_ticketer_python(Path("/home/user/my-project"))
|
|
39
|
+
>>> # Returns: "/home/user/my-project/.venv/bin/python"
|
|
40
|
+
|
|
41
|
+
>>> # Without project path (fallback to pipx)
|
|
42
|
+
>>> python_path = get_mcp_ticketer_python()
|
|
43
|
+
>>> # Returns: "/Users/user/.local/pipx/venvs/mcp-ticketer/bin/python"
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
# Priority 1: Check for project-local venv
|
|
47
|
+
if project_path:
|
|
48
|
+
project_venv_python = project_path / ".venv" / "bin" / "python"
|
|
49
|
+
if project_venv_python.exists():
|
|
50
|
+
return str(project_venv_python)
|
|
51
|
+
|
|
52
|
+
current_executable = sys.executable
|
|
53
|
+
|
|
54
|
+
# Priority 2: Check if we're in a pipx venv
|
|
55
|
+
if "/pipx/venvs/" in current_executable:
|
|
56
|
+
return current_executable
|
|
57
|
+
|
|
58
|
+
# Priority 3: Check mcp-ticketer binary shebang
|
|
59
|
+
mcp_ticketer_path = shutil.which("mcp-ticketer")
|
|
60
|
+
if mcp_ticketer_path:
|
|
61
|
+
try:
|
|
62
|
+
with open(mcp_ticketer_path) as f:
|
|
63
|
+
first_line = f.readline().strip()
|
|
64
|
+
if first_line.startswith("#!") and "python" in first_line:
|
|
65
|
+
python_path = first_line[2:].strip()
|
|
66
|
+
if os.path.exists(python_path):
|
|
67
|
+
return python_path
|
|
68
|
+
except OSError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Priority 4: Fallback to current Python
|
|
72
|
+
return current_executable
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_mcp_server_command(project_path: str | None = None) -> tuple[str, list[str]]:
|
|
76
|
+
"""Get the complete command to run the MCP server.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
project_path: Optional project path to pass as argument and check for venv
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Tuple of (python_executable, args_list)
|
|
83
|
+
Example: ("/path/to/python", ["-m", "mcp_ticketer.mcp.server", "/project/path"])
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
>>> python, args = get_mcp_server_command("/home/user/project")
|
|
87
|
+
>>> # python: "/home/user/project/.venv/bin/python" (if .venv exists)
|
|
88
|
+
>>> # args: ["-m", "mcp_ticketer.mcp.server", "/home/user/project"]
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
# Convert project_path to Path object for venv detection
|
|
92
|
+
project_path_obj = Path(project_path) if project_path else None
|
|
93
|
+
python_path = get_mcp_ticketer_python(project_path=project_path_obj)
|
|
94
|
+
args = ["-m", "mcp_ticketer.mcp.server"]
|
|
95
|
+
|
|
96
|
+
if project_path:
|
|
97
|
+
args.append(str(project_path))
|
|
98
|
+
|
|
99
|
+
return python_path, args
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_python_executable(python_path: str) -> bool:
|
|
103
|
+
"""Validate that a Python executable can import mcp_ticketer.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
python_path: Path to Python executable to validate
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if Python can import mcp_ticketer, False otherwise
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
>>> is_valid = validate_python_executable("/usr/bin/python3")
|
|
113
|
+
>>> # Returns: False (system Python doesn't have mcp_ticketer)
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
import subprocess
|
|
118
|
+
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
[python_path, "-c", "import mcp_ticketer.mcp.server"],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
timeout=5,
|
|
123
|
+
)
|
|
124
|
+
return result.returncode == 0
|
|
125
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
126
|
+
return False
|
|
@@ -16,7 +16,7 @@ console = Console()
|
|
|
16
16
|
def list_queue(
|
|
17
17
|
status: QueueStatus = typer.Option(None, "--status", "-s", help="Filter by status"),
|
|
18
18
|
limit: int = typer.Option(25, "--limit", "-l", help="Maximum items to show"),
|
|
19
|
-
):
|
|
19
|
+
) -> None:
|
|
20
20
|
"""List queue items."""
|
|
21
21
|
queue = Queue()
|
|
22
22
|
items = queue.list_items(status=status, limit=limit)
|
|
@@ -69,20 +69,20 @@ def list_queue(
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
@app.command("retry")
|
|
72
|
-
def retry_item(queue_id: str = typer.Argument(..., help="Queue ID to retry")):
|
|
72
|
+
def retry_item(queue_id: str = typer.Argument(..., help="Queue ID to retry")) -> None:
|
|
73
73
|
"""Retry a failed queue item."""
|
|
74
74
|
queue = Queue()
|
|
75
75
|
item = queue.get_item(queue_id)
|
|
76
76
|
|
|
77
77
|
if not item:
|
|
78
78
|
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
79
|
-
raise typer.Exit(1)
|
|
79
|
+
raise typer.Exit(1) from None
|
|
80
80
|
|
|
81
81
|
if item.status != QueueStatus.FAILED:
|
|
82
82
|
console.print(
|
|
83
83
|
f"[yellow]Item {queue_id} is not failed (status: {item.status})[/yellow]"
|
|
84
84
|
)
|
|
85
|
-
raise typer.Exit(1)
|
|
85
|
+
raise typer.Exit(1) from None
|
|
86
86
|
|
|
87
87
|
# Reset to pending
|
|
88
88
|
queue.update_status(queue_id, QueueStatus.PENDING, error_message=None)
|
|
@@ -103,7 +103,7 @@ def clear_queue(
|
|
|
103
103
|
7, "--days", "-d", help="Clear items older than this many days"
|
|
104
104
|
),
|
|
105
105
|
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
106
|
-
):
|
|
106
|
+
) -> None:
|
|
107
107
|
"""Clear old queue items."""
|
|
108
108
|
queue = Queue()
|
|
109
109
|
|
|
@@ -115,7 +115,7 @@ def clear_queue(
|
|
|
115
115
|
|
|
116
116
|
if not typer.confirm(msg):
|
|
117
117
|
console.print("[yellow]Cancelled[/yellow]")
|
|
118
|
-
raise typer.Exit(0)
|
|
118
|
+
raise typer.Exit(0) from None
|
|
119
119
|
|
|
120
120
|
queue.cleanup_old(days=days)
|
|
121
121
|
console.print("[green]✓[/green] Cleared old queue items")
|
|
@@ -126,7 +126,7 @@ worker_app = typer.Typer(name="worker", help="Worker management commands")
|
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
@worker_app.command("start")
|
|
129
|
-
def start_worker():
|
|
129
|
+
def start_worker() -> None:
|
|
130
130
|
"""Start the background worker."""
|
|
131
131
|
manager = WorkerManager()
|
|
132
132
|
|
|
@@ -142,11 +142,11 @@ def start_worker():
|
|
|
142
142
|
console.print(f"PID: {status.get('pid')}")
|
|
143
143
|
else:
|
|
144
144
|
console.print("[red]✗[/red] Failed to start worker")
|
|
145
|
-
raise typer.Exit(1)
|
|
145
|
+
raise typer.Exit(1) from None
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
@worker_app.command("stop")
|
|
149
|
-
def stop_worker():
|
|
149
|
+
def stop_worker() -> None:
|
|
150
150
|
"""Stop the background worker."""
|
|
151
151
|
manager = WorkerManager()
|
|
152
152
|
|
|
@@ -158,11 +158,11 @@ def stop_worker():
|
|
|
158
158
|
console.print("[green]✓[/green] Worker stopped successfully")
|
|
159
159
|
else:
|
|
160
160
|
console.print("[red]✗[/red] Failed to stop worker")
|
|
161
|
-
raise typer.Exit(1)
|
|
161
|
+
raise typer.Exit(1) from None
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
@worker_app.command("restart")
|
|
165
|
-
def restart_worker():
|
|
165
|
+
def restart_worker() -> None:
|
|
166
166
|
"""Restart the background worker."""
|
|
167
167
|
manager = WorkerManager()
|
|
168
168
|
|
|
@@ -172,11 +172,11 @@ def restart_worker():
|
|
|
172
172
|
console.print(f"PID: {status.get('pid')}")
|
|
173
173
|
else:
|
|
174
174
|
console.print("[red]✗[/red] Failed to restart worker")
|
|
175
|
-
raise typer.Exit(1)
|
|
175
|
+
raise typer.Exit(1) from None
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
@worker_app.command("status")
|
|
179
|
-
def worker_status():
|
|
179
|
+
def worker_status() -> None:
|
|
180
180
|
"""Check worker status."""
|
|
181
181
|
manager = WorkerManager()
|
|
182
182
|
status = manager.get_status()
|
|
@@ -212,7 +212,7 @@ def worker_status():
|
|
|
212
212
|
def worker_logs(
|
|
213
213
|
lines: int = typer.Option(50, "--lines", "-n", help="Number of lines to show"),
|
|
214
214
|
follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
|
|
215
|
-
):
|
|
215
|
+
) -> None:
|
|
216
216
|
"""View worker logs."""
|
|
217
217
|
import time
|
|
218
218
|
from pathlib import Path
|
|
@@ -221,7 +221,7 @@ def worker_logs(
|
|
|
221
221
|
|
|
222
222
|
if not log_file.exists():
|
|
223
223
|
console.print("[yellow]No log file found[/yellow]")
|
|
224
|
-
raise typer.Exit(1)
|
|
224
|
+
raise typer.Exit(1) from None
|
|
225
225
|
|
|
226
226
|
if follow:
|
|
227
227
|
# Follow mode - like tail -f
|
|
@@ -150,7 +150,7 @@ def simple_health_check() -> int:
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
def simple_diagnose() -> dict[str, Any]:
|
|
153
|
-
"""
|
|
153
|
+
"""Perform simple diagnosis without full config system."""
|
|
154
154
|
console.print("\n🔍 [bold blue]MCP Ticketer Simple Diagnosis[/bold blue]")
|
|
155
155
|
console.print("=" * 60)
|
|
156
156
|
|