mcp-ticketer 0.4.11__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 (70) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +9 -3
  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 +313 -96
  10. mcp_ticketer/adapters/jira.py +251 -1
  11. mcp_ticketer/adapters/linear/adapter.py +524 -22
  12. mcp_ticketer/adapters/linear/client.py +61 -9
  13. mcp_ticketer/adapters/linear/mappers.py +9 -3
  14. mcp_ticketer/cache/memory.py +3 -3
  15. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  16. mcp_ticketer/cli/auggie_configure.py +1 -1
  17. mcp_ticketer/cli/codex_configure.py +80 -1
  18. mcp_ticketer/cli/configure.py +33 -43
  19. mcp_ticketer/cli/diagnostics.py +18 -16
  20. mcp_ticketer/cli/discover.py +288 -21
  21. mcp_ticketer/cli/gemini_configure.py +1 -1
  22. mcp_ticketer/cli/instruction_commands.py +429 -0
  23. mcp_ticketer/cli/linear_commands.py +99 -15
  24. mcp_ticketer/cli/main.py +1199 -227
  25. mcp_ticketer/cli/mcp_configure.py +1 -1
  26. mcp_ticketer/cli/migrate_config.py +12 -8
  27. mcp_ticketer/cli/platform_commands.py +6 -6
  28. mcp_ticketer/cli/platform_detection.py +412 -0
  29. mcp_ticketer/cli/queue_commands.py +15 -15
  30. mcp_ticketer/cli/simple_health.py +1 -1
  31. mcp_ticketer/cli/ticket_commands.py +14 -13
  32. mcp_ticketer/cli/update_checker.py +313 -0
  33. mcp_ticketer/cli/utils.py +45 -41
  34. mcp_ticketer/core/__init__.py +12 -0
  35. mcp_ticketer/core/adapter.py +4 -4
  36. mcp_ticketer/core/config.py +17 -10
  37. mcp_ticketer/core/env_discovery.py +33 -3
  38. mcp_ticketer/core/env_loader.py +7 -6
  39. mcp_ticketer/core/exceptions.py +3 -3
  40. mcp_ticketer/core/http_client.py +10 -10
  41. mcp_ticketer/core/instructions.py +405 -0
  42. mcp_ticketer/core/mappers.py +1 -1
  43. mcp_ticketer/core/models.py +1 -1
  44. mcp_ticketer/core/onepassword_secrets.py +379 -0
  45. mcp_ticketer/core/project_config.py +17 -1
  46. mcp_ticketer/core/registry.py +1 -1
  47. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  48. mcp_ticketer/mcp/__init__.py +2 -2
  49. mcp_ticketer/mcp/server/__init__.py +2 -2
  50. mcp_ticketer/mcp/server/main.py +82 -69
  51. mcp_ticketer/mcp/server/tools/__init__.py +9 -0
  52. mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
  53. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  54. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
  55. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  56. mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
  57. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  58. mcp_ticketer/queue/health_monitor.py +1 -0
  59. mcp_ticketer/queue/manager.py +4 -4
  60. mcp_ticketer/queue/queue.py +3 -3
  61. mcp_ticketer/queue/run_worker.py +1 -1
  62. mcp_ticketer/queue/ticket_registry.py +2 -2
  63. mcp_ticketer/queue/worker.py +14 -12
  64. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
  65. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  66. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  67. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  68. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  69. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  70. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -4,8 +4,15 @@ from pathlib import Path
4
4
 
5
5
  import typer
6
6
  from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
7
9
 
8
10
  from ..core.env_discovery import DiscoveredAdapter, EnvDiscovery
11
+ from ..core.onepassword_secrets import (
12
+ OnePasswordConfig,
13
+ OnePasswordSecretsLoader,
14
+ check_op_cli_status,
15
+ )
9
16
  from ..core.project_config import (
10
17
  AdapterConfig,
11
18
  ConfigResolver,
@@ -181,7 +188,7 @@ def save(
181
188
  console.print(
182
189
  "[dim]Make sure your .env file contains adapter credentials[/dim]"
183
190
  )
184
- raise typer.Exit(1)
191
+ raise typer.Exit(1) from None
185
192
 
186
193
  # Determine which adapter to save
187
194
  if adapter:
@@ -191,13 +198,13 @@ def save(
191
198
  console.print(
192
199
  f"[dim]Available: {', '.join(a.adapter_type for a in result.adapters)}[/dim]"
193
200
  )
194
- raise typer.Exit(1)
201
+ raise typer.Exit(1) from None
195
202
  else:
196
203
  # Use recommended adapter
197
204
  discovered_adapter = result.get_primary_adapter()
198
205
  if not discovered_adapter:
199
206
  console.print("[red]Could not determine recommended adapter[/red]")
200
- raise typer.Exit(1)
207
+ raise typer.Exit(1) from None
201
208
 
202
209
  console.print(
203
210
  f"[bold]Using recommended adapter:[/bold] {discovered_adapter.adapter_type}"
@@ -216,7 +223,7 @@ def save(
216
223
  console.print(
217
224
  "[dim]Fix the configuration in your .env file and try again[/dim]"
218
225
  )
219
- raise typer.Exit(1)
226
+ raise typer.Exit(1) from None
220
227
 
221
228
  if dry_run:
222
229
  console.print("\n[yellow]Dry run - no changes made[/yellow]")
@@ -239,14 +246,15 @@ def save(
239
246
  # Add to config
240
247
  config.adapters[discovered_adapter.adapter_type] = adapter_config
241
248
 
242
- # Save
249
+ # Save (always to project config for security)
243
250
  try:
251
+ resolver.save_project_config(config, proj_path)
252
+ config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
253
+
244
254
  if global_config:
245
- resolver.save_global_config(config)
246
- config_location = resolver.GLOBAL_CONFIG_PATH
247
- else:
248
- resolver.save_project_config(config, proj_path)
249
- config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
255
+ console.print(
256
+ "[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
257
+ )
250
258
 
251
259
  console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
252
260
  console.print(
@@ -255,7 +263,7 @@ def save(
255
263
 
256
264
  except Exception as e:
257
265
  console.print(f"\n[red]Failed to save configuration:[/red] {e}")
258
- raise typer.Exit(1)
266
+ raise typer.Exit(1) from None
259
267
 
260
268
 
261
269
  @app.command()
@@ -283,7 +291,7 @@ def interactive(
283
291
  console.print(f" ✅ {env_file}")
284
292
  else:
285
293
  console.print("[red]No .env files found[/red]")
286
- raise typer.Exit(1)
294
+ raise typer.Exit(1) from None
287
295
 
288
296
  # Show discovered adapters
289
297
  if not result.adapters:
@@ -291,7 +299,7 @@ def interactive(
291
299
  console.print(
292
300
  "[dim]Make sure your .env file contains adapter credentials[/dim]"
293
301
  )
294
- raise typer.Exit(1)
302
+ raise typer.Exit(1) from None
295
303
 
296
304
  console.print("\n[bold]Detected adapter configurations:[/bold]")
297
305
  for i, adapter in enumerate(result.adapters, 1):
@@ -331,7 +339,7 @@ def interactive(
331
339
  if choice in [1, 2]:
332
340
  if not primary:
333
341
  console.print("[red]No recommended adapter found[/red]")
334
- raise typer.Exit(1)
342
+ raise typer.Exit(1) from None
335
343
  adapters_to_save = [primary]
336
344
  default_adapter = primary.adapter_type
337
345
  elif choice == 3:
@@ -347,7 +355,7 @@ def interactive(
347
355
  default_adapter = selected.adapter_type
348
356
  else:
349
357
  console.print("[red]Invalid choice[/red]")
350
- raise typer.Exit(1)
358
+ raise typer.Exit(1) from None
351
359
  else: # choice == 4
352
360
  adapters_to_save = result.adapters
353
361
  default_adapter = (
@@ -388,22 +396,281 @@ def interactive(
388
396
 
389
397
  console.print(f" ✅ Added {discovered_adapter.adapter_type}")
390
398
 
391
- # Save
399
+ # Save (always to project config for security)
392
400
  try:
401
+ resolver.save_project_config(config, proj_path)
402
+ config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
403
+
393
404
  if save_global:
394
- resolver.save_global_config(config)
395
- config_location = resolver.GLOBAL_CONFIG_PATH
396
- else:
397
- resolver.save_project_config(config, proj_path)
398
- config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
405
+ console.print(
406
+ "[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
407
+ )
399
408
 
400
409
  console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
401
410
  console.print(f"[green]✅ Default adapter:[/green] {config.default_adapter}")
402
411
 
403
412
  except Exception as e:
404
413
  console.print(f"\n[red]Failed to save configuration:[/red] {e}")
414
+ raise typer.Exit(1) from None
415
+
416
+
417
+ @app.command(name="1password-status")
418
+ def onepassword_status() -> None:
419
+ """Check 1Password CLI installation and authentication status."""
420
+ console.print(
421
+ Panel.fit(
422
+ "[bold cyan]1Password CLI Status[/bold cyan]\n"
423
+ "Checking 1Password integration...",
424
+ border_style="cyan",
425
+ )
426
+ )
427
+
428
+ status = check_op_cli_status()
429
+
430
+ # Create status table
431
+ table = Table(title="1Password CLI Status")
432
+ table.add_column("Component", style="cyan")
433
+ table.add_column("Status", style="white")
434
+
435
+ # CLI installed
436
+ if status["installed"]:
437
+ table.add_row(
438
+ "CLI Installed", f"[green]✓ Yes[/green] (version {status['version']})"
439
+ )
440
+ else:
441
+ table.add_row("CLI Installed", "[red]✗ No[/red]")
442
+ console.print(table)
443
+ console.print(
444
+ "\n[yellow]Install 1Password CLI:[/yellow]\n"
445
+ " macOS: brew install 1password-cli\n"
446
+ " Linux: See https://developer.1password.com/docs/cli/get-started/\n"
447
+ " Windows: See https://developer.1password.com/docs/cli/get-started/"
448
+ )
449
+ return
450
+
451
+ # Authentication
452
+ if status["authenticated"]:
453
+ table.add_row("Authentication", "[green]✓ Signed in[/green]")
454
+
455
+ # Show accounts
456
+ if status["accounts"]:
457
+ for account in status["accounts"]:
458
+ account_url = account.get("url", "N/A")
459
+ account_email = account.get("email", "N/A")
460
+ table.add_row(" Account", f"{account_email} ({account_url})")
461
+ else:
462
+ table.add_row("Authentication", "[yellow]⚠ Not signed in[/yellow]")
463
+
464
+ console.print(table)
465
+
466
+ if not status["authenticated"]:
467
+ console.print("\n[yellow]Sign in to 1Password:[/yellow]\n" " Run: op signin\n")
468
+ else:
469
+ console.print(
470
+ "\n[green]✓ 1Password CLI is ready to use![/green]\n\n"
471
+ "You can now use .env files with op:// secret references.\n"
472
+ "Run 'mcp-ticketer discover 1password-template' to create template files."
473
+ )
474
+
475
+
476
+ @app.command(name="1password-template")
477
+ def onepassword_template(
478
+ adapter: str = typer.Argument(
479
+ ...,
480
+ help="Adapter type (linear, github, jira, aitrackdown)",
481
+ ),
482
+ vault: str = typer.Option(
483
+ "Development",
484
+ "--vault",
485
+ "-v",
486
+ help="1Password vault name for secret references",
487
+ ),
488
+ item: str | None = typer.Option(
489
+ None,
490
+ "--item",
491
+ "-i",
492
+ help="1Password item name (defaults to adapter name)",
493
+ ),
494
+ output: Path | None = typer.Option(
495
+ None,
496
+ "--output",
497
+ "-o",
498
+ help="Output file path (defaults to .env.1password)",
499
+ ),
500
+ ) -> None:
501
+ """Create a .env template file with 1Password secret references.
502
+
503
+ This creates a template file that uses op:// secret references,
504
+ which can be used with: op run --env-file=.env.1password -- <command>
505
+
506
+ Examples:
507
+ # Create Linear template
508
+ mcp-ticketer discover 1password-template linear
509
+
510
+ # Create GitHub template with custom vault
511
+ mcp-ticketer discover 1password-template github --vault=Production
512
+
513
+ # Create template with custom item name
514
+ mcp-ticketer discover 1password-template jira --item="JIRA API Keys"
515
+
516
+ """
517
+ # Check if op CLI is available
518
+ status = check_op_cli_status()
519
+ if not status["installed"]:
520
+ console.print(
521
+ "[red]1Password CLI not installed.[/red]\n\n"
522
+ "Install it first:\n"
523
+ " macOS: brew install 1password-cli\n"
524
+ " Other: https://developer.1password.com/docs/cli/get-started/"
525
+ )
405
526
  raise typer.Exit(1)
406
527
 
528
+ # Set default output path
529
+ if output is None:
530
+ output = Path(f".env.1password.{adapter.lower()}")
531
+
532
+ # Create loader and generate template
533
+ loader = OnePasswordSecretsLoader(OnePasswordConfig())
534
+ loader.create_template_file(output, adapter, vault, item)
535
+
536
+ console.print(
537
+ Panel.fit(
538
+ f"[bold green]✓ Template created![/bold green]\n\n"
539
+ f"File: {output}\n"
540
+ f"Vault: {vault}\n"
541
+ f"Item: {item or adapter.upper()}\n\n"
542
+ f"[bold]Next steps:[/bold]\n"
543
+ f"1. Create item '{item or adapter.upper()}' in 1Password vault '{vault}'\n"
544
+ f"2. Add the required fields to the item\n"
545
+ f"3. Test with: op run --env-file={output} -- mcp-ticketer discover show\n"
546
+ f"4. Save config: op run --env-file={output} -- mcp-ticketer discover save",
547
+ border_style="green",
548
+ )
549
+ )
550
+
551
+ # Show template contents
552
+ console.print("\n[bold]Template contents:[/bold]\n")
553
+ console.print(Panel(output.read_text(), border_style="dim"))
554
+
555
+
556
+ @app.command(name="1password-test")
557
+ def onepassword_test(
558
+ env_file: Path = typer.Option(
559
+ ".env.1password",
560
+ "--file",
561
+ "-f",
562
+ help="Path to .env file with op:// references",
563
+ ),
564
+ ) -> None:
565
+ """Test 1Password secret resolution from .env file.
566
+
567
+ This command loads secrets from the specified .env file and
568
+ displays the resolved values (with sensitive data masked).
569
+
570
+ Example:
571
+ mcp-ticketer discover 1password-test --file=.env.1password.linear
572
+
573
+ """
574
+ # Check if file exists
575
+ if not env_file.exists():
576
+ console.print(f"[red]File not found:[/red] {env_file}")
577
+ raise typer.Exit(1)
578
+
579
+ # Check if op CLI is available and authenticated
580
+ status = check_op_cli_status()
581
+ if not status["installed"]:
582
+ console.print("[red]1Password CLI not installed.[/red]")
583
+ raise typer.Exit(1)
584
+
585
+ if not status["authenticated"]:
586
+ console.print(
587
+ "[red]1Password CLI not authenticated.[/red]\n\n" "Run: op signin"
588
+ )
589
+ raise typer.Exit(1)
590
+
591
+ console.print(
592
+ Panel.fit(
593
+ f"[bold cyan]Testing 1Password Secret Resolution[/bold cyan]\n"
594
+ f"File: {env_file}",
595
+ border_style="cyan",
596
+ )
597
+ )
598
+
599
+ # Load secrets
600
+ loader = OnePasswordSecretsLoader(OnePasswordConfig())
601
+
602
+ try:
603
+ secrets = loader.load_secrets_from_env_file(env_file)
604
+
605
+ # Display resolved secrets
606
+ table = Table(title="Resolved Secrets")
607
+ table.add_column("Variable", style="cyan")
608
+ table.add_column("Value", style="green")
609
+
610
+ for key, value in secrets.items():
611
+ # Mask sensitive values
612
+ display_value = _mask_sensitive(value, key)
613
+ table.add_row(key, display_value)
614
+
615
+ console.print(table)
616
+
617
+ console.print(
618
+ f"\n[green]✓ Successfully resolved {len(secrets)} secrets![/green]"
619
+ )
620
+
621
+ # Test discovery with these secrets
622
+ console.print("\n[bold]Testing configuration discovery...[/bold]")
623
+ EnvDiscovery(enable_1password=False) # Already resolved
624
+
625
+ # Temporarily write resolved secrets to test discovery
626
+ import tempfile
627
+
628
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as tmp:
629
+ for key, value in secrets.items():
630
+ tmp.write(f"{key}={value}\n")
631
+ tmp_path = Path(tmp.name)
632
+
633
+ try:
634
+ # Mock the env file loading by directly providing secrets
635
+ from ..core.env_discovery import DiscoveryResult
636
+
637
+ DiscoveryResult()
638
+
639
+ # Try to detect adapters from the resolved secrets
640
+ from ..core.env_discovery import EnvDiscovery as ED
641
+
642
+ ed = ED(enable_1password=False)
643
+ ed.project_path = Path.cwd()
644
+
645
+ # Manually detect from secrets dict
646
+ linear_adapter = ed._detect_linear(secrets, str(env_file))
647
+ if linear_adapter:
648
+ console.print("\n[green]✓ Detected Linear configuration[/green]")
649
+ _display_discovered_adapter(linear_adapter, ed)
650
+
651
+ github_adapter = ed._detect_github(secrets, str(env_file))
652
+ if github_adapter:
653
+ console.print("\n[green]✓ Detected GitHub configuration[/green]")
654
+ _display_discovered_adapter(github_adapter, ed)
655
+
656
+ jira_adapter = ed._detect_jira(secrets, str(env_file))
657
+ if jira_adapter:
658
+ console.print("\n[green]✓ Detected JIRA configuration[/green]")
659
+ _display_discovered_adapter(jira_adapter, ed)
660
+ finally:
661
+ tmp_path.unlink()
662
+
663
+ except Exception as e:
664
+ console.print(f"\n[red]Failed to resolve secrets:[/red] {e}")
665
+ console.print(
666
+ "\n[yellow]Troubleshooting:[/yellow]\n"
667
+ "1. Check that the item exists in 1Password\n"
668
+ "2. Verify the vault name is correct\n"
669
+ "3. Ensure all field names match\n"
670
+ f"4. Run: op inject --in-file={env_file} (to see detailed errors)"
671
+ )
672
+ raise typer.Exit(1) from None
673
+
407
674
 
408
675
  if __name__ == "__main__":
409
676
  app()
@@ -246,7 +246,7 @@ def configure_gemini_mcp(
246
246
  "Could not find mcp-ticketer Python executable. "
247
247
  "Please ensure mcp-ticketer is installed.\n"
248
248
  "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
249
- )
249
+ ) from e
250
250
 
251
251
  # Step 2: Load project configuration
252
252
  console.print("\n[cyan]📖 Reading project configuration...[/cyan]")