shotgun-sh 0.2.11.dev7__py3-none-any.whl → 0.2.23.dev1__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.

Files changed (51) hide show
  1. shotgun/agents/agent_manager.py +25 -11
  2. shotgun/agents/config/README.md +89 -0
  3. shotgun/agents/config/__init__.py +10 -1
  4. shotgun/agents/config/manager.py +287 -32
  5. shotgun/agents/config/models.py +26 -1
  6. shotgun/agents/config/provider.py +27 -0
  7. shotgun/agents/config/streaming_test.py +119 -0
  8. shotgun/agents/error/__init__.py +11 -0
  9. shotgun/agents/error/models.py +19 -0
  10. shotgun/agents/history/token_counting/anthropic.py +8 -0
  11. shotgun/agents/runner.py +230 -0
  12. shotgun/build_constants.py +1 -1
  13. shotgun/cli/context.py +43 -0
  14. shotgun/cli/error_handler.py +24 -0
  15. shotgun/cli/export.py +34 -34
  16. shotgun/cli/plan.py +34 -34
  17. shotgun/cli/research.py +17 -9
  18. shotgun/cli/specify.py +20 -19
  19. shotgun/cli/tasks.py +34 -34
  20. shotgun/exceptions.py +323 -0
  21. shotgun/llm_proxy/__init__.py +17 -0
  22. shotgun/llm_proxy/client.py +215 -0
  23. shotgun/llm_proxy/models.py +137 -0
  24. shotgun/logging_config.py +42 -0
  25. shotgun/main.py +2 -0
  26. shotgun/posthog_telemetry.py +18 -25
  27. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
  28. shotgun/sdk/codebase.py +14 -3
  29. shotgun/sentry_telemetry.py +140 -2
  30. shotgun/settings.py +5 -0
  31. shotgun/tui/app.py +35 -10
  32. shotgun/tui/screens/chat/chat_screen.py +192 -91
  33. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +62 -11
  34. shotgun/tui/screens/chat_screen/command_providers.py +3 -2
  35. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  36. shotgun/tui/screens/chat_screen/history/chat_history.py +37 -2
  37. shotgun/tui/screens/directory_setup.py +45 -41
  38. shotgun/tui/screens/feedback.py +10 -3
  39. shotgun/tui/screens/github_issue.py +11 -2
  40. shotgun/tui/screens/model_picker.py +8 -1
  41. shotgun/tui/screens/pipx_migration.py +12 -6
  42. shotgun/tui/screens/provider_config.py +25 -8
  43. shotgun/tui/screens/shotgun_auth.py +0 -10
  44. shotgun/tui/screens/welcome.py +32 -0
  45. shotgun/tui/widgets/widget_coordinator.py +3 -2
  46. shotgun_sh-0.2.23.dev1.dist-info/METADATA +472 -0
  47. {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/RECORD +50 -42
  48. shotgun_sh-0.2.11.dev7.dist-info/METADATA +0 -130
  49. {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/WHEEL +0 -0
  50. {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/entry_points.txt +0 -0
  51. {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
1
  """Sentry observability setup for Shotgun."""
2
2
 
3
+ from pathlib import Path
3
4
  from typing import Any
4
5
 
5
6
  from shotgun import __version__
@@ -10,6 +11,122 @@ from shotgun.settings import settings
10
11
  logger = get_early_logger(__name__)
11
12
 
12
13
 
14
+ def _scrub_path(path: str) -> str:
15
+ """Scrub sensitive information from file paths.
16
+
17
+ Removes home directory and current working directory prefixes to prevent
18
+ leaking usernames that might be part of the path.
19
+
20
+ Args:
21
+ path: The file path to scrub
22
+
23
+ Returns:
24
+ The scrubbed path with sensitive prefixes removed
25
+ """
26
+ if not path:
27
+ return path
28
+
29
+ try:
30
+ # Get home and cwd as Path objects for comparison
31
+ home = Path.home()
32
+ cwd = Path.cwd()
33
+
34
+ # Convert path to Path object
35
+ path_obj = Path(path)
36
+
37
+ # Try to make path relative to cwd first (most common case)
38
+ try:
39
+ relative_to_cwd = path_obj.relative_to(cwd)
40
+ return str(relative_to_cwd)
41
+ except ValueError:
42
+ pass
43
+
44
+ # Try to replace home directory with ~
45
+ try:
46
+ relative_to_home = path_obj.relative_to(home)
47
+ return f"~/{relative_to_home}"
48
+ except ValueError:
49
+ pass
50
+
51
+ # If path is absolute but not under cwd or home, just return filename
52
+ if path_obj.is_absolute():
53
+ return path_obj.name
54
+
55
+ # Return as-is if already relative
56
+ return path
57
+
58
+ except Exception:
59
+ # If anything goes wrong, return the original path
60
+ # Better to leak a path than break error reporting
61
+ return path
62
+
63
+
64
+ def _scrub_sensitive_paths(event: dict[str, Any]) -> None:
65
+ """Scrub sensitive paths from Sentry event data.
66
+
67
+ Modifies the event in-place to remove:
68
+ - Home directory paths (might contain usernames)
69
+ - Current working directory paths (might contain usernames)
70
+ - Server name/hostname
71
+ - Paths in sys.argv
72
+
73
+ Args:
74
+ event: The Sentry event dictionary to scrub
75
+ """
76
+ extra = event.get("extra", {})
77
+ if "sys.argv" in extra:
78
+ argv = extra["sys.argv"]
79
+ if isinstance(argv, list):
80
+ extra["sys.argv"] = [
81
+ _scrub_path(arg) if isinstance(arg, str) else arg for arg in argv
82
+ ]
83
+
84
+ # Scrub server name if present
85
+ if "server_name" in event:
86
+ event["server_name"] = ""
87
+
88
+ # Scrub contexts that might contain paths
89
+ if "contexts" in event:
90
+ contexts = event["contexts"]
91
+ # Remove runtime context if it has CWD
92
+ if "runtime" in contexts:
93
+ if "cwd" in contexts["runtime"]:
94
+ del contexts["runtime"]["cwd"]
95
+ # Scrub sys.argv to remove paths
96
+ if "sys.argv" in contexts["runtime"]:
97
+ argv = contexts["runtime"]["sys.argv"]
98
+ if isinstance(argv, list):
99
+ contexts["runtime"]["sys.argv"] = [
100
+ _scrub_path(arg) if isinstance(arg, str) else arg
101
+ for arg in argv
102
+ ]
103
+
104
+ # Scrub exception stack traces
105
+ if "exception" in event and "values" in event["exception"]:
106
+ for exception in event["exception"]["values"]:
107
+ if "stacktrace" in exception and "frames" in exception["stacktrace"]:
108
+ for frame in exception["stacktrace"]["frames"]:
109
+ # Scrub file paths
110
+ if "abs_path" in frame:
111
+ frame["abs_path"] = _scrub_path(frame["abs_path"])
112
+ if "filename" in frame:
113
+ frame["filename"] = _scrub_path(frame["filename"])
114
+
115
+ # Scrub local variables that might contain paths
116
+ if "vars" in frame:
117
+ for var_name, var_value in frame["vars"].items():
118
+ if isinstance(var_value, str):
119
+ frame["vars"][var_name] = _scrub_path(var_value)
120
+
121
+ # Scrub breadcrumbs that might contain paths
122
+ if "breadcrumbs" in event and "values" in event["breadcrumbs"]:
123
+ for breadcrumb in event["breadcrumbs"]["values"]:
124
+ if "data" in breadcrumb:
125
+ for key, value in breadcrumb["data"].items():
126
+ if isinstance(value, str):
127
+ breadcrumb["data"][key] = _scrub_path(value)
128
+
129
+
13
130
  def setup_sentry_observability() -> bool:
14
131
  """Set up Sentry observability for error tracking.
15
132
 
@@ -41,18 +158,38 @@ def setup_sentry_observability() -> bool:
41
158
  environment = "production"
42
159
 
43
160
  def before_send(event: Any, hint: dict[str, Any]) -> Any:
44
- """Filter out user-actionable errors from Sentry.
161
+ """Filter out user-actionable errors and scrub sensitive paths.
45
162
 
46
163
  User-actionable errors (like context size limits) are expected conditions
47
164
  that users need to resolve, not bugs that need tracking.
165
+
166
+ Also scrubs sensitive information like usernames from file paths and
167
+ working directories to protect user privacy.
48
168
  """
169
+
170
+ log_record = hint.get("log_record")
171
+ if log_record:
172
+ # Scrub pathname using the helper function
173
+ log_record.pathname = _scrub_path(log_record.pathname)
174
+
175
+ # Scrub traceback text if it exists
176
+ if hasattr(log_record, "exc_text") and isinstance(
177
+ log_record.exc_text, str
178
+ ):
179
+ # Replace home directory in traceback text
180
+ home = Path.home()
181
+ log_record.exc_text = log_record.exc_text.replace(str(home), "~")
182
+
49
183
  if "exc_info" in hint:
50
- exc_type, exc_value, tb = hint["exc_info"]
184
+ _, exc_value, _ = hint["exc_info"]
51
185
  from shotgun.exceptions import ErrorNotPickedUpBySentry
52
186
 
53
187
  if isinstance(exc_value, ErrorNotPickedUpBySentry):
54
188
  # Don't send to Sentry - this is user-actionable, not a bug
55
189
  return None
190
+
191
+ # Scrub sensitive paths from the event
192
+ _scrub_sensitive_paths(event)
56
193
  return event
57
194
 
58
195
  # Initialize Sentry
@@ -61,6 +198,7 @@ def setup_sentry_observability() -> bool:
61
198
  release=f"shotgun-sh@{__version__}",
62
199
  environment=environment,
63
200
  send_default_pii=False, # Privacy-first: never send PII
201
+ server_name="", # Privacy: don't send hostname (may contain username)
64
202
  traces_sample_rate=0.1 if environment == "production" else 1.0,
65
203
  profiles_sample_rate=0.1 if environment == "production" else 1.0,
66
204
  before_send=before_send,
shotgun/settings.py CHANGED
@@ -108,6 +108,11 @@ class LoggingSettings(BaseSettings):
108
108
  default=True,
109
109
  description="Enable file logging output",
110
110
  )
111
+ max_log_files: int = Field(
112
+ default=10,
113
+ description="Maximum number of log files to keep (older files are deleted)",
114
+ ge=1,
115
+ )
111
116
 
112
117
  model_config = SettingsConfigDict(
113
118
  env_prefix="SHOTGUN_",
shotgun/tui/app.py CHANGED
@@ -6,12 +6,18 @@ from textual.binding import Binding
6
6
  from textual.screen import Screen
7
7
 
8
8
  from shotgun.agents.agent_manager import AgentManager
9
- from shotgun.agents.config import ConfigManager, get_config_manager
9
+ from shotgun.agents.config import (
10
+ ConfigManager,
11
+ get_config_manager,
12
+ )
10
13
  from shotgun.agents.models import AgentType
11
14
  from shotgun.logging_config import get_logger
12
15
  from shotgun.tui.containers import TUIContainer
13
16
  from shotgun.tui.screens.splash import SplashScreen
14
- from shotgun.utils.file_system_utils import get_shotgun_base_path
17
+ from shotgun.utils.file_system_utils import (
18
+ ensure_shotgun_directory_exists,
19
+ get_shotgun_base_path,
20
+ )
15
21
  from shotgun.utils.update_checker import (
16
22
  detect_installation_method,
17
23
  perform_auto_update_async,
@@ -31,10 +37,10 @@ logger = get_logger(__name__)
31
37
  class ShotgunApp(App[None]):
32
38
  # ChatScreen removed from SCREENS dict since it requires dependency injection
33
39
  # and is instantiated manually in refresh_startup_screen()
40
+ # DirectorySetupScreen also removed since it requires error_message parameter
34
41
  SCREENS = {
35
42
  "provider_config": ProviderConfigScreen,
36
43
  "model_picker": ModelPickerScreen,
37
- "directory_setup": DirectorySetupScreen,
38
44
  "github_issue": GitHubIssueScreen,
39
45
  }
40
46
  BINDINGS = [
@@ -99,7 +105,10 @@ class ShotgunApp(App[None]):
99
105
  # Run async config loading in worker
100
106
  async def _check_config() -> None:
101
107
  # Show welcome screen if no providers are configured OR if user hasn't seen it yet
108
+ # Note: If config migration fails, ConfigManager will auto-create fresh config
109
+ # and set migration_failed flag, which WelcomeScreen will display
102
110
  config = await self.config_manager.load()
111
+
103
112
  has_any_key = await self.config_manager.has_any_provider_key()
104
113
  if not has_any_key or not config.shown_welcome_screen:
105
114
  if isinstance(self.screen, WelcomeScreen):
@@ -111,16 +120,32 @@ class ShotgunApp(App[None]):
111
120
  )
112
121
  return
113
122
 
123
+ # Try to create .shotgun directory if it doesn't exist
114
124
  if not self.check_local_shotgun_directory_exists():
115
- if isinstance(self.screen, DirectorySetupScreen):
125
+ try:
126
+ path = ensure_shotgun_directory_exists()
127
+ # Verify directory was created successfully
128
+ if not path.is_dir():
129
+ # Show error screen if creation failed
130
+ if isinstance(self.screen, DirectorySetupScreen):
131
+ return
132
+ self.push_screen(
133
+ DirectorySetupScreen(
134
+ error_message="Unable to create .shotgun directory due to filesystem conflict."
135
+ ),
136
+ callback=lambda _arg: self.refresh_startup_screen(),
137
+ )
138
+ return
139
+ except Exception as exc:
140
+ # Show error screen if creation failed with exception
141
+ if isinstance(self.screen, DirectorySetupScreen):
142
+ return
143
+ self.push_screen(
144
+ DirectorySetupScreen(error_message=str(exc)),
145
+ callback=lambda _arg: self.refresh_startup_screen(),
146
+ )
116
147
  return
117
148
 
118
- self.push_screen(
119
- DirectorySetupScreen(),
120
- callback=lambda _arg: self.refresh_startup_screen(),
121
- )
122
- return
123
-
124
149
  if isinstance(self.screen, ChatScreen):
125
150
  return
126
151