mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,477 @@
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 either:
53
+ - ~/.config/claude/mcp.json (new global location with flat structure)
54
+ - ~/.claude.json (legacy location with projects structure)
55
+
56
+ Returns:
57
+ DetectedPlatform if Claude Code config exists, None otherwise
58
+
59
+ """
60
+ # Check new global location first
61
+ new_config_path = Path.home() / ".config" / "claude" / "mcp.json"
62
+ old_config_path = Path.home() / ".claude.json"
63
+
64
+ # Priority: Use new location if it exists
65
+ config_path = new_config_path if new_config_path.exists() else old_config_path
66
+
67
+ # Check if config file exists
68
+ if not config_path.exists():
69
+ return None
70
+
71
+ # Validate it's valid JSON (but don't require specific structure)
72
+ try:
73
+ with config_path.open() as f:
74
+ content = f.read().strip()
75
+ if content: # Only validate if not empty
76
+ json.loads(content)
77
+
78
+ return DetectedPlatform(
79
+ name="claude-code",
80
+ display_name="Claude Code",
81
+ config_path=config_path,
82
+ is_installed=True,
83
+ scope="project",
84
+ executable_path=None, # Claude Code doesn't have a CLI
85
+ )
86
+ except (json.JSONDecodeError, OSError):
87
+ # Config exists but is corrupted - still consider it "detected"
88
+ # but mark as not installed/usable
89
+ return DetectedPlatform(
90
+ name="claude-code",
91
+ display_name="Claude Code",
92
+ config_path=config_path,
93
+ is_installed=False,
94
+ scope="project",
95
+ executable_path=None,
96
+ )
97
+
98
+ @staticmethod
99
+ def detect_claude_desktop() -> DetectedPlatform | None:
100
+ """Detect Claude Desktop installation.
101
+
102
+ Claude Desktop uses global configuration with platform-specific paths:
103
+ - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
104
+ - Linux: ~/.config/Claude/claude_desktop_config.json
105
+ - Windows: %APPDATA%/Claude/claude_desktop_config.json
106
+
107
+ Returns:
108
+ DetectedPlatform if Claude Desktop config exists, None otherwise
109
+
110
+ """
111
+ # Determine platform-specific config path
112
+ if sys.platform == "darwin": # macOS
113
+ config_path = (
114
+ Path.home()
115
+ / "Library"
116
+ / "Application Support"
117
+ / "Claude"
118
+ / "claude_desktop_config.json"
119
+ )
120
+ elif sys.platform == "win32": # Windows
121
+ appdata = os.environ.get("APPDATA", "")
122
+ if not appdata:
123
+ return None
124
+ config_path = Path(appdata) / "Claude" / "claude_desktop_config.json"
125
+ else: # Linux
126
+ config_path = (
127
+ Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
128
+ )
129
+
130
+ # Check if config file exists
131
+ if not config_path.exists():
132
+ return None
133
+
134
+ # Validate it's valid JSON
135
+ try:
136
+ with config_path.open() as f:
137
+ content = f.read().strip()
138
+ if content: # Only validate if not empty
139
+ json.loads(content)
140
+
141
+ return DetectedPlatform(
142
+ name="claude-desktop",
143
+ display_name="Claude Desktop",
144
+ config_path=config_path,
145
+ is_installed=True,
146
+ scope="global",
147
+ executable_path=None, # Claude Desktop is a GUI app
148
+ )
149
+ except (json.JSONDecodeError, OSError):
150
+ # Config exists but is corrupted
151
+ return DetectedPlatform(
152
+ name="claude-desktop",
153
+ display_name="Claude Desktop",
154
+ config_path=config_path,
155
+ is_installed=False,
156
+ scope="global",
157
+ executable_path=None,
158
+ )
159
+
160
+ @staticmethod
161
+ def detect_auggie() -> DetectedPlatform | None:
162
+ """Detect Auggie installation.
163
+
164
+ Auggie requires both:
165
+ 1. `auggie` CLI executable in PATH
166
+ 2. Configuration file at ~/.augment/settings.json
167
+
168
+ Returns:
169
+ DetectedPlatform if Auggie is installed, None otherwise
170
+
171
+ """
172
+ # Check for CLI executable
173
+ executable_path = shutil.which("auggie")
174
+ if not executable_path:
175
+ return None
176
+
177
+ # Check for config file
178
+ config_path = Path.home() / ".augment" / "settings.json"
179
+
180
+ # Auggie is installed if CLI exists, even without config
181
+ is_installed = True
182
+
183
+ # If config exists, validate it
184
+ if config_path.exists():
185
+ try:
186
+ with config_path.open() as f:
187
+ content = f.read().strip()
188
+ if content:
189
+ json.loads(content)
190
+ except (json.JSONDecodeError, OSError):
191
+ # Config exists but is corrupted
192
+ is_installed = False
193
+
194
+ return DetectedPlatform(
195
+ name="auggie",
196
+ display_name="Auggie",
197
+ config_path=config_path,
198
+ is_installed=is_installed,
199
+ scope="global",
200
+ executable_path=executable_path,
201
+ )
202
+
203
+ @staticmethod
204
+ def detect_cursor() -> DetectedPlatform | None:
205
+ """Detect Cursor code editor installation.
206
+
207
+ Cursor uses project-level MCP configuration stored in:
208
+ - ~/.cursor/mcp.json (global location with flat structure)
209
+
210
+ Returns:
211
+ DetectedPlatform if Cursor config exists, None otherwise
212
+
213
+ """
214
+ # Check global configuration location
215
+ config_path = Path.home() / ".cursor" / "mcp.json"
216
+
217
+ # Check if config file exists
218
+ if not config_path.exists():
219
+ return None
220
+
221
+ # Validate it's valid JSON
222
+ try:
223
+ with config_path.open() as f:
224
+ content = f.read().strip()
225
+ if content: # Only validate if not empty
226
+ json.loads(content)
227
+
228
+ return DetectedPlatform(
229
+ name="cursor",
230
+ display_name="Cursor",
231
+ config_path=config_path,
232
+ is_installed=True,
233
+ scope="project",
234
+ executable_path=None, # Cursor doesn't have a CLI
235
+ )
236
+ except (json.JSONDecodeError, OSError):
237
+ # Config exists but is corrupted - still consider it "detected"
238
+ # but mark as not installed/usable
239
+ return DetectedPlatform(
240
+ name="cursor",
241
+ display_name="Cursor",
242
+ config_path=config_path,
243
+ is_installed=False,
244
+ scope="project",
245
+ executable_path=None,
246
+ )
247
+
248
+ @staticmethod
249
+ def detect_codex() -> DetectedPlatform | None:
250
+ """Detect Codex installation.
251
+
252
+ Codex requires both:
253
+ 1. `codex` CLI executable in PATH
254
+ 2. Configuration file at ~/.codex/config.toml
255
+
256
+ Returns:
257
+ DetectedPlatform if Codex is installed, None otherwise
258
+
259
+ """
260
+ # Check for CLI executable
261
+ executable_path = shutil.which("codex")
262
+ if not executable_path:
263
+ return None
264
+
265
+ # Check for config file
266
+ config_path = Path.home() / ".codex" / "config.toml"
267
+
268
+ # Codex is installed if CLI exists, even without config
269
+ is_installed = True
270
+
271
+ # If config exists, validate it exists and is readable
272
+ if config_path.exists():
273
+ try:
274
+ with config_path.open() as f:
275
+ f.read() # Just check if readable
276
+ except OSError:
277
+ is_installed = False
278
+
279
+ return DetectedPlatform(
280
+ name="codex",
281
+ display_name="Codex",
282
+ config_path=config_path,
283
+ is_installed=is_installed,
284
+ scope="global",
285
+ executable_path=executable_path,
286
+ )
287
+
288
+ @staticmethod
289
+ def detect_gemini(project_path: Path | None = None) -> DetectedPlatform | None:
290
+ """Detect Gemini installation.
291
+
292
+ Gemini supports both project-level and global configurations:
293
+ 1. `gemini` CLI executable in PATH
294
+ 2. Configuration at .gemini/settings.json (project) or
295
+ ~/.gemini/settings.json (global)
296
+
297
+ Args:
298
+ project_path: Optional project directory to check for project-level config
299
+
300
+ Returns:
301
+ DetectedPlatform if Gemini is installed, None otherwise
302
+
303
+ """
304
+ # Check for CLI executable
305
+ executable_path = shutil.which("gemini")
306
+ if not executable_path:
307
+ return None
308
+
309
+ # Check for config files (project-level first, then global)
310
+ project_config = None
311
+ global_config = Path.home() / ".gemini" / "settings.json"
312
+
313
+ if project_path:
314
+ project_config = project_path / ".gemini" / "settings.json"
315
+
316
+ # Determine which config exists
317
+ config_path = None
318
+ scope = "global"
319
+
320
+ if project_config and project_config.exists():
321
+ config_path = project_config
322
+ scope = "project"
323
+ elif global_config.exists():
324
+ config_path = global_config
325
+ scope = "global"
326
+ else:
327
+ # No config found, use global path as default
328
+ config_path = global_config
329
+
330
+ # Gemini is installed if CLI exists, even without config
331
+ is_installed = True
332
+
333
+ # If config exists, validate it
334
+ if config_path.exists():
335
+ try:
336
+ with config_path.open() as f:
337
+ content = f.read().strip()
338
+ if content:
339
+ json.loads(content)
340
+ except (json.JSONDecodeError, OSError):
341
+ # Config exists but is corrupted
342
+ is_installed = False
343
+
344
+ # Check if both configs exist
345
+ if project_config and project_config.exists() and global_config.exists():
346
+ scope = "both"
347
+
348
+ return DetectedPlatform(
349
+ name="gemini",
350
+ display_name="Gemini",
351
+ config_path=config_path,
352
+ is_installed=is_installed,
353
+ scope=scope,
354
+ executable_path=executable_path,
355
+ )
356
+
357
+ @classmethod
358
+ def detect_all(
359
+ cls, project_path: Path | None = None, exclude_desktop: bool = False
360
+ ) -> list[DetectedPlatform]:
361
+ """Detect all installed AI client platforms.
362
+
363
+ Args:
364
+ project_path: Optional project directory for project-level detection
365
+ exclude_desktop: If True, exclude desktop AI assistants (Claude Desktop)
366
+
367
+ Returns:
368
+ List of detected platforms (empty if none found)
369
+
370
+ Examples:
371
+ >>> detector = PlatformDetector()
372
+ >>> platforms = detector.detect_all()
373
+ >>> for platform in platforms:
374
+ ... print(f"{platform.display_name}: {platform.is_installed}")
375
+ Claude Code: True
376
+ Claude Desktop: False
377
+
378
+ >>> # With project path for Gemini detection
379
+ >>> platforms = detector.detect_all(Path("/home/user/project"))
380
+ >>> gemini = next(p for p in platforms if p.name == "gemini")
381
+ >>> print(gemini.scope) # "project" or "global" or "both"
382
+
383
+ >>> # Exclude desktop AI assistants (code editors only)
384
+ >>> platforms = detector.detect_all(exclude_desktop=True)
385
+ >>> # Returns: Claude Code, Cursor, Auggie, Codex, Gemini (NOT Claude Desktop)
386
+
387
+ """
388
+ detected = []
389
+
390
+ # Detect Claude Code (project-level code editor)
391
+ claude_code = cls.detect_claude_code()
392
+ if claude_code:
393
+ detected.append(claude_code)
394
+
395
+ # Detect Claude Desktop (desktop AI assistant - optional)
396
+ if not exclude_desktop:
397
+ claude_desktop = cls.detect_claude_desktop()
398
+ if claude_desktop:
399
+ detected.append(claude_desktop)
400
+
401
+ # Detect Cursor (code editor)
402
+ cursor = cls.detect_cursor()
403
+ if cursor:
404
+ detected.append(cursor)
405
+
406
+ # Detect Auggie (code assistant)
407
+ auggie = cls.detect_auggie()
408
+ if auggie:
409
+ detected.append(auggie)
410
+
411
+ # Detect Codex (code assistant)
412
+ codex = cls.detect_codex()
413
+ if codex:
414
+ detected.append(codex)
415
+
416
+ # Detect Gemini (code assistant with project path support)
417
+ gemini = cls.detect_gemini(project_path=project_path)
418
+ if gemini:
419
+ detected.append(gemini)
420
+
421
+ return detected
422
+
423
+
424
+ def get_platform_by_name(
425
+ platform_name: str, project_path: Path | None = None
426
+ ) -> DetectedPlatform | None:
427
+ """Get detection result for a specific platform by name.
428
+
429
+ Args:
430
+ platform_name: Platform identifier (e.g., "claude-code", "auggie")
431
+ project_path: Optional project directory for project-level detection
432
+
433
+ Returns:
434
+ DetectedPlatform if found, None if platform doesn't exist or isn't installed
435
+
436
+ Examples:
437
+ >>> platform = get_platform_by_name("claude-code")
438
+ >>> if platform and platform.is_installed:
439
+ ... print(f"Config at: {platform.config_path}")
440
+
441
+ """
442
+ detector = PlatformDetector()
443
+
444
+ # Map platform names to detection methods
445
+ detection_map = {
446
+ "claude-code": detector.detect_claude_code,
447
+ "claude-desktop": detector.detect_claude_desktop,
448
+ "cursor": detector.detect_cursor,
449
+ "auggie": detector.detect_auggie,
450
+ "codex": detector.detect_codex,
451
+ "gemini": lambda: detector.detect_gemini(project_path),
452
+ }
453
+
454
+ detect_func = detection_map.get(platform_name)
455
+ if not detect_func:
456
+ return None
457
+
458
+ return detect_func()
459
+
460
+
461
+ def is_platform_installed(platform_name: str, project_path: Path | None = None) -> bool:
462
+ """Check if a specific platform is installed and usable.
463
+
464
+ Args:
465
+ platform_name: Platform identifier (e.g., "claude-code", "auggie")
466
+ project_path: Optional project directory for project-level detection
467
+
468
+ Returns:
469
+ True if platform is installed and has valid configuration
470
+
471
+ Examples:
472
+ >>> if is_platform_installed("claude-code"):
473
+ ... print("Claude Code is installed and configured")
474
+
475
+ """
476
+ platform = get_platform_by_name(platform_name, project_path)
477
+ return platform is not None and platform.is_installed