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.
- shotgun/agents/agent_manager.py +25 -11
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +287 -32
- shotgun/agents/config/models.py +26 -1
- shotgun/agents/config/provider.py +27 -0
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/history/token_counting/anthropic.py +8 -0
- shotgun/agents/runner.py +230 -0
- shotgun/build_constants.py +1 -1
- shotgun/cli/context.py +43 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/exceptions.py +323 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +2 -0
- shotgun/posthog_telemetry.py +18 -25
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +140 -2
- shotgun/settings.py +5 -0
- shotgun/tui/app.py +35 -10
- shotgun/tui/screens/chat/chat_screen.py +192 -91
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +62 -11
- shotgun/tui/screens/chat_screen/command_providers.py +3 -2
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/chat_history.py +37 -2
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +8 -1
- shotgun/tui/screens/pipx_migration.py +12 -6
- shotgun/tui/screens/provider_config.py +25 -8
- shotgun/tui/screens/shotgun_auth.py +0 -10
- shotgun/tui/screens/welcome.py +32 -0
- shotgun/tui/widgets/widget_coordinator.py +3 -2
- shotgun_sh-0.2.23.dev1.dist-info/METADATA +472 -0
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/RECORD +50 -42
- shotgun_sh-0.2.11.dev7.dist-info/METADATA +0 -130
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/licenses/LICENSE +0 -0
shotgun/sentry_telemetry.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|