shotgun-sh 0.1.14__py3-none-any.whl → 0.2.11__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 (143) hide show
  1. shotgun/agents/agent_manager.py +715 -75
  2. shotgun/agents/common.py +80 -75
  3. shotgun/agents/config/constants.py +21 -10
  4. shotgun/agents/config/manager.py +322 -97
  5. shotgun/agents/config/models.py +114 -84
  6. shotgun/agents/config/provider.py +232 -88
  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 +125 -2
  13. shotgun/agents/conversation_manager.py +57 -19
  14. shotgun/agents/export.py +6 -7
  15. shotgun/agents/history/compaction.py +10 -5
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +129 -12
  18. shotgun/agents/history/token_counting/__init__.py +31 -0
  19. shotgun/agents/history/token_counting/anthropic.py +127 -0
  20. shotgun/agents/history/token_counting/base.py +78 -0
  21. shotgun/agents/history/token_counting/openai.py +90 -0
  22. shotgun/agents/history/token_counting/sentencepiece_counter.py +127 -0
  23. shotgun/agents/history/token_counting/tokenizer_cache.py +92 -0
  24. shotgun/agents/history/token_counting/utils.py +144 -0
  25. shotgun/agents/history/token_estimation.py +12 -12
  26. shotgun/agents/llm.py +62 -0
  27. shotgun/agents/models.py +59 -4
  28. shotgun/agents/plan.py +6 -7
  29. shotgun/agents/research.py +7 -8
  30. shotgun/agents/specify.py +6 -7
  31. shotgun/agents/tasks.py +6 -7
  32. shotgun/agents/tools/__init__.py +0 -2
  33. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  34. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  35. shotgun/agents/tools/codebase/file_read.py +11 -2
  36. shotgun/agents/tools/codebase/query_graph.py +6 -0
  37. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  38. shotgun/agents/tools/file_management.py +82 -16
  39. shotgun/agents/tools/registry.py +217 -0
  40. shotgun/agents/tools/web_search/__init__.py +55 -16
  41. shotgun/agents/tools/web_search/anthropic.py +76 -51
  42. shotgun/agents/tools/web_search/gemini.py +50 -27
  43. shotgun/agents/tools/web_search/openai.py +26 -17
  44. shotgun/agents/tools/web_search/utils.py +2 -2
  45. shotgun/agents/usage_manager.py +164 -0
  46. shotgun/api_endpoints.py +15 -0
  47. shotgun/cli/clear.py +53 -0
  48. shotgun/cli/compact.py +186 -0
  49. shotgun/cli/config.py +41 -67
  50. shotgun/cli/context.py +111 -0
  51. shotgun/cli/export.py +1 -1
  52. shotgun/cli/feedback.py +50 -0
  53. shotgun/cli/models.py +3 -2
  54. shotgun/cli/plan.py +1 -1
  55. shotgun/cli/research.py +1 -1
  56. shotgun/cli/specify.py +1 -1
  57. shotgun/cli/tasks.py +1 -1
  58. shotgun/cli/update.py +16 -2
  59. shotgun/codebase/core/change_detector.py +5 -3
  60. shotgun/codebase/core/code_retrieval.py +4 -2
  61. shotgun/codebase/core/ingestor.py +57 -16
  62. shotgun/codebase/core/manager.py +20 -7
  63. shotgun/codebase/core/nl_query.py +1 -1
  64. shotgun/codebase/models.py +4 -4
  65. shotgun/exceptions.py +32 -0
  66. shotgun/llm_proxy/__init__.py +19 -0
  67. shotgun/llm_proxy/clients.py +44 -0
  68. shotgun/llm_proxy/constants.py +15 -0
  69. shotgun/logging_config.py +18 -27
  70. shotgun/main.py +91 -12
  71. shotgun/posthog_telemetry.py +81 -10
  72. shotgun/prompts/agents/export.j2 +18 -1
  73. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  74. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  75. shotgun/prompts/agents/plan.j2 +1 -1
  76. shotgun/prompts/agents/research.j2 +1 -1
  77. shotgun/prompts/agents/specify.j2 +270 -3
  78. shotgun/prompts/agents/state/system_state.j2 +4 -0
  79. shotgun/prompts/agents/tasks.j2 +1 -1
  80. shotgun/prompts/loader.py +2 -2
  81. shotgun/prompts/tools/web_search.j2 +14 -0
  82. shotgun/sentry_telemetry.py +27 -18
  83. shotgun/settings.py +238 -0
  84. shotgun/shotgun_web/__init__.py +19 -0
  85. shotgun/shotgun_web/client.py +138 -0
  86. shotgun/shotgun_web/constants.py +21 -0
  87. shotgun/shotgun_web/models.py +47 -0
  88. shotgun/telemetry.py +24 -36
  89. shotgun/tui/app.py +251 -23
  90. shotgun/tui/commands/__init__.py +1 -1
  91. shotgun/tui/components/context_indicator.py +179 -0
  92. shotgun/tui/components/mode_indicator.py +70 -0
  93. shotgun/tui/components/status_bar.py +48 -0
  94. shotgun/tui/containers.py +91 -0
  95. shotgun/tui/dependencies.py +39 -0
  96. shotgun/tui/protocols.py +45 -0
  97. shotgun/tui/screens/chat/__init__.py +5 -0
  98. shotgun/tui/screens/chat/chat.tcss +54 -0
  99. shotgun/tui/screens/chat/chat_screen.py +1234 -0
  100. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  101. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  102. shotgun/tui/screens/chat/help_text.py +40 -0
  103. shotgun/tui/screens/chat/prompt_history.py +48 -0
  104. shotgun/tui/screens/chat.tcss +11 -0
  105. shotgun/tui/screens/chat_screen/command_providers.py +226 -11
  106. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  107. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  108. shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
  109. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  110. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  111. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  112. shotgun/tui/screens/confirmation_dialog.py +151 -0
  113. shotgun/tui/screens/feedback.py +193 -0
  114. shotgun/tui/screens/github_issue.py +102 -0
  115. shotgun/tui/screens/model_picker.py +352 -0
  116. shotgun/tui/screens/onboarding.py +431 -0
  117. shotgun/tui/screens/pipx_migration.py +153 -0
  118. shotgun/tui/screens/provider_config.py +156 -39
  119. shotgun/tui/screens/shotgun_auth.py +295 -0
  120. shotgun/tui/screens/welcome.py +198 -0
  121. shotgun/tui/services/__init__.py +5 -0
  122. shotgun/tui/services/conversation_service.py +184 -0
  123. shotgun/tui/state/__init__.py +7 -0
  124. shotgun/tui/state/processing_state.py +185 -0
  125. shotgun/tui/utils/mode_progress.py +14 -7
  126. shotgun/tui/widgets/__init__.py +5 -0
  127. shotgun/tui/widgets/widget_coordinator.py +262 -0
  128. shotgun/utils/datetime_utils.py +77 -0
  129. shotgun/utils/env_utils.py +13 -0
  130. shotgun/utils/file_system_utils.py +22 -2
  131. shotgun/utils/marketing.py +110 -0
  132. shotgun/utils/update_checker.py +69 -14
  133. shotgun_sh-0.2.11.dist-info/METADATA +130 -0
  134. shotgun_sh-0.2.11.dist-info/RECORD +194 -0
  135. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/entry_points.txt +1 -0
  136. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/licenses/LICENSE +1 -1
  137. shotgun/agents/history/token_counting.py +0 -429
  138. shotgun/agents/tools/user_interaction.py +0 -37
  139. shotgun/tui/screens/chat.py +0 -797
  140. shotgun/tui/screens/chat_screen/history.py +0 -350
  141. shotgun_sh-0.1.14.dist-info/METADATA +0 -466
  142. shotgun_sh-0.1.14.dist-info/RECORD +0 -133
  143. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/WHEEL +0 -0
@@ -1,9 +1,10 @@
1
1
  """Sentry observability setup for Shotgun."""
2
2
 
3
- import os
3
+ from typing import Any
4
4
 
5
5
  from shotgun import __version__
6
6
  from shotgun.logging_config import get_early_logger
7
+ from shotgun.settings import settings
7
8
 
8
9
  # Use early logger to prevent automatic StreamHandler creation
9
10
  logger = get_early_logger(__name__)
@@ -23,32 +24,37 @@ def setup_sentry_observability() -> bool:
23
24
  logger.debug("Sentry is already initialized, skipping")
24
25
  return True
25
26
 
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")
27
+ # Get DSN from settings (handles build constants + env vars automatically)
28
+ dsn = settings.telemetry.sentry_dsn
38
29
 
39
30
  if not dsn:
40
31
  logger.debug("No Sentry DSN configured, skipping Sentry initialization")
41
32
  return False
42
33
 
43
- logger.debug("Found DSN, proceeding with Sentry setup")
34
+ logger.debug("Using Sentry DSN from settings, proceeding with setup")
44
35
 
45
36
  # Determine environment based on version
46
- # Dev versions contain "dev", "rc", "alpha", or "beta"
37
+ # Dev versions contain "dev", "rc", "alpha", "beta"
47
38
  if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
48
39
  environment = "development"
49
40
  else:
50
41
  environment = "production"
51
42
 
43
+ def before_send(event: Any, hint: dict[str, Any]) -> Any:
44
+ """Filter out user-actionable errors from Sentry.
45
+
46
+ User-actionable errors (like context size limits) are expected conditions
47
+ that users need to resolve, not bugs that need tracking.
48
+ """
49
+ if "exc_info" in hint:
50
+ exc_type, exc_value, tb = hint["exc_info"]
51
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
52
+
53
+ if isinstance(exc_value, ErrorNotPickedUpBySentry):
54
+ # Don't send to Sentry - this is user-actionable, not a bug
55
+ return None
56
+ return event
57
+
52
58
  # Initialize Sentry
53
59
  sentry_sdk.init(
54
60
  dsn=dsn,
@@ -57,15 +63,18 @@ def setup_sentry_observability() -> bool:
57
63
  send_default_pii=False, # Privacy-first: never send PII
58
64
  traces_sample_rate=0.1 if environment == "production" else 1.0,
59
65
  profiles_sample_rate=0.1 if environment == "production" else 1.0,
66
+ before_send=before_send,
60
67
  )
61
68
 
62
- # Set user context with anonymous user ID from config
69
+ # Set user context with anonymous shotgun instance ID from config
63
70
  try:
71
+ import asyncio
72
+
64
73
  from shotgun.agents.config import get_config_manager
65
74
 
66
75
  config_manager = get_config_manager()
67
- user_id = config_manager.get_user_id()
68
- sentry_sdk.set_user({"id": user_id})
76
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
77
+ sentry_sdk.set_user({"id": shotgun_instance_id})
69
78
  logger.debug("Sentry user context set with anonymous ID")
70
79
  except Exception as e:
71
80
  logger.warning("Failed to set Sentry user context: %s", 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()
@@ -0,0 +1,19 @@
1
+ """Shotgun Web API client for subscription and authentication."""
2
+
3
+ from .client import ShotgunWebClient, check_token_status, create_unification_token
4
+ from .models import (
5
+ TokenCreateRequest,
6
+ TokenCreateResponse,
7
+ TokenStatus,
8
+ TokenStatusResponse,
9
+ )
10
+
11
+ __all__ = [
12
+ "ShotgunWebClient",
13
+ "create_unification_token",
14
+ "check_token_status",
15
+ "TokenCreateRequest",
16
+ "TokenCreateResponse",
17
+ "TokenStatus",
18
+ "TokenStatusResponse",
19
+ ]
@@ -0,0 +1,138 @@
1
+ """HTTP client for Shotgun Web API."""
2
+
3
+ import httpx
4
+
5
+ from shotgun.logging_config import get_logger
6
+
7
+ from .constants import (
8
+ SHOTGUN_WEB_BASE_URL,
9
+ UNIFICATION_TOKEN_CREATE_PATH,
10
+ UNIFICATION_TOKEN_STATUS_PATH,
11
+ )
12
+ from .models import (
13
+ TokenCreateRequest,
14
+ TokenCreateResponse,
15
+ TokenStatusResponse,
16
+ )
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class ShotgunWebClient:
22
+ """HTTP client for Shotgun Web API."""
23
+
24
+ def __init__(self, base_url: str | None = None, timeout: float = 10.0):
25
+ """Initialize Shotgun Web client.
26
+
27
+ Args:
28
+ base_url: Base URL for Shotgun Web API. If None, uses SHOTGUN_WEB_BASE_URL
29
+ timeout: Request timeout in seconds
30
+ """
31
+ self.base_url = base_url or SHOTGUN_WEB_BASE_URL
32
+ self.timeout = timeout
33
+
34
+ def create_unification_token(self, shotgun_instance_id: str) -> TokenCreateResponse:
35
+ """Create a unification token for CLI authentication.
36
+
37
+ Args:
38
+ shotgun_instance_id: UUID for this shotgun instance
39
+
40
+ Returns:
41
+ Token creation response with token and auth URL
42
+
43
+ Raises:
44
+ httpx.HTTPError: If request fails
45
+ """
46
+ url = f"{self.base_url}{UNIFICATION_TOKEN_CREATE_PATH}"
47
+ request_data = TokenCreateRequest(shotgun_instance_id=shotgun_instance_id)
48
+
49
+ logger.debug("Creating unification token for instance %s", shotgun_instance_id)
50
+
51
+ try:
52
+ response = httpx.post(
53
+ url,
54
+ json=request_data.model_dump(),
55
+ timeout=self.timeout,
56
+ )
57
+ response.raise_for_status()
58
+
59
+ data = response.json()
60
+ result = TokenCreateResponse.model_validate(data)
61
+
62
+ logger.info(
63
+ "Successfully created unification token, expires in %d seconds",
64
+ result.expires_in_seconds,
65
+ )
66
+ return result
67
+
68
+ except httpx.HTTPError as e:
69
+ logger.error("Failed to create unification token: %s", e)
70
+ raise
71
+
72
+ def check_token_status(self, token: str) -> TokenStatusResponse:
73
+ """Check token status and get keys if completed.
74
+
75
+ Args:
76
+ token: Unification token to check
77
+
78
+ Returns:
79
+ Token status response with status and keys (if completed)
80
+
81
+ Raises:
82
+ httpx.HTTPStatusError: If token not found (404) or expired (410)
83
+ httpx.HTTPError: For other request failures
84
+ """
85
+ url = f"{self.base_url}{UNIFICATION_TOKEN_STATUS_PATH.format(token=token)}"
86
+
87
+ logger.debug("Checking status for token %s...", token[:8])
88
+
89
+ try:
90
+ response = httpx.get(url, timeout=self.timeout)
91
+ response.raise_for_status()
92
+
93
+ data = response.json()
94
+ result = TokenStatusResponse.model_validate(data)
95
+
96
+ logger.debug("Token status: %s", result.status)
97
+ return result
98
+
99
+ except httpx.HTTPStatusError as e:
100
+ if e.response.status_code == 404:
101
+ logger.error("Token not found: %s", token[:8])
102
+ elif e.response.status_code == 410:
103
+ logger.error("Token expired: %s", token[:8])
104
+ raise
105
+ except httpx.HTTPError as e:
106
+ logger.error("Failed to check token status: %s", e)
107
+ raise
108
+
109
+
110
+ # Convenience functions for standalone use
111
+ def create_unification_token(shotgun_instance_id: str) -> TokenCreateResponse:
112
+ """Create a unification token.
113
+
114
+ Convenience function that creates a client and calls create_unification_token.
115
+
116
+ Args:
117
+ shotgun_instance_id: UUID for this shotgun instance
118
+
119
+ Returns:
120
+ Token creation response
121
+ """
122
+ client = ShotgunWebClient()
123
+ return client.create_unification_token(shotgun_instance_id)
124
+
125
+
126
+ def check_token_status(token: str) -> TokenStatusResponse:
127
+ """Check token status.
128
+
129
+ Convenience function that creates a client and calls check_token_status.
130
+
131
+ Args:
132
+ token: Unification token to check
133
+
134
+ Returns:
135
+ Token status response
136
+ """
137
+ client = ShotgunWebClient()
138
+ return client.check_token_status(token)
@@ -0,0 +1,21 @@
1
+ """Constants for Shotgun Web API."""
2
+
3
+ # Import from centralized API endpoints module
4
+ from shotgun.api_endpoints import SHOTGUN_WEB_BASE_URL
5
+
6
+ # API endpoints
7
+ UNIFICATION_TOKEN_CREATE_PATH = "/api/unification/token/create" # noqa: S105
8
+ UNIFICATION_TOKEN_STATUS_PATH = "/api/unification/token/{token}/status" # noqa: S105
9
+
10
+ # Polling configuration
11
+ DEFAULT_POLL_INTERVAL_SECONDS = 3
12
+ DEFAULT_TOKEN_TIMEOUT_SECONDS = 1800 # 30 minutes
13
+
14
+ # Re-export for backward compatibility
15
+ __all__ = [
16
+ "SHOTGUN_WEB_BASE_URL",
17
+ "UNIFICATION_TOKEN_CREATE_PATH",
18
+ "UNIFICATION_TOKEN_STATUS_PATH",
19
+ "DEFAULT_POLL_INTERVAL_SECONDS",
20
+ "DEFAULT_TOKEN_TIMEOUT_SECONDS",
21
+ ]
@@ -0,0 +1,47 @@
1
+ """Pydantic models for Shotgun Web API."""
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class TokenStatus(StrEnum):
9
+ """Token status enum matching API specification."""
10
+
11
+ PENDING = "pending"
12
+ COMPLETED = "completed"
13
+ AWAITING_PAYMENT = "awaiting_payment"
14
+ EXPIRED = "expired"
15
+
16
+
17
+ class TokenCreateRequest(BaseModel):
18
+ """Request model for creating a unification token."""
19
+
20
+ shotgun_instance_id: str = Field(
21
+ description="CLI-provided UUID for shotgun instance"
22
+ )
23
+
24
+
25
+ class TokenCreateResponse(BaseModel):
26
+ """Response model for token creation."""
27
+
28
+ token: str = Field(description="Secure authentication token")
29
+ auth_url: str = Field(description="Web authentication URL for user to complete")
30
+ expires_in_seconds: int = Field(description="Token expiration time in seconds")
31
+
32
+
33
+ class TokenStatusResponse(BaseModel):
34
+ """Response model for token status check."""
35
+
36
+ status: TokenStatus = Field(description="Current token status")
37
+ supabase_key: str | None = Field(
38
+ default=None,
39
+ description="Supabase user JWT (only returned when status=completed)",
40
+ )
41
+ litellm_key: str | None = Field(
42
+ default=None,
43
+ description="LiteLLM virtual key (only returned when status=completed)",
44
+ )
45
+ message: str | None = Field(
46
+ default=None, description="Human-readable status message"
47
+ )
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
@@ -65,19 +40,32 @@ def setup_logfire_observability() -> bool:
65
40
  # Instrument Pydantic AI for better observability
66
41
  logfire.instrument_pydantic_ai()
67
42
 
43
+ # Add LogfireLoggingHandler to root logger so logfire logs also go to file
44
+ import logging
45
+
46
+ root_logger = logging.getLogger()
47
+ logfire_handler = logfire.LogfireLoggingHandler()
48
+ root_logger.addHandler(logfire_handler)
49
+ logger.debug("Added LogfireLoggingHandler to root logger for file integration")
50
+
68
51
  # Set user context using baggage for all logs and spans
69
52
  try:
53
+ import asyncio
54
+
70
55
  from opentelemetry import baggage, context
71
56
 
72
57
  from shotgun.agents.config import get_config_manager
73
58
 
74
59
  config_manager = get_config_manager()
75
- user_id = config_manager.get_user_id()
60
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
76
61
 
77
- # Set user_id as baggage in global context - this will be included in all logs/spans
78
- ctx = baggage.set_baggage("user_id", user_id)
62
+ # Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
63
+ ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
79
64
  context.attach(ctx)
80
- logger.debug("Logfire user context set with user_id: %s", user_id)
65
+ logger.debug(
66
+ "Logfire user context set with shotgun_instance_id: %s",
67
+ shotgun_instance_id,
68
+ )
81
69
  except Exception as e:
82
70
  logger.warning("Failed to set Logfire user context: %s", e)
83
71