mcp-ticketer 0.12.0__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 (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,415 @@
1
+ """MCP server management commands for mcp-ticketer."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+ # Create MCP configuration command group
13
+ mcp_app = typer.Typer(
14
+ name="mcp",
15
+ help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
16
+ add_completion=False,
17
+ invoke_without_command=True,
18
+ )
19
+
20
+
21
+ @mcp_app.callback()
22
+ def mcp_callback(
23
+ ctx: typer.Context,
24
+ project_path: str | None = typer.Option(
25
+ None, "--path", "-p", help="Project directory path (default: current directory)"
26
+ ),
27
+ ) -> None:
28
+ """MCP command group - runs MCP server if no subcommand provided.
29
+
30
+ Examples:
31
+ --------
32
+ mcp-ticketer mcp # Start server in current directory
33
+ mcp-ticketer mcp --path /dir # Start server in specific directory
34
+ mcp-ticketer mcp -p /dir # Start server (short form)
35
+ mcp-ticketer mcp status # Check MCP status
36
+ mcp-ticketer mcp serve # Explicitly start server
37
+
38
+ """
39
+ if ctx.invoked_subcommand is None:
40
+ # No subcommand provided, run the serve command
41
+ # Change to project directory if provided
42
+ if project_path:
43
+ import os
44
+
45
+ os.chdir(project_path)
46
+ # Invoke the serve command through context
47
+ ctx.invoke(mcp_serve, adapter=None, base_path=None)
48
+
49
+
50
+ @mcp_app.command(name="serve")
51
+ def mcp_serve(
52
+ adapter: str | None = typer.Option(
53
+ None, "--adapter", "-a", help="Override default adapter type"
54
+ ),
55
+ base_path: str | None = typer.Option(
56
+ None, "--base-path", help="Base path for AITrackdown adapter"
57
+ ),
58
+ ) -> None:
59
+ """Start MCP server for JSON-RPC communication over stdio.
60
+
61
+ This command is used by Claude Code/Desktop when connecting to the MCP server.
62
+ You typically don't need to run this manually - use 'mcp-ticketer install add' to configure.
63
+
64
+ Configuration Resolution:
65
+ - When MCP server starts, it uses the current working directory (cwd)
66
+ - The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
67
+ - Configuration is loaded with this priority:
68
+ 1. Project-specific: .mcp-ticketer/config.json in cwd
69
+ 2. Global: ~/.mcp-ticketer/config.json
70
+ 3. Default: aitrackdown adapter with .aitrackdown base path
71
+ """
72
+ # Local imports to avoid circular dependency
73
+ from ..mcp.server.server_sdk import configure_adapter
74
+ from ..mcp.server.server_sdk import main as sdk_main
75
+
76
+ # Import load_config locally to avoid circular import
77
+ # (main.py imports this module, so we can't import from main at module level)
78
+ from .ticket_commands import load_config
79
+
80
+ # Load configuration (respects project-specific config in cwd)
81
+ config = load_config()
82
+
83
+ # Determine adapter type with priority: CLI arg > config > .env files > default
84
+ if adapter:
85
+ # Priority 1: Command line argument
86
+ adapter_type = adapter
87
+ # Get base config from config file
88
+ adapters_config = config.get("adapters", {})
89
+ adapter_config = adapters_config.get(adapter_type, {})
90
+ else:
91
+ # Priority 2: Configuration file (project-specific)
92
+ adapter_type = config.get("default_adapter")
93
+ if adapter_type:
94
+ adapters_config = config.get("adapters", {})
95
+ adapter_config = adapters_config.get(adapter_type, {})
96
+ else:
97
+ # Priority 3: .env files (auto-detection fallback)
98
+ from ..mcp.server.main import _load_env_configuration
99
+
100
+ env_config = _load_env_configuration()
101
+ if env_config:
102
+ adapter_type = env_config["adapter_type"]
103
+ adapter_config = env_config["adapter_config"]
104
+ else:
105
+ # Priority 4: Default fallback
106
+ adapter_type = "aitrackdown"
107
+ adapters_config = config.get("adapters", {})
108
+ adapter_config = adapters_config.get(adapter_type, {})
109
+
110
+ # Override with command line options if provided (highest priority)
111
+ if base_path and adapter_type == "aitrackdown":
112
+ adapter_config["base_path"] = base_path
113
+
114
+ # Fallback to legacy config format
115
+ if not adapter_config and "config" in config:
116
+ adapter_config = config["config"]
117
+
118
+ # MCP server uses stdio for JSON-RPC, so we can't print to stdout
119
+ # Only print to stderr to avoid interfering with the protocol
120
+ if sys.stderr.isatty():
121
+ # Only print if stderr is a terminal (not redirected)
122
+ console.file = sys.stderr
123
+ console.print(
124
+ f"[green]Starting MCP SDK server[/green] with {adapter_type} adapter"
125
+ )
126
+ console.print(
127
+ "[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
128
+ )
129
+
130
+ # Configure adapter and run SDK server
131
+ try:
132
+ configure_adapter(adapter_type, adapter_config)
133
+ sdk_main()
134
+ except KeyboardInterrupt:
135
+ # Send this to stderr
136
+ if sys.stderr.isatty():
137
+ console.print("\n[yellow]Server stopped by user[/yellow]")
138
+ sys.exit(0)
139
+ except Exception as e:
140
+ # Log error to stderr
141
+ sys.stderr.write(f"MCP server error: {e}\n")
142
+ sys.exit(1)
143
+
144
+
145
+ @mcp_app.command(name="claude")
146
+ def mcp_claude(
147
+ global_config: bool = typer.Option(
148
+ False,
149
+ "--global",
150
+ "-g",
151
+ help="Configure Claude Desktop instead of project-level",
152
+ ),
153
+ force: bool = typer.Option(
154
+ False, "--force", "-f", help="Overwrite existing configuration"
155
+ ),
156
+ ) -> None:
157
+ """Configure Claude Code to use mcp-ticketer MCP server.
158
+
159
+ Reads configuration from .mcp-ticketer/config.json and updates
160
+ Claude Code's MCP settings accordingly.
161
+
162
+ By default, configures project-level (.mcp/config.json).
163
+ Use --global to configure Claude Desktop instead.
164
+
165
+ Examples:
166
+ --------
167
+ # Configure for current project (default)
168
+ mcp-ticketer mcp claude
169
+
170
+ # Configure Claude Desktop globally
171
+ mcp-ticketer mcp claude --global
172
+
173
+ # Force overwrite existing configuration
174
+ mcp-ticketer mcp claude --force
175
+
176
+ """
177
+ from ..cli.mcp_configure import configure_claude_mcp
178
+
179
+ try:
180
+ configure_claude_mcp(global_config=global_config, force=force)
181
+ except Exception as e:
182
+ console.print(f"[red]✗ Configuration failed:[/red] {e}")
183
+ raise typer.Exit(1) from e
184
+
185
+
186
+ @mcp_app.command(name="gemini")
187
+ def mcp_gemini(
188
+ scope: str = typer.Option(
189
+ "project",
190
+ "--scope",
191
+ "-s",
192
+ help="Configuration scope: 'project' (default) or 'user'",
193
+ ),
194
+ force: bool = typer.Option(
195
+ False, "--force", "-f", help="Overwrite existing configuration"
196
+ ),
197
+ ) -> None:
198
+ """Configure Gemini CLI to use mcp-ticketer MCP server.
199
+
200
+ Reads configuration from .mcp-ticketer/config.json and creates
201
+ Gemini CLI settings file with mcp-ticketer configuration.
202
+
203
+ By default, configures project-level (.gemini/settings.json).
204
+ Use --scope user to configure user-level (~/.gemini/settings.json).
205
+
206
+ Examples:
207
+ --------
208
+ # Configure for current project (default)
209
+ mcp-ticketer mcp gemini
210
+
211
+ # Configure at user level
212
+ mcp-ticketer mcp gemini --scope user
213
+
214
+ # Force overwrite existing configuration
215
+ mcp-ticketer mcp gemini --force
216
+
217
+ """
218
+ from ..cli.gemini_configure import configure_gemini_mcp
219
+
220
+ # Validate scope parameter
221
+ if scope not in ["project", "user"]:
222
+ console.print(
223
+ f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
224
+ )
225
+ raise typer.Exit(1) from None
226
+
227
+ try:
228
+ configure_gemini_mcp(scope=scope, force=force) # type: ignore
229
+ except Exception as e:
230
+ console.print(f"[red]✗ Configuration failed:[/red] {e}")
231
+ raise typer.Exit(1) from e
232
+
233
+
234
+ @mcp_app.command(name="codex")
235
+ def mcp_codex(
236
+ force: bool = typer.Option(
237
+ False, "--force", "-f", help="Overwrite existing configuration"
238
+ ),
239
+ ) -> None:
240
+ """Configure Codex CLI to use mcp-ticketer MCP server.
241
+
242
+ Reads configuration from .mcp-ticketer/config.json and creates
243
+ Codex CLI config.toml with mcp-ticketer configuration.
244
+
245
+ IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
246
+ There is no project-level configuration support. After configuration,
247
+ you must restart Codex CLI for changes to take effect.
248
+
249
+ Examples:
250
+ --------
251
+ # Configure Codex CLI globally
252
+ mcp-ticketer mcp codex
253
+
254
+ # Force overwrite existing configuration
255
+ mcp-ticketer mcp codex --force
256
+
257
+ """
258
+ from ..cli.codex_configure import configure_codex_mcp
259
+
260
+ try:
261
+ configure_codex_mcp(force=force)
262
+ except Exception as e:
263
+ console.print(f"[red]✗ Configuration failed:[/red] {e}")
264
+ raise typer.Exit(1) from e
265
+
266
+
267
+ @mcp_app.command(name="auggie")
268
+ def mcp_auggie(
269
+ force: bool = typer.Option(
270
+ False, "--force", "-f", help="Overwrite existing configuration"
271
+ ),
272
+ ) -> None:
273
+ """Configure Auggie CLI to use mcp-ticketer MCP server.
274
+
275
+ Reads configuration from .mcp-ticketer/config.json and creates
276
+ Auggie CLI settings.json with mcp-ticketer configuration.
277
+
278
+ IMPORTANT: Auggie CLI ONLY supports global configuration at ~/.augment/settings.json.
279
+ There is no project-level configuration support. After configuration,
280
+ you must restart Auggie CLI for changes to take effect.
281
+
282
+ Examples:
283
+ --------
284
+ # Configure Auggie CLI globally
285
+ mcp-ticketer mcp auggie
286
+
287
+ # Force overwrite existing configuration
288
+ mcp-ticketer mcp auggie --force
289
+
290
+ """
291
+ from ..cli.auggie_configure import configure_auggie_mcp
292
+
293
+ try:
294
+ configure_auggie_mcp(force=force)
295
+ except Exception as e:
296
+ console.print(f"[red]✗ Configuration failed:[/red] {e}")
297
+ raise typer.Exit(1) from e
298
+
299
+
300
+ @mcp_app.command(name="status")
301
+ def mcp_status() -> None:
302
+ """Check MCP server status.
303
+
304
+ Shows whether the MCP server is configured and running for various platforms.
305
+
306
+ Examples:
307
+ --------
308
+ mcp-ticketer mcp status
309
+
310
+ """
311
+ console.print("[bold]MCP Server Status[/bold]\n")
312
+
313
+ # Check project-level configuration
314
+ project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
315
+ if project_config.exists():
316
+ console.print(f"[green]✓[/green] Project config found: {project_config}")
317
+ try:
318
+ with open(project_config) as f:
319
+ config = json.load(f)
320
+ adapter = config.get("default_adapter", "aitrackdown")
321
+ console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
322
+ except Exception as e:
323
+ console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
324
+ else:
325
+ console.print("[yellow]○[/yellow] No project config found")
326
+
327
+ # Check Claude Code configuration
328
+ claude_code_config = Path.cwd() / ".mcp" / "config.json"
329
+ if claude_code_config.exists():
330
+ console.print(
331
+ f"\n[green]✓[/green] Claude Code configured: {claude_code_config}"
332
+ )
333
+ else:
334
+ console.print("\n[yellow]○[/yellow] Claude Code not configured")
335
+
336
+ # Check Claude Desktop configuration
337
+ claude_desktop_config = (
338
+ Path.home()
339
+ / "Library"
340
+ / "Application Support"
341
+ / "Claude"
342
+ / "claude_desktop_config.json"
343
+ )
344
+ if claude_desktop_config.exists():
345
+ try:
346
+ with open(claude_desktop_config) as f:
347
+ config = json.load(f)
348
+ if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
349
+ console.print(
350
+ f"[green]✓[/green] Claude Desktop configured: {claude_desktop_config}"
351
+ )
352
+ else:
353
+ console.print(
354
+ "[yellow]○[/yellow] Claude Desktop config exists but mcp-ticketer not found"
355
+ )
356
+ except Exception:
357
+ console.print(
358
+ "[yellow]○[/yellow] Claude Desktop config exists but could not be read"
359
+ )
360
+ else:
361
+ console.print("[yellow]○[/yellow] Claude Desktop not configured")
362
+
363
+ # Check Gemini configuration
364
+ gemini_project_config = Path.cwd() / ".gemini" / "settings.json"
365
+ gemini_user_config = Path.home() / ".gemini" / "settings.json"
366
+ if gemini_project_config.exists():
367
+ console.print(
368
+ f"\n[green]✓[/green] Gemini (project) configured: {gemini_project_config}"
369
+ )
370
+ elif gemini_user_config.exists():
371
+ console.print(
372
+ f"\n[green]✓[/green] Gemini (user) configured: {gemini_user_config}"
373
+ )
374
+ else:
375
+ console.print("\n[yellow]○[/yellow] Gemini not configured")
376
+
377
+ # Check Codex configuration
378
+ codex_config = Path.home() / ".codex" / "config.toml"
379
+ if codex_config.exists():
380
+ console.print(f"[green]✓[/green] Codex configured: {codex_config}")
381
+ else:
382
+ console.print("[yellow]○[/yellow] Codex not configured")
383
+
384
+ # Check Auggie configuration
385
+ auggie_config = Path.home() / ".augment" / "settings.json"
386
+ if auggie_config.exists():
387
+ console.print(f"[green]✓[/green] Auggie configured: {auggie_config}")
388
+ else:
389
+ console.print("[yellow]○[/yellow] Auggie not configured")
390
+
391
+ console.print(
392
+ "\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
393
+ )
394
+
395
+
396
+ @mcp_app.command(name="stop")
397
+ def mcp_stop() -> None:
398
+ """Stop MCP server (placeholder - MCP runs on-demand via stdio).
399
+
400
+ Note: The MCP server runs on-demand when AI clients connect via stdio.
401
+ It doesn't run as a persistent background service, so there's nothing to stop.
402
+ This command is provided for consistency but has no effect.
403
+
404
+ Examples:
405
+ --------
406
+ mcp-ticketer mcp stop
407
+
408
+ """
409
+ console.print(
410
+ "[yellow]ℹ[/yellow] MCP server runs on-demand via stdio (not as a background service)"
411
+ )
412
+ console.print("There is no persistent server process to stop.")
413
+ console.print(
414
+ "\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
415
+ )
@@ -49,14 +49,20 @@ class PlatformDetector:
49
49
  def detect_claude_code() -> DetectedPlatform | None:
50
50
  """Detect Claude Code installation.
51
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.
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)
54
55
 
55
56
  Returns:
56
57
  DetectedPlatform if Claude Code config exists, None otherwise
57
58
 
58
59
  """
59
- config_path = Path.home() / ".claude.json"
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
60
66
 
61
67
  # Check if config file exists
62
68
  if not config_path.exists():
@@ -194,6 +200,51 @@ class PlatformDetector:
194
200
  executable_path=executable_path,
195
201
  )
196
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
+
197
248
  @staticmethod
198
249
  def detect_codex() -> DetectedPlatform | None:
199
250
  """Detect Codex installation.
@@ -304,11 +355,14 @@ class PlatformDetector:
304
355
  )
305
356
 
306
357
  @classmethod
307
- def detect_all(cls, project_path: Path | None = None) -> list[DetectedPlatform]:
358
+ def detect_all(
359
+ cls, project_path: Path | None = None, exclude_desktop: bool = False
360
+ ) -> list[DetectedPlatform]:
308
361
  """Detect all installed AI client platforms.
309
362
 
310
363
  Args:
311
364
  project_path: Optional project directory for project-level detection
365
+ exclude_desktop: If True, exclude desktop AI assistants (Claude Desktop)
312
366
 
313
367
  Returns:
314
368
  List of detected platforms (empty if none found)
@@ -326,30 +380,40 @@ class PlatformDetector:
326
380
  >>> gemini = next(p for p in platforms if p.name == "gemini")
327
381
  >>> print(gemini.scope) # "project" or "global" or "both"
328
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
+
329
387
  """
330
388
  detected = []
331
389
 
332
- # Detect Claude Code
390
+ # Detect Claude Code (project-level code editor)
333
391
  claude_code = cls.detect_claude_code()
334
392
  if claude_code:
335
393
  detected.append(claude_code)
336
394
 
337
- # Detect Claude Desktop
338
- claude_desktop = cls.detect_claude_desktop()
339
- if claude_desktop:
340
- detected.append(claude_desktop)
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)
341
405
 
342
- # Detect Auggie
406
+ # Detect Auggie (code assistant)
343
407
  auggie = cls.detect_auggie()
344
408
  if auggie:
345
409
  detected.append(auggie)
346
410
 
347
- # Detect Codex
411
+ # Detect Codex (code assistant)
348
412
  codex = cls.detect_codex()
349
413
  if codex:
350
414
  detected.append(codex)
351
415
 
352
- # Detect Gemini (with project path support)
416
+ # Detect Gemini (code assistant with project path support)
353
417
  gemini = cls.detect_gemini(project_path=project_path)
354
418
  if gemini:
355
419
  detected.append(gemini)
@@ -381,6 +445,7 @@ def get_platform_by_name(
381
445
  detection_map = {
382
446
  "claude-code": detector.detect_claude_code,
383
447
  "claude-desktop": detector.detect_claude_desktop,
448
+ "cursor": detector.detect_cursor,
384
449
  "auggie": detector.detect_auggie,
385
450
  "codex": detector.detect_codex,
386
451
  "gemini": lambda: detector.detect_gemini(project_path),