shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.2.17__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.
Files changed (117) hide show
  1. shotgun/agents/agent_manager.py +354 -46
  2. shotgun/agents/common.py +14 -8
  3. shotgun/agents/config/constants.py +0 -6
  4. shotgun/agents/config/manager.py +66 -35
  5. shotgun/agents/config/models.py +41 -1
  6. shotgun/agents/config/provider.py +33 -5
  7. shotgun/agents/context_analyzer/__init__.py +28 -0
  8. shotgun/agents/context_analyzer/analyzer.py +471 -0
  9. shotgun/agents/context_analyzer/constants.py +9 -0
  10. shotgun/agents/context_analyzer/formatter.py +115 -0
  11. shotgun/agents/context_analyzer/models.py +212 -0
  12. shotgun/agents/conversation_history.py +2 -0
  13. shotgun/agents/conversation_manager.py +35 -19
  14. shotgun/agents/export.py +2 -2
  15. shotgun/agents/history/compaction.py +9 -4
  16. shotgun/agents/history/history_processors.py +113 -5
  17. shotgun/agents/history/token_counting/anthropic.py +17 -1
  18. shotgun/agents/history/token_counting/base.py +14 -3
  19. shotgun/agents/history/token_counting/openai.py +11 -1
  20. shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
  21. shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
  22. shotgun/agents/history/token_counting/utils.py +0 -3
  23. shotgun/agents/plan.py +2 -2
  24. shotgun/agents/research.py +3 -3
  25. shotgun/agents/specify.py +2 -2
  26. shotgun/agents/tasks.py +2 -2
  27. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  28. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  29. shotgun/agents/tools/codebase/file_read.py +11 -2
  30. shotgun/agents/tools/codebase/query_graph.py +6 -0
  31. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  32. shotgun/agents/tools/file_management.py +27 -7
  33. shotgun/agents/tools/registry.py +217 -0
  34. shotgun/agents/tools/web_search/__init__.py +8 -8
  35. shotgun/agents/tools/web_search/anthropic.py +8 -2
  36. shotgun/agents/tools/web_search/gemini.py +7 -1
  37. shotgun/agents/tools/web_search/openai.py +7 -1
  38. shotgun/agents/tools/web_search/utils.py +2 -2
  39. shotgun/agents/usage_manager.py +16 -11
  40. shotgun/api_endpoints.py +7 -3
  41. shotgun/build_constants.py +3 -3
  42. shotgun/cli/clear.py +53 -0
  43. shotgun/cli/compact.py +186 -0
  44. shotgun/cli/config.py +8 -5
  45. shotgun/cli/context.py +111 -0
  46. shotgun/cli/export.py +1 -1
  47. shotgun/cli/feedback.py +4 -2
  48. shotgun/cli/models.py +1 -0
  49. shotgun/cli/plan.py +1 -1
  50. shotgun/cli/research.py +1 -1
  51. shotgun/cli/specify.py +1 -1
  52. shotgun/cli/tasks.py +1 -1
  53. shotgun/cli/update.py +16 -2
  54. shotgun/codebase/core/change_detector.py +5 -3
  55. shotgun/codebase/core/code_retrieval.py +4 -2
  56. shotgun/codebase/core/ingestor.py +10 -8
  57. shotgun/codebase/core/manager.py +13 -4
  58. shotgun/codebase/core/nl_query.py +1 -1
  59. shotgun/exceptions.py +32 -0
  60. shotgun/logging_config.py +18 -27
  61. shotgun/main.py +73 -11
  62. shotgun/posthog_telemetry.py +37 -28
  63. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
  64. shotgun/sentry_telemetry.py +163 -16
  65. shotgun/settings.py +238 -0
  66. shotgun/telemetry.py +10 -33
  67. shotgun/tui/app.py +243 -43
  68. shotgun/tui/commands/__init__.py +1 -1
  69. shotgun/tui/components/context_indicator.py +179 -0
  70. shotgun/tui/components/mode_indicator.py +70 -0
  71. shotgun/tui/components/status_bar.py +48 -0
  72. shotgun/tui/containers.py +91 -0
  73. shotgun/tui/dependencies.py +39 -0
  74. shotgun/tui/protocols.py +45 -0
  75. shotgun/tui/screens/chat/__init__.py +5 -0
  76. shotgun/tui/screens/chat/chat.tcss +54 -0
  77. shotgun/tui/screens/chat/chat_screen.py +1254 -0
  78. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  79. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  80. shotgun/tui/screens/chat/help_text.py +40 -0
  81. shotgun/tui/screens/chat/prompt_history.py +48 -0
  82. shotgun/tui/screens/chat.tcss +11 -0
  83. shotgun/tui/screens/chat_screen/command_providers.py +78 -2
  84. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  85. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  86. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  87. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  88. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  89. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  90. shotgun/tui/screens/confirmation_dialog.py +151 -0
  91. shotgun/tui/screens/feedback.py +4 -4
  92. shotgun/tui/screens/github_issue.py +102 -0
  93. shotgun/tui/screens/model_picker.py +49 -24
  94. shotgun/tui/screens/onboarding.py +431 -0
  95. shotgun/tui/screens/pipx_migration.py +153 -0
  96. shotgun/tui/screens/provider_config.py +50 -27
  97. shotgun/tui/screens/shotgun_auth.py +2 -2
  98. shotgun/tui/screens/welcome.py +14 -11
  99. shotgun/tui/services/__init__.py +5 -0
  100. shotgun/tui/services/conversation_service.py +184 -0
  101. shotgun/tui/state/__init__.py +7 -0
  102. shotgun/tui/state/processing_state.py +185 -0
  103. shotgun/tui/utils/mode_progress.py +14 -7
  104. shotgun/tui/widgets/__init__.py +5 -0
  105. shotgun/tui/widgets/widget_coordinator.py +263 -0
  106. shotgun/utils/file_system_utils.py +22 -2
  107. shotgun/utils/marketing.py +110 -0
  108. shotgun/utils/update_checker.py +69 -14
  109. shotgun_sh-0.2.17.dist-info/METADATA +465 -0
  110. shotgun_sh-0.2.17.dist-info/RECORD +194 -0
  111. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
  112. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/licenses/LICENSE +1 -1
  113. shotgun/tui/screens/chat.py +0 -996
  114. shotgun/tui/screens/chat_screen/history.py +0 -335
  115. shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
  116. shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
  117. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
@@ -1,14 +1,132 @@
1
1
  """Sentry observability setup for Shotgun."""
2
2
 
3
- import os
3
+ from pathlib import Path
4
+ from typing import Any
4
5
 
5
6
  from shotgun import __version__
6
7
  from shotgun.logging_config import get_early_logger
8
+ from shotgun.settings import settings
7
9
 
8
10
  # Use early logger to prevent automatic StreamHandler creation
9
11
  logger = get_early_logger(__name__)
10
12
 
11
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
+
12
130
  def setup_sentry_observability() -> bool:
13
131
  """Set up Sentry observability for error tracking.
14
132
 
@@ -23,48 +141,77 @@ def setup_sentry_observability() -> bool:
23
141
  logger.debug("Sentry is already initialized, skipping")
24
142
  return True
25
143
 
26
- # Try to get DSN from build constants first (production builds)
27
- dsn = None
28
- try:
29
- from shotgun import build_constants
30
-
31
- dsn = build_constants.SENTRY_DSN
32
- logger.debug("Using Sentry DSN from build constants")
33
- except ImportError:
34
- # Fallback to environment variable (development)
35
- dsn = os.getenv("SENTRY_DSN", "")
36
- if dsn:
37
- logger.debug("Using Sentry DSN from environment variable")
144
+ # Get DSN from settings (handles build constants + env vars automatically)
145
+ dsn = settings.telemetry.sentry_dsn
38
146
 
39
147
  if not dsn:
40
148
  logger.debug("No Sentry DSN configured, skipping Sentry initialization")
41
149
  return False
42
150
 
43
- logger.debug("Found DSN, proceeding with Sentry setup")
151
+ logger.debug("Using Sentry DSN from settings, proceeding with setup")
44
152
 
45
153
  # Determine environment based on version
46
- # Dev versions contain "dev", "rc", "alpha", or "beta"
154
+ # Dev versions contain "dev", "rc", "alpha", "beta"
47
155
  if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
48
156
  environment = "development"
49
157
  else:
50
158
  environment = "production"
51
159
 
160
+ def before_send(event: Any, hint: dict[str, Any]) -> Any:
161
+ """Filter out user-actionable errors and scrub sensitive paths.
162
+
163
+ User-actionable errors (like context size limits) are expected conditions
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.
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
+
183
+ if "exc_info" in hint:
184
+ _, exc_value, _ = hint["exc_info"]
185
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
186
+
187
+ if isinstance(exc_value, ErrorNotPickedUpBySentry):
188
+ # Don't send to Sentry - this is user-actionable, not a bug
189
+ return None
190
+
191
+ # Scrub sensitive paths from the event
192
+ _scrub_sensitive_paths(event)
193
+ return event
194
+
52
195
  # Initialize Sentry
53
196
  sentry_sdk.init(
54
197
  dsn=dsn,
55
198
  release=f"shotgun-sh@{__version__}",
56
199
  environment=environment,
57
200
  send_default_pii=False, # Privacy-first: never send PII
201
+ server_name="", # Privacy: don't send hostname (may contain username)
58
202
  traces_sample_rate=0.1 if environment == "production" else 1.0,
59
203
  profiles_sample_rate=0.1 if environment == "production" else 1.0,
204
+ before_send=before_send,
60
205
  )
61
206
 
62
207
  # Set user context with anonymous shotgun instance ID from config
63
208
  try:
209
+ import asyncio
210
+
64
211
  from shotgun.agents.config import get_config_manager
65
212
 
66
213
  config_manager = get_config_manager()
67
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
214
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
68
215
  sentry_sdk.set_user({"id": shotgun_instance_id})
69
216
  logger.debug("Sentry user context set with anonymous ID")
70
217
  except Exception as e:
shotgun/settings.py ADDED
@@ -0,0 +1,238 @@
1
+ """Centralized application settings using Pydantic Settings.
2
+
3
+ All environment variables use the SHOTGUN_ prefix to avoid conflicts with other tools.
4
+ Settings are loaded with the following priority:
5
+ 1. Environment variables (highest priority)
6
+ 2. Build constants (embedded at build time)
7
+ 3. Default values (lowest priority)
8
+
9
+ Example usage:
10
+ from shotgun.settings import settings
11
+
12
+ # Access telemetry settings
13
+ if settings.telemetry.sentry_dsn:
14
+ sentry_sdk.init(dsn=settings.telemetry.sentry_dsn)
15
+
16
+ # Access logging settings
17
+ logger.setLevel(settings.logging.log_level)
18
+
19
+ # Access API settings
20
+ response = httpx.get(settings.api.web_base_url)
21
+ """
22
+
23
+ from typing import Any
24
+
25
+ from pydantic import Field, field_validator
26
+ from pydantic_settings import BaseSettings, SettingsConfigDict
27
+
28
+
29
+ def _get_build_constant(name: str, default: Any = None) -> Any:
30
+ """Get a value from build_constants.py, falling back to default.
31
+
32
+ Args:
33
+ name: The constant name to retrieve (e.g., "SENTRY_DSN")
34
+ default: Default value if constant not found
35
+
36
+ Returns:
37
+ The constant value, or default if not found/import fails
38
+ """
39
+ try:
40
+ from shotgun import build_constants
41
+
42
+ return getattr(build_constants, name, default)
43
+ except ImportError:
44
+ return default
45
+
46
+
47
+ class TelemetrySettings(BaseSettings):
48
+ """Telemetry and observability settings.
49
+
50
+ These settings control error tracking (Sentry), analytics (PostHog),
51
+ and observability (Logfire) integrations.
52
+ """
53
+
54
+ sentry_dsn: str = Field(
55
+ default_factory=lambda: _get_build_constant("SENTRY_DSN", ""),
56
+ description="Sentry DSN for error tracking",
57
+ )
58
+ posthog_api_key: str = Field(
59
+ default_factory=lambda: _get_build_constant("POSTHOG_API_KEY", ""),
60
+ description="PostHog API key for analytics",
61
+ )
62
+ posthog_project_id: str = Field(
63
+ default_factory=lambda: _get_build_constant("POSTHOG_PROJECT_ID", ""),
64
+ description="PostHog project ID",
65
+ )
66
+ logfire_enabled: bool = Field(
67
+ default_factory=lambda: _get_build_constant("LOGFIRE_ENABLED", False),
68
+ description="Enable Logfire observability (dev builds only)",
69
+ )
70
+ logfire_token: str = Field(
71
+ default_factory=lambda: _get_build_constant("LOGFIRE_TOKEN", ""),
72
+ description="Logfire authentication token",
73
+ )
74
+
75
+ model_config = SettingsConfigDict(
76
+ env_prefix="SHOTGUN_",
77
+ env_file=".env",
78
+ env_file_encoding="utf-8",
79
+ extra="ignore",
80
+ )
81
+
82
+ @field_validator("logfire_enabled", mode="before")
83
+ @classmethod
84
+ def parse_bool(cls, v: Any) -> bool:
85
+ """Parse boolean values from strings (matches is_truthy behavior)."""
86
+ if isinstance(v, bool):
87
+ return v
88
+ if isinstance(v, str):
89
+ return v.lower() in ("true", "1", "yes")
90
+ return bool(v)
91
+
92
+
93
+ class LoggingSettings(BaseSettings):
94
+ """Logging configuration settings.
95
+
96
+ Controls log level, console output, and file logging behavior.
97
+ """
98
+
99
+ log_level: str = Field(
100
+ default="INFO",
101
+ description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
102
+ )
103
+ logging_to_console: bool = Field(
104
+ default=False,
105
+ description="Enable console logging output",
106
+ )
107
+ logging_to_file: bool = Field(
108
+ default=True,
109
+ description="Enable file logging output",
110
+ )
111
+
112
+ model_config = SettingsConfigDict(
113
+ env_prefix="SHOTGUN_",
114
+ env_file=".env",
115
+ env_file_encoding="utf-8",
116
+ extra="ignore",
117
+ )
118
+
119
+ @field_validator("log_level")
120
+ @classmethod
121
+ def validate_log_level(cls, v: str) -> str:
122
+ """Validate log level is one of the allowed values."""
123
+ v = v.upper()
124
+ valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
125
+ if v not in valid_levels:
126
+ return "INFO" # Default to INFO if invalid
127
+ return v
128
+
129
+ @field_validator("logging_to_console", "logging_to_file", mode="before")
130
+ @classmethod
131
+ def parse_bool(cls, v: Any) -> bool:
132
+ """Parse boolean values from strings (matches is_truthy behavior)."""
133
+ if isinstance(v, bool):
134
+ return v
135
+ if isinstance(v, str):
136
+ return v.lower() in ("true", "1", "yes")
137
+ return bool(v)
138
+
139
+
140
+ class ApiSettings(BaseSettings):
141
+ """API endpoint settings.
142
+
143
+ Configuration for Shotgun backend services.
144
+ """
145
+
146
+ web_base_url: str = Field(
147
+ default="https://api-219702594231.us-east4.run.app",
148
+ description="Shotgun Web API base URL (authentication/subscription)",
149
+ )
150
+ account_llm_base_url: str = Field(
151
+ default="https://litellm-219702594231.us-east4.run.app",
152
+ description="Shotgun's LiteLLM proxy base URL (AI model requests)",
153
+ )
154
+
155
+ model_config = SettingsConfigDict(
156
+ env_prefix="SHOTGUN_",
157
+ env_file=".env",
158
+ env_file_encoding="utf-8",
159
+ extra="ignore",
160
+ )
161
+
162
+
163
+ class DevelopmentSettings(BaseSettings):
164
+ """Development and testing settings.
165
+
166
+ These settings are primarily used for testing and development purposes.
167
+ """
168
+
169
+ home: str | None = Field(
170
+ default=None,
171
+ description="Override Shotgun home directory (for testing)",
172
+ )
173
+ pipx_simulate: bool = Field(
174
+ default=False,
175
+ description="Simulate pipx installation (for testing)",
176
+ )
177
+
178
+ model_config = SettingsConfigDict(
179
+ env_prefix="SHOTGUN_",
180
+ env_file=".env",
181
+ env_file_encoding="utf-8",
182
+ extra="ignore",
183
+ )
184
+
185
+ @field_validator("pipx_simulate", mode="before")
186
+ @classmethod
187
+ def parse_bool(cls, v: Any) -> bool:
188
+ """Parse boolean values from strings (matches is_truthy behavior)."""
189
+ if isinstance(v, bool):
190
+ return v
191
+ if isinstance(v, str):
192
+ return v.lower() in ("true", "1", "yes")
193
+ return bool(v)
194
+
195
+
196
+ class Settings(BaseSettings):
197
+ """Main application settings with SHOTGUN_ prefix.
198
+
199
+ This is the main settings class that composes all other settings groups.
200
+ Access settings via the global `settings` singleton instance.
201
+
202
+ Example:
203
+ from shotgun.settings import settings
204
+
205
+ # Telemetry settings
206
+ settings.telemetry.sentry_dsn
207
+ settings.telemetry.posthog_api_key
208
+ settings.telemetry.logfire_enabled
209
+
210
+ # Logging settings
211
+ settings.logging.log_level
212
+ settings.logging.logging_to_console
213
+
214
+ # API settings
215
+ settings.api.web_base_url
216
+ settings.api.account_llm_base_url
217
+
218
+ # Development settings
219
+ settings.dev.home
220
+ settings.dev.pipx_simulate
221
+ """
222
+
223
+ telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings)
224
+ logging: LoggingSettings = Field(default_factory=LoggingSettings)
225
+ api: ApiSettings = Field(default_factory=ApiSettings)
226
+ dev: DevelopmentSettings = Field(default_factory=DevelopmentSettings)
227
+
228
+ model_config = SettingsConfigDict(
229
+ env_prefix="SHOTGUN_",
230
+ env_file=".env",
231
+ env_file_encoding="utf-8",
232
+ extra="ignore",
233
+ )
234
+
235
+
236
+ # Global settings singleton
237
+ # Import this in your modules: from shotgun.settings import settings
238
+ settings = Settings()
shotgun/telemetry.py CHANGED
@@ -1,9 +1,7 @@
1
1
  """Observability setup for Logfire."""
2
2
 
3
- import os
4
-
5
3
  from shotgun.logging_config import get_early_logger
6
- from shotgun.utils.env_utils import is_falsy, is_truthy
4
+ from shotgun.settings import settings
7
5
 
8
6
  # Use early logger to prevent automatic StreamHandler creation
9
7
  logger = get_early_logger(__name__)
@@ -15,36 +13,13 @@ def setup_logfire_observability() -> bool:
15
13
  Returns:
16
14
  True if Logfire was successfully set up, False otherwise
17
15
  """
18
- # Try to get Logfire configuration from build constants first, fall back to env vars
19
- logfire_enabled = None
20
- logfire_token = None
21
-
22
- try:
23
- from shotgun.build_constants import LOGFIRE_ENABLED, LOGFIRE_TOKEN
24
-
25
- # Use build constants if they're not empty
26
- if LOGFIRE_ENABLED:
27
- logfire_enabled = LOGFIRE_ENABLED
28
- if LOGFIRE_TOKEN:
29
- logfire_token = LOGFIRE_TOKEN
30
- except ImportError:
31
- # No build constants available
32
- pass
33
-
34
- # Fall back to environment variables if not set from build constants
35
- if not logfire_enabled:
36
- logfire_enabled = os.getenv("LOGFIRE_ENABLED", "false")
37
- if not logfire_token:
38
- logfire_token = os.getenv("LOGFIRE_TOKEN")
39
-
40
- # Allow environment variable to override and disable Logfire
41
- env_override = os.getenv("LOGFIRE_ENABLED")
42
- if env_override and is_falsy(env_override):
43
- logfire_enabled = env_override
16
+ # Get Logfire configuration from settings (handles build constants + env vars)
17
+ logfire_enabled = settings.telemetry.logfire_enabled
18
+ logfire_token = settings.telemetry.logfire_token
44
19
 
45
20
  # Check if Logfire observability is enabled
46
- if not is_truthy(logfire_enabled):
47
- logger.debug("Logfire observability disabled via LOGFIRE_ENABLED")
21
+ if not logfire_enabled:
22
+ logger.debug("Logfire observability disabled")
48
23
  return False
49
24
 
50
25
  try:
@@ -52,7 +27,7 @@ def setup_logfire_observability() -> bool:
52
27
 
53
28
  # Check for Logfire token
54
29
  if not logfire_token:
55
- logger.warning("LOGFIRE_TOKEN not set, Logfire observability disabled")
30
+ logger.warning("Logfire token not set, Logfire observability disabled")
56
31
  return False
57
32
 
58
33
  # Configure Logfire
@@ -75,12 +50,14 @@ def setup_logfire_observability() -> bool:
75
50
 
76
51
  # Set user context using baggage for all logs and spans
77
52
  try:
53
+ import asyncio
54
+
78
55
  from opentelemetry import baggage, context
79
56
 
80
57
  from shotgun.agents.config import get_config_manager
81
58
 
82
59
  config_manager = get_config_manager()
83
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
60
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
84
61
 
85
62
  # Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
86
63
  ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)