shotgun-sh 0.1.0.dev6__py3-none-any.whl → 0.1.0.dev8__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 shotgun-sh might be problematic. Click here for more details.

shotgun/__init__.py CHANGED
@@ -1,3 +1,5 @@
1
1
  """Shotgun CLI package."""
2
2
 
3
- __version__ = "0.1.0.dev2"
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("shotgun-sh")
@@ -1,6 +1,7 @@
1
1
  """Configuration manager for Shotgun CLI."""
2
2
 
3
3
  import json
4
+ import uuid
4
5
  from pathlib import Path
5
6
  from typing import Any
6
7
 
@@ -41,9 +42,11 @@ class ConfigManager:
41
42
 
42
43
  if not self.config_path.exists():
43
44
  logger.info(
44
- "Configuration file not found, using defaults: %s", self.config_path
45
+ "Configuration file not found, creating new config with user_id: %s",
46
+ self.config_path,
45
47
  )
46
- self._config = ShotgunConfig()
48
+ # Create new config with generated user_id
49
+ self._config = self.initialize()
47
50
  return self._config
48
51
 
49
52
  try:
@@ -61,8 +64,8 @@ class ConfigManager:
61
64
  logger.error(
62
65
  "Failed to load configuration from %s: %s", self.config_path, e
63
66
  )
64
- logger.info("Using default configuration")
65
- self._config = ShotgunConfig()
67
+ logger.info("Creating new configuration with generated user_id")
68
+ self._config = self.initialize()
66
69
  return self._config
67
70
 
68
71
  def save(self, config: ShotgunConfig | None = None) -> None:
@@ -72,7 +75,14 @@ class ConfigManager:
72
75
  config: Configuration to save. If None, saves current loaded config
73
76
  """
74
77
  if config is None:
75
- config = self._config or ShotgunConfig()
78
+ if self._config:
79
+ config = self._config
80
+ else:
81
+ # Create a new config with generated user_id
82
+ config = ShotgunConfig(
83
+ user_id=str(uuid.uuid4()),
84
+ config_version=1,
85
+ )
76
86
 
77
87
  # Ensure directory exists
78
88
  self.config_path.parent.mkdir(parents=True, exist_ok=True)
@@ -150,9 +160,17 @@ class ConfigManager:
150
160
  Returns:
151
161
  Default ShotgunConfig
152
162
  """
153
- config = ShotgunConfig()
163
+ # Generate unique user ID for new config
164
+ config = ShotgunConfig(
165
+ user_id=str(uuid.uuid4()),
166
+ config_version=1,
167
+ )
154
168
  self.save(config)
155
- logger.info("Configuration initialized at %s", self.config_path)
169
+ logger.info(
170
+ "Configuration initialized at %s with user_id: %s",
171
+ self.config_path,
172
+ config.user_id,
173
+ )
156
174
  return config
157
175
 
158
176
  def _convert_secrets_to_secretstr(self, data: dict[str, Any]) -> None:
@@ -209,6 +227,15 @@ class ConfigManager:
209
227
 
210
228
  return bool(value.strip())
211
229
 
230
+ def get_user_id(self) -> str:
231
+ """Get the user ID from configuration.
232
+
233
+ Returns:
234
+ The unique user ID string
235
+ """
236
+ config = self.load()
237
+ return config.user_id
238
+
212
239
 
213
240
  def get_config_manager() -> ConfigManager:
214
241
  """Get the global ConfigManager instance."""
@@ -123,3 +123,5 @@ class ShotgunConfig(BaseModel):
123
123
  default_provider: ProviderType = Field(
124
124
  default=ProviderType.OPENAI, description="Default AI provider to use"
125
125
  )
126
+ user_id: str = Field(description="Unique anonymous user identifier")
127
+ config_version: int = Field(default=1, description="Configuration schema version")
@@ -0,0 +1,20 @@
1
+ """Build-time constants generated during packaging.
2
+
3
+ This file is auto-generated during the build process.
4
+ DO NOT EDIT MANUALLY.
5
+ """
6
+
7
+ # Sentry DSN embedded at build time (empty string if not provided)
8
+ SENTRY_DSN = 'https://2818a6d165c64eccc94cfd51ce05d6aa@o4506813296738304.ingest.us.sentry.io/4510045952409600'
9
+
10
+ # PostHog configuration embedded at build time (empty strings if not provided)
11
+ POSTHOG_API_KEY = ''
12
+ POSTHOG_PROJECT_ID = '191396'
13
+
14
+ # Logfire configuration embedded at build time (only for dev builds)
15
+ LOGFIRE_ENABLED = 'true'
16
+ LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
17
+
18
+ # Build metadata
19
+ BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
20
+ IS_DEV_BUILD = True
shotgun/cli/config.py CHANGED
@@ -259,3 +259,17 @@ def _mask_value(value: str) -> str:
259
259
  if len(value) <= 8:
260
260
  return "••••••••"
261
261
  return f"{value[:4]}{'•' * (len(value) - 8)}{value[-4:]}"
262
+
263
+
264
+ @app.command()
265
+ def get_user_id() -> None:
266
+ """Get the anonymous user ID from configuration."""
267
+ config_manager = get_config_manager()
268
+
269
+ try:
270
+ user_id = config_manager.get_user_id()
271
+ console.print(f"[green]User ID:[/green] {user_id}")
272
+ except Exception as e:
273
+ logger.error(f"Error getting user ID: {e}")
274
+ console.print(f"❌ Failed to get user ID: {str(e)}", style="red")
275
+ raise typer.Exit(1) from e
shotgun/cli/plan.py CHANGED
@@ -38,6 +38,17 @@ def plan(
38
38
  logger.info("📋 Planning Goal: %s", goal)
39
39
 
40
40
  try:
41
+ # Track plan command usage
42
+ from shotgun.posthog_telemetry import track_event
43
+
44
+ track_event(
45
+ "plan_command",
46
+ {
47
+ "non_interactive": non_interactive,
48
+ "provider": provider.value if provider else "default",
49
+ },
50
+ )
51
+
41
52
  # Create agent dependencies
42
53
  agent_runtime_options = AgentRuntimeOptions(
43
54
  interactive_mode=not non_interactive
shotgun/cli/research.py CHANGED
@@ -58,6 +58,17 @@ async def async_research(
58
58
  provider: ProviderType | None = None,
59
59
  ) -> None:
60
60
  """Async wrapper for research process."""
61
+ # Track research command usage
62
+ from shotgun.posthog_telemetry import track_event
63
+
64
+ track_event(
65
+ "research_command",
66
+ {
67
+ "non_interactive": non_interactive,
68
+ "provider": provider.value if provider else "default",
69
+ },
70
+ )
71
+
61
72
  # Create agent dependencies
62
73
  agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
63
74
 
shotgun/cli/tasks.py CHANGED
@@ -44,6 +44,17 @@ def tasks(
44
44
  logger.info("📋 Task Creation Instruction: %s", instruction)
45
45
 
46
46
  try:
47
+ # Track tasks command usage
48
+ from shotgun.posthog_telemetry import track_event
49
+
50
+ track_event(
51
+ "tasks_command",
52
+ {
53
+ "non_interactive": non_interactive,
54
+ "provider": provider.value if provider else "default",
55
+ },
56
+ )
57
+
47
58
  # Create agent dependencies
48
59
  agent_runtime_options = AgentRuntimeOptions(
49
60
  interactive_mode=not non_interactive
shotgun/cli/update.py ADDED
@@ -0,0 +1,152 @@
1
+ """Update command for shotgun CLI."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+
9
+ from shotgun.logging_config import get_logger
10
+ from shotgun.utils.update_checker import (
11
+ detect_installation_method,
12
+ get_latest_version,
13
+ is_dev_version,
14
+ perform_update,
15
+ )
16
+
17
+ logger = get_logger(__name__)
18
+ console = Console()
19
+ app = typer.Typer()
20
+
21
+
22
+ @app.callback(invoke_without_command=True)
23
+ def update(
24
+ ctx: typer.Context,
25
+ force: Annotated[
26
+ bool,
27
+ typer.Option(
28
+ "--force",
29
+ "-f",
30
+ help="Force update even for development versions",
31
+ ),
32
+ ] = False,
33
+ check_only: Annotated[
34
+ bool,
35
+ typer.Option(
36
+ "--check",
37
+ "-c",
38
+ help="Check for updates without installing",
39
+ ),
40
+ ] = False,
41
+ ) -> None:
42
+ """Check for and install updates to shotgun-sh.
43
+
44
+ This command will:
45
+ - Check PyPI for the latest version
46
+ - Detect your installation method (pipx, pip, or venv)
47
+ - Perform the appropriate upgrade command
48
+
49
+ Examples:
50
+ shotgun update # Check and install updates
51
+ shotgun update --check # Only check for updates
52
+ shotgun update --force # Force update (even for dev versions)
53
+ """
54
+ if ctx.resilient_parsing:
55
+ return
56
+
57
+ # Handle check-only mode
58
+ if check_only:
59
+ with Progress(
60
+ SpinnerColumn(),
61
+ TextColumn("[progress.description]{task.description}"),
62
+ console=console,
63
+ ) as progress:
64
+ progress.add_task("Checking for updates...", total=None)
65
+
66
+ latest = get_latest_version()
67
+ if not latest:
68
+ console.print(
69
+ "[red]✗[/red] Failed to check for updates", style="bold red"
70
+ )
71
+ raise typer.Exit(1)
72
+
73
+ from shotgun import __version__
74
+ from shotgun.utils.update_checker import compare_versions
75
+
76
+ if compare_versions(__version__, latest):
77
+ console.print(
78
+ f"[green]✓[/green] Update available: [cyan]{__version__}[/cyan] → [green]{latest}[/green]",
79
+ style="bold",
80
+ )
81
+ console.print("Run 'shotgun update' to install the update")
82
+ else:
83
+ console.print(
84
+ f"[green]✓[/green] You're on the latest version ([cyan]{__version__}[/cyan])",
85
+ style="bold",
86
+ )
87
+ return
88
+
89
+ # Check for dev version
90
+ if is_dev_version() and not force:
91
+ console.print(
92
+ "[yellow]⚠[/yellow] You're running a development version",
93
+ style="bold yellow",
94
+ )
95
+ console.print(
96
+ "Use --force to update anyway, or install the stable version with:\n"
97
+ " pipx install shotgun-sh\n"
98
+ " or\n"
99
+ " pip install shotgun-sh",
100
+ )
101
+ raise typer.Exit(1)
102
+
103
+ # Confirm if forcing dev version update
104
+ if is_dev_version() and force:
105
+ confirm = typer.confirm(
106
+ "⚠️ You're about to replace a development version. Continue?",
107
+ default=False,
108
+ )
109
+ if not confirm:
110
+ console.print("Update cancelled", style="dim")
111
+ raise typer.Exit(0)
112
+
113
+ # Detect installation method
114
+ method = detect_installation_method()
115
+ console.print(f"Installation method: [cyan]{method}[/cyan]", style="dim")
116
+
117
+ # Perform update
118
+ with Progress(
119
+ SpinnerColumn(),
120
+ TextColumn("[progress.description]{task.description}"),
121
+ console=console,
122
+ ) as progress:
123
+ task = progress.add_task("Updating shotgun-sh...", total=None)
124
+
125
+ success, message = perform_update(force=force)
126
+
127
+ if success:
128
+ progress.update(task, description="[green]✓[/green] Update complete!")
129
+ console.print(f"\n[green]✓[/green] {message}", style="bold green")
130
+ console.print(
131
+ "\n[dim]Restart your terminal or run 'shotgun --version' to verify the update[/dim]"
132
+ )
133
+ else:
134
+ progress.update(task, description="[red]✗[/red] Update failed")
135
+ console.print(f"\n[red]✗[/red] {message}", style="bold red")
136
+
137
+ # Provide manual update instructions
138
+ if method == "pipx":
139
+ console.print(
140
+ "\n[yellow]Try updating manually:[/yellow]\n"
141
+ " pipx upgrade shotgun-sh"
142
+ )
143
+ else:
144
+ console.print(
145
+ "\n[yellow]Try updating manually:[/yellow]\n"
146
+ " pip install --upgrade shotgun-sh"
147
+ )
148
+ raise typer.Exit(1)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ app()
shotgun/main.py CHANGED
@@ -5,11 +5,15 @@ from typing import Annotated
5
5
  import typer
6
6
  from dotenv import load_dotenv
7
7
 
8
+ from shotgun import __version__
8
9
  from shotgun.agents.config import get_config_manager
9
- from shotgun.cli import codebase, config, plan, research, tasks
10
+ from shotgun.cli import codebase, config, plan, research, tasks, update
10
11
  from shotgun.logging_config import configure_root_logger, get_logger
12
+ from shotgun.posthog_telemetry import setup_posthog_observability
13
+ from shotgun.sentry_telemetry import setup_sentry_observability
11
14
  from shotgun.telemetry import setup_logfire_observability
12
15
  from shotgun.tui import app as tui_app
16
+ from shotgun.utils.update_checker import check_for_updates_async
13
17
 
14
18
  # Load environment variables from .env file
15
19
  load_dotenv()
@@ -29,6 +33,24 @@ except Exception as e:
29
33
  _logfire_enabled = setup_logfire_observability()
30
34
  logger.debug("Logfire observability enabled: %s", _logfire_enabled)
31
35
 
36
+ # Initialize Sentry telemetry
37
+ _sentry_enabled = setup_sentry_observability()
38
+ logger.debug("Sentry observability enabled: %s", _sentry_enabled)
39
+
40
+ # Initialize PostHog analytics
41
+ _posthog_enabled = setup_posthog_observability()
42
+ logger.debug("PostHog analytics enabled: %s", _posthog_enabled)
43
+
44
+ # Global variable to store update notification
45
+ _update_notification: str | None = None
46
+
47
+
48
+ def _update_callback(notification: str) -> None:
49
+ """Callback to store update notification."""
50
+ global _update_notification
51
+ _update_notification = notification
52
+
53
+
32
54
  app = typer.Typer(
33
55
  name="shotgun",
34
56
  help="Shotgun - AI-powered CLI tool for research, planning, and task management",
@@ -43,12 +65,16 @@ app.add_typer(
43
65
  app.add_typer(research.app, name="research", help="Perform research with agentic loops")
44
66
  app.add_typer(plan.app, name="plan", help="Generate structured plans")
45
67
  app.add_typer(tasks.app, name="tasks", help="Generate task lists with agentic approach")
68
+ app.add_typer(update.app, name="update", help="Check for and install updates")
46
69
 
47
70
 
48
71
  def version_callback(value: bool) -> None:
49
72
  """Show version and exit."""
50
73
  if value:
51
- logger.info("shotgun 0.1.0")
74
+ from rich.console import Console
75
+
76
+ console = Console()
77
+ console.print(f"shotgun {__version__}")
52
78
  raise typer.Exit()
53
79
 
54
80
 
@@ -65,14 +91,58 @@ def main(
65
91
  help="Show version and exit",
66
92
  ),
67
93
  ] = False,
94
+ no_update_check: Annotated[
95
+ bool,
96
+ typer.Option(
97
+ "--no-update-check",
98
+ help="Disable automatic update checks",
99
+ ),
100
+ ] = False,
68
101
  ) -> None:
69
102
  """Shotgun - AI-powered CLI tool."""
70
103
  logger.debug("Starting shotgun CLI application")
104
+
105
+ # Start async update check (non-blocking)
106
+ if not ctx.resilient_parsing:
107
+ check_for_updates_async(
108
+ callback=_update_callback, no_update_check=no_update_check
109
+ )
110
+
71
111
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
72
112
  logger.debug("Launching shotgun TUI application")
73
- tui_app.run()
113
+ tui_app.run(no_update_check=no_update_check)
114
+
115
+ # Show update notification after TUI exits
116
+ if _update_notification:
117
+ from rich.console import Console
118
+
119
+ console = Console()
120
+ console.print(f"\n[cyan]{_update_notification}[/cyan]", style="bold")
121
+
74
122
  raise typer.Exit()
75
123
 
124
+ # For CLI commands, we'll show notification at the end
125
+ # This is handled by registering an atexit handler
126
+ if not ctx.resilient_parsing and ctx.invoked_subcommand is not None:
127
+ import atexit
128
+
129
+ def show_update_notification() -> None:
130
+ if _update_notification:
131
+ from rich.console import Console
132
+
133
+ console = Console()
134
+ console.print(f"\n[cyan]{_update_notification}[/cyan]", style="bold")
135
+
136
+ atexit.register(show_update_notification)
137
+
138
+ # Register PostHog shutdown handler
139
+ def shutdown_posthog() -> None:
140
+ from shotgun.posthog_telemetry import shutdown
141
+
142
+ shutdown()
143
+
144
+ atexit.register(shutdown_posthog)
145
+
76
146
 
77
147
  if __name__ == "__main__":
78
148
  app()
@@ -0,0 +1,156 @@
1
+ """PostHog analytics setup for Shotgun."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import Any
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # Global PostHog client instance
10
+ _posthog_client = None
11
+
12
+
13
+ def setup_posthog_observability() -> bool:
14
+ """Set up PostHog analytics for usage tracking.
15
+
16
+ Returns:
17
+ True if PostHog was successfully set up, False otherwise
18
+ """
19
+ global _posthog_client
20
+
21
+ try:
22
+ import posthog
23
+
24
+ # Check if PostHog is already initialized
25
+ if _posthog_client is not None:
26
+ logger.debug("PostHog is already initialized, skipping")
27
+ return True
28
+
29
+ # Try to get API key from build constants first (production builds)
30
+ api_key = None
31
+
32
+ try:
33
+ from shotgun import build_constants
34
+
35
+ api_key = build_constants.POSTHOG_API_KEY
36
+ if api_key:
37
+ logger.debug("Using PostHog configuration from build constants")
38
+ except (ImportError, AttributeError):
39
+ pass
40
+
41
+ # Fallback to environment variables if build constants are empty or missing
42
+ if not api_key:
43
+ api_key = os.getenv("POSTHOG_API_KEY", "")
44
+ if api_key:
45
+ logger.debug("Using PostHog configuration from environment variables")
46
+
47
+ if not api_key:
48
+ logger.debug(
49
+ "No PostHog API key configured, skipping PostHog initialization"
50
+ )
51
+ return False
52
+
53
+ logger.debug("Found PostHog configuration, proceeding with setup")
54
+
55
+ # Get version for context
56
+ from shotgun import __version__
57
+
58
+ # Determine environment based on version
59
+ # Dev versions contain "dev", "rc", "alpha", or "beta"
60
+ if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
61
+ environment = "development"
62
+ else:
63
+ environment = "production"
64
+
65
+ # Initialize PostHog client
66
+ posthog.api_key = api_key
67
+ posthog.host = "https://us.i.posthog.com" # Use US cloud instance
68
+
69
+ # Store the client for later use
70
+ _posthog_client = posthog
71
+
72
+ # Set user context with anonymous user ID from config
73
+ try:
74
+ from shotgun.agents.config import get_config_manager
75
+
76
+ config_manager = get_config_manager()
77
+ user_id = config_manager.get_user_id()
78
+
79
+ # Set default properties for all events
80
+ posthog.disabled = False
81
+ posthog.personal_api_key = None # Not needed for event tracking
82
+
83
+ logger.debug(
84
+ "PostHog user context will be set with anonymous ID: %s", user_id
85
+ )
86
+ except Exception as e:
87
+ logger.warning("Failed to get user context: %s", e)
88
+
89
+ logger.debug(
90
+ "PostHog analytics configured successfully (environment: %s, version: %s)",
91
+ environment,
92
+ __version__,
93
+ )
94
+ return True
95
+
96
+ except ImportError as e:
97
+ logger.error("PostHog SDK not available: %s", e)
98
+ return False
99
+ except Exception as e:
100
+ logger.warning("Failed to setup PostHog analytics: %s", e)
101
+ return False
102
+
103
+
104
+ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> None:
105
+ """Track an event in PostHog.
106
+
107
+ Args:
108
+ event_name: Name of the event to track
109
+ properties: Optional properties to include with the event
110
+ """
111
+ global _posthog_client
112
+
113
+ if _posthog_client is None:
114
+ logger.debug("PostHog not initialized, skipping event: %s", event_name)
115
+ return
116
+
117
+ try:
118
+ from shotgun import __version__
119
+ from shotgun.agents.config import get_config_manager
120
+
121
+ # Get user ID for tracking
122
+ config_manager = get_config_manager()
123
+ user_id = config_manager.get_user_id()
124
+
125
+ # Add version and environment to properties
126
+ if properties is None:
127
+ properties = {}
128
+ properties["version"] = __version__
129
+
130
+ # Determine environment
131
+ if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
132
+ properties["environment"] = "development"
133
+ else:
134
+ properties["environment"] = "production"
135
+
136
+ # Track the event using PostHog's capture method
137
+ _posthog_client.capture(
138
+ distinct_id=user_id, event=event_name, properties=properties
139
+ )
140
+ logger.debug("Tracked PostHog event: %s", event_name)
141
+ except Exception as e:
142
+ logger.warning("Failed to track PostHog event '%s': %s", event_name, e)
143
+
144
+
145
+ def shutdown() -> None:
146
+ """Shutdown PostHog client and flush any pending events."""
147
+ global _posthog_client
148
+
149
+ if _posthog_client is not None:
150
+ try:
151
+ _posthog_client.shutdown()
152
+ logger.debug("PostHog client shutdown successfully")
153
+ except Exception as e:
154
+ logger.warning("Error shutting down PostHog: %s", e)
155
+ finally:
156
+ _posthog_client = None