mcp-ticketer 0.1.20__py3-none-any.whl → 0.1.22__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 (42) hide show
  1. mcp_ticketer/__init__.py +7 -7
  2. mcp_ticketer/__version__.py +4 -2
  3. mcp_ticketer/adapters/__init__.py +4 -4
  4. mcp_ticketer/adapters/aitrackdown.py +54 -38
  5. mcp_ticketer/adapters/github.py +175 -109
  6. mcp_ticketer/adapters/hybrid.py +90 -45
  7. mcp_ticketer/adapters/jira.py +139 -130
  8. mcp_ticketer/adapters/linear.py +374 -225
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +14 -15
  11. mcp_ticketer/cli/__init__.py +1 -1
  12. mcp_ticketer/cli/configure.py +69 -93
  13. mcp_ticketer/cli/discover.py +43 -35
  14. mcp_ticketer/cli/main.py +250 -293
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +10 -12
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +115 -60
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +36 -30
  21. mcp_ticketer/core/config.py +113 -77
  22. mcp_ticketer/core/env_discovery.py +51 -19
  23. mcp_ticketer/core/http_client.py +46 -29
  24. mcp_ticketer/core/mappers.py +79 -35
  25. mcp_ticketer/core/models.py +29 -15
  26. mcp_ticketer/core/project_config.py +131 -66
  27. mcp_ticketer/core/registry.py +12 -12
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +183 -129
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +29 -25
  33. mcp_ticketer/queue/queue.py +144 -82
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +48 -33
  36. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.22.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.20.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -3,25 +3,23 @@
3
3
  import asyncio
4
4
  import json
5
5
  import os
6
- from pathlib import Path
7
- from typing import Optional, List
8
6
  from enum import Enum
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
9
 
10
10
  import typer
11
+ from dotenv import load_dotenv
11
12
  from rich.console import Console
12
13
  from rich.table import Table
13
- from rich import print as rprint
14
- from dotenv import load_dotenv
15
14
 
16
- from ..core import Task, TicketState, Priority, AdapterRegistry
15
+ from ..__version__ import __version__
16
+ from ..core import AdapterRegistry, Priority, TicketState
17
17
  from ..core.models import SearchQuery
18
- from ..adapters import AITrackdownAdapter
19
18
  from ..queue import Queue, QueueStatus, WorkerManager
20
- from .queue_commands import app as queue_app
21
- from ..__version__ import __version__
22
- from .configure import configure_wizard, show_current_config, set_adapter_config
23
- from .migrate_config import migrate_config_command
19
+ from .configure import configure_wizard, set_adapter_config, show_current_config
24
20
  from .discover import app as discover_app
21
+ from .migrate_config import migrate_config_command
22
+ from .queue_commands import app as queue_app
25
23
 
26
24
  # Load environment variables from .env files
27
25
  # Priority: .env.local (highest) > .env (base)
@@ -58,21 +56,21 @@ def main_callback(
58
56
  "-v",
59
57
  callback=version_callback,
60
58
  is_eager=True,
61
- help="Show version and exit"
59
+ help="Show version and exit",
62
60
  ),
63
61
  ):
64
- """
65
- MCP Ticketer - Universal ticket management interface.
62
+ """MCP Ticketer - Universal ticket management interface.
66
63
  """
67
64
  pass
68
65
 
69
66
 
70
- # Configuration file management
71
- CONFIG_FILE = Path.home() / ".mcp-ticketer" / "config.json"
67
+ # Configuration file management - PROJECT-LOCAL ONLY
68
+ CONFIG_FILE = Path.cwd() / ".mcp-ticketer" / "config.json"
72
69
 
73
70
 
74
71
  class AdapterType(str, Enum):
75
72
  """Available adapter types."""
73
+
76
74
  AITRACKDOWN = "aitrackdown"
77
75
  LINEAR = "linear"
78
76
  JIRA = "jira"
@@ -80,48 +78,82 @@ class AdapterType(str, Enum):
80
78
 
81
79
 
82
80
  def load_config(project_dir: Optional[Path] = None) -> dict:
83
- """Load configuration from file.
81
+ """Load configuration from project-local config file ONLY.
82
+
83
+ SECURITY: This method ONLY reads from the current project directory
84
+ to prevent configuration leakage across projects. It will NEVER read
85
+ from user home directory or system-wide locations.
84
86
 
85
87
  Args:
86
88
  project_dir: Optional project directory to load config from
87
89
 
88
90
  Resolution order:
89
91
  1. Project-specific config (.mcp-ticketer/config.json in project_dir or cwd)
90
- 2. Global config (~/.mcp-ticketer/config.json)
92
+ 2. Default to aitrackdown adapter
91
93
 
92
94
  Returns:
93
- Configuration dictionary
95
+ Configuration dictionary with adapter and config keys.
96
+ Defaults to aitrackdown if no local config exists.
97
+
94
98
  """
99
+ import logging
100
+
101
+ logger = logging.getLogger(__name__)
102
+
95
103
  # Use provided project_dir or current working directory
96
104
  base_dir = project_dir or Path.cwd()
97
105
 
98
- # Check project-specific config first
106
+ # ONLY check project-specific config in project directory
99
107
  project_config = base_dir / ".mcp-ticketer" / "config.json"
100
108
  if project_config.exists():
109
+ # Validate that config file is actually in project directory
101
110
  try:
102
- with open(project_config, "r") as f:
103
- return json.load(f)
104
- except (json.JSONDecodeError, IOError) as e:
105
- console.print(f"[yellow]Warning: Could not load project config: {e}[/yellow]")
106
- # Fall through to global config
107
-
108
- # Fall back to global config
109
- if CONFIG_FILE.exists():
110
- try:
111
- with open(CONFIG_FILE, "r") as f:
112
- return json.load(f)
113
- except (json.JSONDecodeError, IOError) as e:
114
- console.print(f"[yellow]Warning: Could not load global config: {e}[/yellow]")
111
+ if not project_config.resolve().is_relative_to(base_dir.resolve()):
112
+ logger.error(
113
+ f"Security violation: Config file {project_config} "
114
+ "is not within project directory"
115
+ )
116
+ raise ValueError(
117
+ f"Security violation: Config file {project_config} "
118
+ "is not within project directory"
119
+ )
120
+ except (ValueError, RuntimeError):
121
+ # is_relative_to may raise ValueError in some cases
122
+ pass
115
123
 
116
- # Default fallback
124
+ try:
125
+ with open(project_config) as f:
126
+ config = json.load(f)
127
+ logger.info(
128
+ f"Loaded configuration from project-local: {project_config}"
129
+ )
130
+ return config
131
+ except (OSError, json.JSONDecodeError) as e:
132
+ logger.warning(f"Could not load project config: {e}, using defaults")
133
+ console.print(
134
+ f"[yellow]Warning: Could not load project config: {e}[/yellow]"
135
+ )
136
+
137
+ # Default to aitrackdown with local base path
138
+ logger.info("No project-local config found, defaulting to aitrackdown adapter")
117
139
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
118
140
 
119
141
 
120
142
  def save_config(config: dict) -> None:
121
- """Save configuration to file."""
122
- CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
123
- with open(CONFIG_FILE, "w") as f:
143
+ """Save configuration to project-local config file ONLY.
144
+
145
+ SECURITY: This method ONLY saves to the current project directory
146
+ to prevent configuration leakage across projects.
147
+ """
148
+ import logging
149
+
150
+ logger = logging.getLogger(__name__)
151
+
152
+ project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
153
+ project_config.parent.mkdir(parents=True, exist_ok=True)
154
+ with open(project_config, "w") as f:
124
155
  json.dump(config, f, indent=2)
156
+ logger.info(f"Saved configuration to project-local: {project_config}")
125
157
 
126
158
 
127
159
  def merge_config(updates: dict) -> dict:
@@ -132,6 +164,7 @@ def merge_config(updates: dict) -> dict:
132
164
 
133
165
  Returns:
134
166
  Updated configuration
167
+
135
168
  """
136
169
  config = load_config()
137
170
 
@@ -151,12 +184,15 @@ def merge_config(updates: dict) -> dict:
151
184
  return config
152
185
 
153
186
 
154
- def get_adapter(override_adapter: Optional[str] = None, override_config: Optional[dict] = None):
187
+ def get_adapter(
188
+ override_adapter: Optional[str] = None, override_config: Optional[dict] = None
189
+ ):
155
190
  """Get configured adapter instance.
156
191
 
157
192
  Args:
158
193
  override_adapter: Override the default adapter type
159
194
  override_config: Override configuration for the adapter
195
+
160
196
  """
161
197
  config = load_config()
162
198
 
@@ -182,6 +218,7 @@ def get_adapter(override_adapter: Optional[str] = None, override_config: Optiona
182
218
 
183
219
  # Add environment variables for authentication
184
220
  import os
221
+
185
222
  if adapter_type == "linear":
186
223
  if not adapter_config.get("api_key"):
187
224
  adapter_config["api_key"] = os.getenv("LINEAR_API_KEY")
@@ -203,64 +240,48 @@ def init(
203
240
  None,
204
241
  "--adapter",
205
242
  "-a",
206
- help="Adapter type to use (auto-detected from .env if not specified)"
243
+ help="Adapter type to use (auto-detected from .env if not specified)",
207
244
  ),
208
245
  project_path: Optional[str] = typer.Option(
209
- None,
210
- "--path",
211
- help="Project path (default: current directory)"
246
+ None, "--path", help="Project path (default: current directory)"
212
247
  ),
213
248
  global_config: bool = typer.Option(
214
249
  False,
215
250
  "--global",
216
251
  "-g",
217
- help="Save to global config instead of project-specific"
252
+ help="Save to global config instead of project-specific",
218
253
  ),
219
254
  base_path: Optional[str] = typer.Option(
220
255
  None,
221
256
  "--base-path",
222
257
  "-p",
223
- help="Base path for ticket storage (AITrackdown only)"
258
+ help="Base path for ticket storage (AITrackdown only)",
224
259
  ),
225
260
  api_key: Optional[str] = typer.Option(
226
- None,
227
- "--api-key",
228
- help="API key for Linear or API token for JIRA"
261
+ None, "--api-key", help="API key for Linear or API token for JIRA"
229
262
  ),
230
263
  team_id: Optional[str] = typer.Option(
231
- None,
232
- "--team-id",
233
- help="Linear team ID (required for Linear adapter)"
264
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
234
265
  ),
235
266
  jira_server: Optional[str] = typer.Option(
236
267
  None,
237
268
  "--jira-server",
238
- help="JIRA server URL (e.g., https://company.atlassian.net)"
269
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
239
270
  ),
240
271
  jira_email: Optional[str] = typer.Option(
241
- None,
242
- "--jira-email",
243
- help="JIRA user email for authentication"
272
+ None, "--jira-email", help="JIRA user email for authentication"
244
273
  ),
245
274
  jira_project: Optional[str] = typer.Option(
246
- None,
247
- "--jira-project",
248
- help="Default JIRA project key"
275
+ None, "--jira-project", help="Default JIRA project key"
249
276
  ),
250
277
  github_owner: Optional[str] = typer.Option(
251
- None,
252
- "--github-owner",
253
- help="GitHub repository owner"
278
+ None, "--github-owner", help="GitHub repository owner"
254
279
  ),
255
280
  github_repo: Optional[str] = typer.Option(
256
- None,
257
- "--github-repo",
258
- help="GitHub repository name"
281
+ None, "--github-repo", help="GitHub repository name"
259
282
  ),
260
283
  github_token: Optional[str] = typer.Option(
261
- None,
262
- "--github-token",
263
- help="GitHub Personal Access Token"
284
+ None, "--github-token", help="GitHub Personal Access Token"
264
285
  ),
265
286
  ) -> None:
266
287
  """Initialize mcp-ticketer for the current project.
@@ -280,10 +301,12 @@ def init(
280
301
 
281
302
  # Save globally (not recommended)
282
303
  mcp-ticketer init --global
304
+
283
305
  """
284
306
  from pathlib import Path
285
- from ..core.project_config import ConfigResolver
307
+
286
308
  from ..core.env_discovery import discover_config
309
+ from ..core.project_config import ConfigResolver
287
310
 
288
311
  # Determine project path
289
312
  proj_path = Path(project_path) if project_path else Path.cwd()
@@ -295,7 +318,7 @@ def init(
295
318
  if config_path.exists():
296
319
  if not typer.confirm(
297
320
  f"Configuration already exists at {config_path}. Overwrite?",
298
- default=False
321
+ default=False,
299
322
  ):
300
323
  console.print("[yellow]Initialization cancelled.[/yellow]")
301
324
  raise typer.Exit(0)
@@ -305,30 +328,37 @@ def init(
305
328
  adapter_type = adapter
306
329
 
307
330
  if not adapter_type:
308
- console.print("[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]")
331
+ console.print(
332
+ "[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
333
+ )
309
334
  discovered = discover_config(proj_path)
310
335
 
311
336
  if discovered and discovered.adapters:
312
337
  primary = discovered.get_primary_adapter()
313
338
  if primary:
314
339
  adapter_type = primary.adapter_type
315
- console.print(f"[green]✓ Detected {adapter_type} adapter from environment files[/green]")
340
+ console.print(
341
+ f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
342
+ )
316
343
 
317
344
  # Show what was discovered
318
- console.print(f"\n[dim]Configuration found in: {primary.found_in}[/dim]")
345
+ console.print(
346
+ f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
347
+ )
319
348
  console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
320
349
  else:
321
350
  adapter_type = "aitrackdown" # Fallback
322
- console.print("[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]")
351
+ console.print(
352
+ "[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]"
353
+ )
323
354
  else:
324
355
  adapter_type = "aitrackdown" # Fallback
325
- console.print("[yellow]⚠ No .env files found, defaulting to aitrackdown[/yellow]")
356
+ console.print(
357
+ "[yellow]⚠ No .env files found, defaulting to aitrackdown[/yellow]"
358
+ )
326
359
 
327
360
  # 2. Create configuration based on adapter type
328
- config = {
329
- "default_adapter": adapter_type,
330
- "adapters": {}
331
- }
361
+ config = {"default_adapter": adapter_type, "adapters": {}}
332
362
 
333
363
  # 3. If discovered and matches adapter_type, use discovered config
334
364
  if discovered and adapter_type != "aitrackdown":
@@ -355,7 +385,9 @@ def init(
355
385
  linear_config["api_key"] = linear_api_key
356
386
  elif not discovered:
357
387
  console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
358
- console.print("Set LINEAR_API_KEY environment variable or use --api-key option")
388
+ console.print(
389
+ "Set LINEAR_API_KEY environment variable or use --api-key option"
390
+ )
359
391
 
360
392
  if linear_config:
361
393
  config["adapters"]["linear"] = linear_config
@@ -370,7 +402,9 @@ def init(
370
402
 
371
403
  if not server:
372
404
  console.print("[red]Error:[/red] JIRA server URL is required")
373
- console.print("Use --jira-server or set JIRA_SERVER environment variable")
405
+ console.print(
406
+ "Use --jira-server or set JIRA_SERVER environment variable"
407
+ )
374
408
  raise typer.Exit(1)
375
409
 
376
410
  if not email:
@@ -380,15 +414,15 @@ def init(
380
414
 
381
415
  if not token:
382
416
  console.print("[red]Error:[/red] JIRA API token is required")
383
- console.print("Use --api-key or set JIRA_API_TOKEN environment variable")
384
- console.print("[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]")
417
+ console.print(
418
+ "Use --api-key or set JIRA_API_TOKEN environment variable"
419
+ )
420
+ console.print(
421
+ "[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
422
+ )
385
423
  raise typer.Exit(1)
386
424
 
387
- jira_config = {
388
- "server": server,
389
- "email": email,
390
- "api_token": token
391
- }
425
+ jira_config = {"server": server, "email": email, "api_token": token}
392
426
 
393
427
  if project:
394
428
  jira_config["project_key"] = project
@@ -404,25 +438,37 @@ def init(
404
438
 
405
439
  if not owner:
406
440
  console.print("[red]Error:[/red] GitHub repository owner is required")
407
- console.print("Use --github-owner or set GITHUB_OWNER environment variable")
441
+ console.print(
442
+ "Use --github-owner or set GITHUB_OWNER environment variable"
443
+ )
408
444
  raise typer.Exit(1)
409
445
 
410
446
  if not repo:
411
447
  console.print("[red]Error:[/red] GitHub repository name is required")
412
- console.print("Use --github-repo or set GITHUB_REPO environment variable")
448
+ console.print(
449
+ "Use --github-repo or set GITHUB_REPO environment variable"
450
+ )
413
451
  raise typer.Exit(1)
414
452
 
415
453
  if not token:
416
- console.print("[red]Error:[/red] GitHub Personal Access Token is required")
417
- console.print("Use --github-token or set GITHUB_TOKEN environment variable")
418
- console.print("[dim]Create token at: https://github.com/settings/tokens/new[/dim]")
419
- console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]")
454
+ console.print(
455
+ "[red]Error:[/red] GitHub Personal Access Token is required"
456
+ )
457
+ console.print(
458
+ "Use --github-token or set GITHUB_TOKEN environment variable"
459
+ )
460
+ console.print(
461
+ "[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
462
+ )
463
+ console.print(
464
+ "[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]"
465
+ )
420
466
  raise typer.Exit(1)
421
467
 
422
468
  config["adapters"]["github"] = {
423
469
  "owner": owner,
424
470
  "repo": repo,
425
- "token": token
471
+ "token": token,
426
472
  }
427
473
 
428
474
  # 5. Save to appropriate location
@@ -432,7 +478,7 @@ def init(
432
478
  config_file_path = resolver.GLOBAL_CONFIG_PATH
433
479
  config_file_path.parent.mkdir(parents=True, exist_ok=True)
434
480
 
435
- with open(config_file_path, 'w') as f:
481
+ with open(config_file_path, "w") as f:
436
482
  json.dump(config, f, indent=2)
437
483
 
438
484
  console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
@@ -442,7 +488,7 @@ def init(
442
488
  config_file_path = proj_path / ".mcp-ticketer" / "config.json"
443
489
  config_file_path.parent.mkdir(parents=True, exist_ok=True)
444
490
 
445
- with open(config_file_path, 'w') as f:
491
+ with open(config_file_path, "w") as f:
446
492
  json.dump(config, f, indent=2)
447
493
 
448
494
  console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
@@ -453,12 +499,12 @@ def init(
453
499
  if gitignore_path.exists():
454
500
  gitignore_content = gitignore_path.read_text()
455
501
  if ".mcp-ticketer" not in gitignore_content:
456
- with open(gitignore_path, 'a') as f:
502
+ with open(gitignore_path, "a") as f:
457
503
  f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
458
504
  console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
459
505
  else:
460
506
  # Create .gitignore if it doesn't exist
461
- with open(gitignore_path, 'w') as f:
507
+ with open(gitignore_path, "w") as f:
462
508
  f.write("# MCP Ticketer\n.mcp-ticketer/\n")
463
509
  console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
464
510
 
@@ -469,64 +515,48 @@ def install(
469
515
  None,
470
516
  "--adapter",
471
517
  "-a",
472
- help="Adapter type to use (auto-detected from .env if not specified)"
518
+ help="Adapter type to use (auto-detected from .env if not specified)",
473
519
  ),
474
520
  project_path: Optional[str] = typer.Option(
475
- None,
476
- "--path",
477
- help="Project path (default: current directory)"
521
+ None, "--path", help="Project path (default: current directory)"
478
522
  ),
479
523
  global_config: bool = typer.Option(
480
524
  False,
481
525
  "--global",
482
526
  "-g",
483
- help="Save to global config instead of project-specific"
527
+ help="Save to global config instead of project-specific",
484
528
  ),
485
529
  base_path: Optional[str] = typer.Option(
486
530
  None,
487
531
  "--base-path",
488
532
  "-p",
489
- help="Base path for ticket storage (AITrackdown only)"
533
+ help="Base path for ticket storage (AITrackdown only)",
490
534
  ),
491
535
  api_key: Optional[str] = typer.Option(
492
- None,
493
- "--api-key",
494
- help="API key for Linear or API token for JIRA"
536
+ None, "--api-key", help="API key for Linear or API token for JIRA"
495
537
  ),
496
538
  team_id: Optional[str] = typer.Option(
497
- None,
498
- "--team-id",
499
- help="Linear team ID (required for Linear adapter)"
539
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
500
540
  ),
501
541
  jira_server: Optional[str] = typer.Option(
502
542
  None,
503
543
  "--jira-server",
504
- help="JIRA server URL (e.g., https://company.atlassian.net)"
544
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
505
545
  ),
506
546
  jira_email: Optional[str] = typer.Option(
507
- None,
508
- "--jira-email",
509
- help="JIRA user email for authentication"
547
+ None, "--jira-email", help="JIRA user email for authentication"
510
548
  ),
511
549
  jira_project: Optional[str] = typer.Option(
512
- None,
513
- "--jira-project",
514
- help="Default JIRA project key"
550
+ None, "--jira-project", help="Default JIRA project key"
515
551
  ),
516
552
  github_owner: Optional[str] = typer.Option(
517
- None,
518
- "--github-owner",
519
- help="GitHub repository owner"
553
+ None, "--github-owner", help="GitHub repository owner"
520
554
  ),
521
555
  github_repo: Optional[str] = typer.Option(
522
- None,
523
- "--github-repo",
524
- help="GitHub repository name"
556
+ None, "--github-repo", help="GitHub repository name"
525
557
  ),
526
558
  github_token: Optional[str] = typer.Option(
527
- None,
528
- "--github-token",
529
- help="GitHub Personal Access Token"
559
+ None, "--github-token", help="GitHub Personal Access Token"
530
560
  ),
531
561
  ) -> None:
532
562
  """Initialize mcp-ticketer for the current project (alias for init).
@@ -547,6 +577,7 @@ def install(
547
577
 
548
578
  # Save globally (not recommended)
549
579
  mcp-ticketer install --global
580
+
550
581
  """
551
582
  # Call init with all parameters
552
583
  init(
@@ -568,45 +599,20 @@ def install(
568
599
  @app.command("set")
569
600
  def set_config(
570
601
  adapter: Optional[AdapterType] = typer.Option(
571
- None,
572
- "--adapter",
573
- "-a",
574
- help="Set default adapter"
602
+ None, "--adapter", "-a", help="Set default adapter"
575
603
  ),
576
604
  team_key: Optional[str] = typer.Option(
577
- None,
578
- "--team-key",
579
- help="Linear team key (e.g., BTA)"
580
- ),
581
- team_id: Optional[str] = typer.Option(
582
- None,
583
- "--team-id",
584
- help="Linear team ID"
605
+ None, "--team-key", help="Linear team key (e.g., BTA)"
585
606
  ),
607
+ team_id: Optional[str] = typer.Option(None, "--team-id", help="Linear team ID"),
586
608
  owner: Optional[str] = typer.Option(
587
- None,
588
- "--owner",
589
- help="GitHub repository owner"
590
- ),
591
- repo: Optional[str] = typer.Option(
592
- None,
593
- "--repo",
594
- help="GitHub repository name"
595
- ),
596
- server: Optional[str] = typer.Option(
597
- None,
598
- "--server",
599
- help="JIRA server URL"
600
- ),
601
- project: Optional[str] = typer.Option(
602
- None,
603
- "--project",
604
- help="JIRA project key"
609
+ None, "--owner", help="GitHub repository owner"
605
610
  ),
611
+ repo: Optional[str] = typer.Option(None, "--repo", help="GitHub repository name"),
612
+ server: Optional[str] = typer.Option(None, "--server", help="JIRA server URL"),
613
+ project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
606
614
  base_path: Optional[str] = typer.Option(
607
- None,
608
- "--base-path",
609
- help="AITrackdown base path"
615
+ None, "--base-path", help="AITrackdown base path"
610
616
  ),
611
617
  ) -> None:
612
618
  """Set default adapter and adapter-specific configuration.
@@ -617,7 +623,9 @@ def set_config(
617
623
  # Show current configuration
618
624
  config = load_config()
619
625
  console.print("[bold]Current Configuration:[/bold]")
620
- console.print(f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]")
626
+ console.print(
627
+ f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]"
628
+ )
621
629
 
622
630
  adapters_config = config.get("adapters", {})
623
631
  if adapters_config:
@@ -626,7 +634,11 @@ def set_config(
626
634
  console.print(f"\n[cyan]{adapter_name}:[/cyan]")
627
635
  for key, value in adapter_config.items():
628
636
  # Don't display sensitive values like tokens
629
- if "token" in key.lower() or "key" in key.lower() and "team" not in key.lower():
637
+ if (
638
+ "token" in key.lower()
639
+ or "key" in key.lower()
640
+ and "team" not in key.lower()
641
+ ):
630
642
  value = "***" if value else "not set"
631
643
  console.print(f" {key}: {value}")
632
644
  return
@@ -649,7 +661,7 @@ def set_config(
649
661
  if team_id:
650
662
  linear_config["team_id"] = team_id
651
663
  adapter_configs["linear"] = linear_config
652
- console.print(f"[green]✓[/green] Linear settings updated")
664
+ console.print("[green]✓[/green] Linear settings updated")
653
665
 
654
666
  # GitHub configuration
655
667
  if owner or repo:
@@ -659,7 +671,7 @@ def set_config(
659
671
  if repo:
660
672
  github_config["repo"] = repo
661
673
  adapter_configs["github"] = github_config
662
- console.print(f"[green]✓[/green] GitHub settings updated")
674
+ console.print("[green]✓[/green] GitHub settings updated")
663
675
 
664
676
  # JIRA configuration
665
677
  if server or project:
@@ -669,12 +681,12 @@ def set_config(
669
681
  if project:
670
682
  jira_config["project_key"] = project
671
683
  adapter_configs["jira"] = jira_config
672
- console.print(f"[green]✓[/green] JIRA settings updated")
684
+ console.print("[green]✓[/green] JIRA settings updated")
673
685
 
674
686
  # AITrackdown configuration
675
687
  if base_path:
676
688
  adapter_configs["aitrackdown"] = {"base_path": base_path}
677
- console.print(f"[green]✓[/green] AITrackdown settings updated")
689
+ console.print("[green]✓[/green] AITrackdown settings updated")
678
690
 
679
691
  if adapter_configs:
680
692
  updates["adapters"] = adapter_configs
@@ -688,36 +700,22 @@ def set_config(
688
700
 
689
701
  @app.command("configure")
690
702
  def configure_command(
691
- show: bool = typer.Option(
692
- False,
693
- "--show",
694
- help="Show current configuration"
695
- ),
703
+ show: bool = typer.Option(False, "--show", help="Show current configuration"),
696
704
  adapter: Optional[str] = typer.Option(
697
- None,
698
- "--adapter",
699
- help="Set default adapter type"
700
- ),
701
- api_key: Optional[str] = typer.Option(
702
- None,
703
- "--api-key",
704
- help="Set API key/token"
705
+ None, "--adapter", help="Set default adapter type"
705
706
  ),
707
+ api_key: Optional[str] = typer.Option(None, "--api-key", help="Set API key/token"),
706
708
  project_id: Optional[str] = typer.Option(
707
- None,
708
- "--project-id",
709
- help="Set project ID"
709
+ None, "--project-id", help="Set project ID"
710
710
  ),
711
711
  team_id: Optional[str] = typer.Option(
712
- None,
713
- "--team-id",
714
- help="Set team ID (Linear)"
712
+ None, "--team-id", help="Set team ID (Linear)"
715
713
  ),
716
714
  global_scope: bool = typer.Option(
717
715
  False,
718
716
  "--global",
719
717
  "-g",
720
- help="Save to global config instead of project-specific"
718
+ help="Save to global config instead of project-specific",
721
719
  ),
722
720
  ) -> None:
723
721
  """Configure MCP Ticketer integration.
@@ -738,7 +736,7 @@ def configure_command(
738
736
  api_key=api_key,
739
737
  project_id=project_id,
740
738
  team_id=team_id,
741
- global_scope=global_scope
739
+ global_scope=global_scope,
742
740
  )
743
741
  return
744
742
 
@@ -749,9 +747,7 @@ def configure_command(
749
747
  @app.command("migrate-config")
750
748
  def migrate_config(
751
749
  dry_run: bool = typer.Option(
752
- False,
753
- "--dry-run",
754
- help="Show what would be done without making changes"
750
+ False, "--dry-run", help="Show what would be done without making changes"
755
751
  ),
756
752
  ) -> None:
757
753
  """Migrate configuration from old format to new format.
@@ -785,50 +781,42 @@ def status_command():
785
781
  # Show worker status
786
782
  worker_status = manager.get_status()
787
783
  if worker_status["running"]:
788
- console.print(f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})")
784
+ console.print(
785
+ f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
786
+ )
789
787
  else:
790
788
  console.print("\n[red]○ Worker is not running[/red]")
791
789
  if pending > 0:
792
- console.print("[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]")
790
+ console.print(
791
+ "[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
792
+ )
793
793
 
794
794
 
795
795
  @app.command()
796
796
  def create(
797
797
  title: str = typer.Argument(..., help="Ticket title"),
798
798
  description: Optional[str] = typer.Option(
799
- None,
800
- "--description",
801
- "-d",
802
- help="Ticket description"
799
+ None, "--description", "-d", help="Ticket description"
803
800
  ),
804
801
  priority: Priority = typer.Option(
805
- Priority.MEDIUM,
806
- "--priority",
807
- "-p",
808
- help="Priority level"
802
+ Priority.MEDIUM, "--priority", "-p", help="Priority level"
809
803
  ),
810
804
  tags: Optional[List[str]] = typer.Option(
811
- None,
812
- "--tag",
813
- "-t",
814
- help="Tags (can be specified multiple times)"
805
+ None, "--tag", "-t", help="Tags (can be specified multiple times)"
815
806
  ),
816
807
  assignee: Optional[str] = typer.Option(
817
- None,
818
- "--assignee",
819
- "-a",
820
- help="Assignee username"
808
+ None, "--assignee", "-a", help="Assignee username"
821
809
  ),
822
810
  adapter: Optional[AdapterType] = typer.Option(
823
- None,
824
- "--adapter",
825
- help="Override default adapter"
811
+ None, "--adapter", help="Override default adapter"
826
812
  ),
827
813
  ) -> None:
828
814
  """Create a new ticket."""
829
815
  # Get the adapter name
830
816
  config = load_config()
831
- adapter_name = adapter.value if adapter else config.get("default_adapter", "aitrackdown")
817
+ adapter_name = (
818
+ adapter.value if adapter else config.get("default_adapter", "aitrackdown")
819
+ )
832
820
 
833
821
  # Create task data
834
822
  task_data = {
@@ -842,9 +830,7 @@ def create(
842
830
  # Add to queue
843
831
  queue = Queue()
844
832
  queue_id = queue.add(
845
- ticket_data=task_data,
846
- adapter=adapter_name,
847
- operation="create"
833
+ ticket_data=task_data, adapter=adapter_name, operation="create"
848
834
  )
849
835
 
850
836
  console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
@@ -861,32 +847,22 @@ def create(
861
847
  @app.command("list")
862
848
  def list_tickets(
863
849
  state: Optional[TicketState] = typer.Option(
864
- None,
865
- "--state",
866
- "-s",
867
- help="Filter by state"
850
+ None, "--state", "-s", help="Filter by state"
868
851
  ),
869
852
  priority: Optional[Priority] = typer.Option(
870
- None,
871
- "--priority",
872
- "-p",
873
- help="Filter by priority"
874
- ),
875
- limit: int = typer.Option(
876
- 10,
877
- "--limit",
878
- "-l",
879
- help="Maximum number of tickets"
853
+ None, "--priority", "-p", help="Filter by priority"
880
854
  ),
855
+ limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
881
856
  adapter: Optional[AdapterType] = typer.Option(
882
- None,
883
- "--adapter",
884
- help="Override default adapter"
857
+ None, "--adapter", help="Override default adapter"
885
858
  ),
886
859
  ) -> None:
887
860
  """List tickets with optional filters."""
861
+
888
862
  async def _list():
889
- adapter_instance = get_adapter(override_adapter=adapter.value if adapter else None)
863
+ adapter_instance = get_adapter(
864
+ override_adapter=adapter.value if adapter else None
865
+ )
890
866
  filters = {}
891
867
  if state:
892
868
  filters["state"] = state
@@ -923,21 +899,17 @@ def list_tickets(
923
899
  @app.command()
924
900
  def show(
925
901
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
926
- comments: bool = typer.Option(
927
- False,
928
- "--comments",
929
- "-c",
930
- help="Show comments"
931
- ),
902
+ comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
932
903
  adapter: Optional[AdapterType] = typer.Option(
933
- None,
934
- "--adapter",
935
- help="Override default adapter"
904
+ None, "--adapter", help="Override default adapter"
936
905
  ),
937
906
  ) -> None:
938
907
  """Show detailed ticket information."""
908
+
939
909
  async def _show():
940
- adapter_instance = get_adapter(override_adapter=adapter.value if adapter else None)
910
+ adapter_instance = get_adapter(
911
+ override_adapter=adapter.value if adapter else None
912
+ )
941
913
  ticket = await adapter_instance.read(ticket_id)
942
914
  ticket_comments = None
943
915
  if comments and ticket:
@@ -957,7 +929,7 @@ def show(
957
929
  console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
958
930
 
959
931
  if ticket.description:
960
- console.print(f"\n[dim]Description:[/dim]")
932
+ console.print("\n[dim]Description:[/dim]")
961
933
  console.print(ticket.description)
962
934
 
963
935
  if ticket.tags:
@@ -979,27 +951,16 @@ def update(
979
951
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
980
952
  title: Optional[str] = typer.Option(None, "--title", help="New title"),
981
953
  description: Optional[str] = typer.Option(
982
- None,
983
- "--description",
984
- "-d",
985
- help="New description"
954
+ None, "--description", "-d", help="New description"
986
955
  ),
987
956
  priority: Optional[Priority] = typer.Option(
988
- None,
989
- "--priority",
990
- "-p",
991
- help="New priority"
957
+ None, "--priority", "-p", help="New priority"
992
958
  ),
993
959
  assignee: Optional[str] = typer.Option(
994
- None,
995
- "--assignee",
996
- "-a",
997
- help="New assignee"
960
+ None, "--assignee", "-a", help="New assignee"
998
961
  ),
999
962
  adapter: Optional[AdapterType] = typer.Option(
1000
- None,
1001
- "--adapter",
1002
- help="Override default adapter"
963
+ None, "--adapter", help="Override default adapter"
1003
964
  ),
1004
965
  ) -> None:
1005
966
  """Update ticket fields."""
@@ -1009,7 +970,9 @@ def update(
1009
970
  if description:
1010
971
  updates["description"] = description
1011
972
  if priority:
1012
- updates["priority"] = priority.value if isinstance(priority, Priority) else priority
973
+ updates["priority"] = (
974
+ priority.value if isinstance(priority, Priority) else priority
975
+ )
1013
976
  if assignee:
1014
977
  updates["assignee"] = assignee
1015
978
 
@@ -1019,18 +982,16 @@ def update(
1019
982
 
1020
983
  # Get the adapter name
1021
984
  config = load_config()
1022
- adapter_name = adapter.value if adapter else config.get("default_adapter", "aitrackdown")
985
+ adapter_name = (
986
+ adapter.value if adapter else config.get("default_adapter", "aitrackdown")
987
+ )
1023
988
 
1024
989
  # Add ticket_id to updates
1025
990
  updates["ticket_id"] = ticket_id
1026
991
 
1027
992
  # Add to queue
1028
993
  queue = Queue()
1029
- queue_id = queue.add(
1030
- ticket_data=updates,
1031
- adapter=adapter_name,
1032
- operation="update"
1033
- )
994
+ queue_id = queue.add(ticket_data=updates, adapter=adapter_name, operation="update")
1034
995
 
1035
996
  console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
1036
997
  for key, value in updates.items():
@@ -1049,25 +1010,25 @@ def transition(
1049
1010
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1050
1011
  state: TicketState = typer.Argument(..., help="Target state"),
1051
1012
  adapter: Optional[AdapterType] = typer.Option(
1052
- None,
1053
- "--adapter",
1054
- help="Override default adapter"
1013
+ None, "--adapter", help="Override default adapter"
1055
1014
  ),
1056
1015
  ) -> None:
1057
1016
  """Change ticket state with validation."""
1058
1017
  # Get the adapter name
1059
1018
  config = load_config()
1060
- adapter_name = adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1019
+ adapter_name = (
1020
+ adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1021
+ )
1061
1022
 
1062
1023
  # Add to queue
1063
1024
  queue = Queue()
1064
1025
  queue_id = queue.add(
1065
1026
  ticket_data={
1066
1027
  "ticket_id": ticket_id,
1067
- "state": state.value if hasattr(state, 'value') else state
1028
+ "state": state.value if hasattr(state, "value") else state,
1068
1029
  },
1069
1030
  adapter=adapter_name,
1070
- operation="transition"
1031
+ operation="transition",
1071
1032
  )
1072
1033
 
1073
1034
  console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
@@ -1088,14 +1049,15 @@ def search(
1088
1049
  assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
1089
1050
  limit: int = typer.Option(10, "--limit", "-l"),
1090
1051
  adapter: Optional[AdapterType] = typer.Option(
1091
- None,
1092
- "--adapter",
1093
- help="Override default adapter"
1052
+ None, "--adapter", help="Override default adapter"
1094
1053
  ),
1095
1054
  ) -> None:
1096
1055
  """Search tickets with advanced query."""
1056
+
1097
1057
  async def _search():
1098
- adapter_instance = get_adapter(override_adapter=adapter.value if adapter else None)
1058
+ adapter_instance = get_adapter(
1059
+ override_adapter=adapter.value if adapter else None
1060
+ )
1099
1061
  search_query = SearchQuery(
1100
1062
  query=query,
1101
1063
  state=state,
@@ -1130,9 +1092,7 @@ app.add_typer(discover_app, name="discover")
1130
1092
 
1131
1093
 
1132
1094
  @app.command()
1133
- def check(
1134
- queue_id: str = typer.Argument(..., help="Queue ID to check")
1135
- ):
1095
+ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
1136
1096
  """Check status of a queued operation."""
1137
1097
  queue = Queue()
1138
1098
  item = queue.get_item(queue_id)
@@ -1165,7 +1125,7 @@ def check(
1165
1125
  if item.error_message:
1166
1126
  console.print(f"\n[red]Error:[/red] {item.error_message}")
1167
1127
  elif item.result:
1168
- console.print(f"\n[green]Result:[/green]")
1128
+ console.print("\n[green]Result:[/green]")
1169
1129
  for key, value in item.result.items():
1170
1130
  console.print(f" {key}: {value}")
1171
1131
 
@@ -1176,15 +1136,10 @@ def check(
1176
1136
  @app.command()
1177
1137
  def serve(
1178
1138
  adapter: Optional[AdapterType] = typer.Option(
1179
- None,
1180
- "--adapter",
1181
- "-a",
1182
- help="Override default adapter type"
1139
+ None, "--adapter", "-a", help="Override default adapter type"
1183
1140
  ),
1184
1141
  base_path: Optional[str] = typer.Option(
1185
- None,
1186
- "--base-path",
1187
- help="Base path for AITrackdown adapter"
1142
+ None, "--base-path", help="Base path for AITrackdown adapter"
1188
1143
  ),
1189
1144
  ):
1190
1145
  """Start MCP server for JSON-RPC communication over stdio.
@@ -1206,7 +1161,9 @@ def serve(
1206
1161
  config = load_config()
1207
1162
 
1208
1163
  # Determine adapter type
1209
- adapter_type = adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1164
+ adapter_type = (
1165
+ adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1166
+ )
1210
1167
 
1211
1168
  # Get adapter configuration
1212
1169
  adapters_config = config.get("adapters", {})
@@ -1223,11 +1180,14 @@ def serve(
1223
1180
  # MCP server uses stdio for JSON-RPC, so we can't print to stdout
1224
1181
  # Only print to stderr to avoid interfering with the protocol
1225
1182
  import sys
1183
+
1226
1184
  if sys.stderr.isatty():
1227
1185
  # Only print if stderr is a terminal (not redirected)
1228
1186
  console.file = sys.stderr
1229
1187
  console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
1230
- console.print("[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]")
1188
+ console.print(
1189
+ "[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
1190
+ )
1231
1191
 
1232
1192
  # Create and run server
1233
1193
  try:
@@ -1237,7 +1197,7 @@ def serve(
1237
1197
  # Also send this to stderr
1238
1198
  if sys.stderr.isatty():
1239
1199
  console.print("\n[yellow]Server stopped by user[/yellow]")
1240
- if 'server' in locals():
1200
+ if "server" in locals():
1241
1201
  asyncio.run(server.stop())
1242
1202
  except Exception as e:
1243
1203
  # Log error to stderr
@@ -1251,13 +1211,10 @@ def mcp(
1251
1211
  False,
1252
1212
  "--global",
1253
1213
  "-g",
1254
- help="Configure Claude Desktop instead of project-level"
1214
+ help="Configure Claude Desktop instead of project-level",
1255
1215
  ),
1256
1216
  force: bool = typer.Option(
1257
- False,
1258
- "--force",
1259
- "-f",
1260
- help="Overwrite existing configuration"
1217
+ False, "--force", "-f", help="Overwrite existing configuration"
1261
1218
  ),
1262
1219
  ):
1263
1220
  """Configure Claude Code to use mcp-ticketer MCP server.
@@ -1283,4 +1240,4 @@ def main():
1283
1240
 
1284
1241
 
1285
1242
  if __name__ == "__main__":
1286
- main()
1243
+ main()