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.

Files changed (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {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
- """Simple diagnosis that works without full config system."""
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