shotgun-sh 0.2.1.dev3__py3-none-any.whl → 0.2.1.dev5__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.

@@ -4,7 +4,8 @@ from enum import StrEnum, auto
4
4
 
5
5
  # Field names
6
6
  API_KEY_FIELD = "api_key"
7
- USER_ID_FIELD = "user_id"
7
+ SUPABASE_JWT_FIELD = "supabase_jwt"
8
+ SHOTGUN_INSTANCE_ID_FIELD = "shotgun_instance_id"
8
9
  CONFIG_VERSION_FIELD = "config_version"
9
10
 
10
11
 
@@ -12,6 +12,8 @@ from shotgun.utils import get_shotgun_home
12
12
 
13
13
  from .constants import (
14
14
  API_KEY_FIELD,
15
+ SHOTGUN_INSTANCE_ID_FIELD,
16
+ SUPABASE_JWT_FIELD,
15
17
  ConfigSection,
16
18
  )
17
19
  from .models import (
@@ -46,21 +48,24 @@ class ConfigManager:
46
48
 
47
49
  self._config: ShotgunConfig | None = None
48
50
 
49
- def load(self) -> ShotgunConfig:
51
+ def load(self, force_reload: bool = True) -> ShotgunConfig:
50
52
  """Load configuration from file.
51
53
 
54
+ Args:
55
+ force_reload: If True, reload from disk even if cached (default: True)
56
+
52
57
  Returns:
53
58
  ShotgunConfig: Loaded configuration or default config if file doesn't exist
54
59
  """
55
- if self._config is not None:
60
+ if self._config is not None and not force_reload:
56
61
  return self._config
57
62
 
58
63
  if not self.config_path.exists():
59
64
  logger.info(
60
- "Configuration file not found, creating new config with user_id: %s",
65
+ "Configuration file not found, creating new config at: %s",
61
66
  self.config_path,
62
67
  )
63
- # Create new config with generated user_id
68
+ # Create new config with generated shotgun_instance_id
64
69
  self._config = self.initialize()
65
70
  return self._config
66
71
 
@@ -68,6 +73,14 @@ class ConfigManager:
68
73
  with open(self.config_path, encoding="utf-8") as f:
69
74
  data = json.load(f)
70
75
 
76
+ # Migration: Rename user_id to shotgun_instance_id (config v2 -> v3)
77
+ if "user_id" in data and SHOTGUN_INSTANCE_ID_FIELD not in data:
78
+ data[SHOTGUN_INSTANCE_ID_FIELD] = data.pop("user_id")
79
+ data["config_version"] = 3
80
+ logger.info(
81
+ "Migrated config v2->v3: renamed user_id to shotgun_instance_id"
82
+ )
83
+
71
84
  # Convert plain text secrets to SecretStr objects
72
85
  self._convert_secrets_to_secretstr(data)
73
86
 
@@ -131,7 +144,7 @@ class ConfigManager:
131
144
  logger.error(
132
145
  "Failed to load configuration from %s: %s", self.config_path, e
133
146
  )
134
- logger.info("Creating new configuration with generated user_id")
147
+ logger.info("Creating new configuration with generated shotgun_instance_id")
135
148
  self._config = self.initialize()
136
149
  return self._config
137
150
 
@@ -145,9 +158,9 @@ class ConfigManager:
145
158
  if self._config:
146
159
  config = self._config
147
160
  else:
148
- # Create a new config with generated user_id
161
+ # Create a new config with generated shotgun_instance_id
149
162
  config = ShotgunConfig(
150
- user_id=str(uuid.uuid4()),
163
+ shotgun_instance_id=str(uuid.uuid4()),
151
164
  )
152
165
 
153
166
  # Ensure directory exists
@@ -243,7 +256,8 @@ class ConfigManager:
243
256
 
244
257
  This checks only the configuration file.
245
258
  """
246
- config = self.load()
259
+ # Use force_reload=False to avoid infinite loop when called from load()
260
+ config = self.load(force_reload=False)
247
261
  provider_enum = self._ensure_provider_enum(provider)
248
262
  provider_config = self._get_provider_config(config, provider_enum)
249
263
 
@@ -251,7 +265,8 @@ class ConfigManager:
251
265
 
252
266
  def has_any_provider_key(self) -> bool:
253
267
  """Determine whether any provider has a configured API key."""
254
- config = self.load()
268
+ # Use force_reload=False to avoid infinite loop when called from load()
269
+ config = self.load(force_reload=False)
255
270
  # Check LLM provider keys (BYOK)
256
271
  has_llm_key = any(
257
272
  self._provider_has_api_key(self._get_provider_config(config, provider))
@@ -271,15 +286,15 @@ class ConfigManager:
271
286
  Returns:
272
287
  Default ShotgunConfig
273
288
  """
274
- # Generate unique user ID for new config
289
+ # Generate unique shotgun instance ID for new config
275
290
  config = ShotgunConfig(
276
- user_id=str(uuid.uuid4()),
291
+ shotgun_instance_id=str(uuid.uuid4()),
277
292
  )
278
293
  self.save(config)
279
294
  logger.info(
280
- "Configuration initialized at %s with user_id: %s",
295
+ "Configuration initialized at %s with shotgun_instance_id: %s",
281
296
  self.config_path,
282
- config.user_id,
297
+ config.shotgun_instance_id,
283
298
  )
284
299
  return config
285
300
 
@@ -287,6 +302,7 @@ class ConfigManager:
287
302
  """Convert plain text secrets in data to SecretStr objects."""
288
303
  for section in ConfigSection:
289
304
  if section.value in data and isinstance(data[section.value], dict):
305
+ # Convert API key
290
306
  if (
291
307
  API_KEY_FIELD in data[section.value]
292
308
  and data[section.value][API_KEY_FIELD] is not None
@@ -294,11 +310,21 @@ class ConfigManager:
294
310
  data[section.value][API_KEY_FIELD] = SecretStr(
295
311
  data[section.value][API_KEY_FIELD]
296
312
  )
313
+ # Convert supabase JWT (shotgun section only)
314
+ if (
315
+ section == ConfigSection.SHOTGUN
316
+ and SUPABASE_JWT_FIELD in data[section.value]
317
+ and data[section.value][SUPABASE_JWT_FIELD] is not None
318
+ ):
319
+ data[section.value][SUPABASE_JWT_FIELD] = SecretStr(
320
+ data[section.value][SUPABASE_JWT_FIELD]
321
+ )
297
322
 
298
323
  def _convert_secretstr_to_plain(self, data: dict[str, Any]) -> None:
299
324
  """Convert SecretStr objects in data to plain text for JSON serialization."""
300
325
  for section in ConfigSection:
301
326
  if section.value in data and isinstance(data[section.value], dict):
327
+ # Convert API key
302
328
  if (
303
329
  API_KEY_FIELD in data[section.value]
304
330
  and data[section.value][API_KEY_FIELD] is not None
@@ -307,6 +333,18 @@ class ConfigManager:
307
333
  data[section.value][API_KEY_FIELD] = data[section.value][
308
334
  API_KEY_FIELD
309
335
  ].get_secret_value()
336
+ # Convert supabase JWT (shotgun section only)
337
+ if (
338
+ section == ConfigSection.SHOTGUN
339
+ and SUPABASE_JWT_FIELD in data[section.value]
340
+ and data[section.value][SUPABASE_JWT_FIELD] is not None
341
+ ):
342
+ if hasattr(
343
+ data[section.value][SUPABASE_JWT_FIELD], "get_secret_value"
344
+ ):
345
+ data[section.value][SUPABASE_JWT_FIELD] = data[section.value][
346
+ SUPABASE_JWT_FIELD
347
+ ].get_secret_value()
310
348
 
311
349
  def _ensure_provider_enum(self, provider: ProviderType | str) -> ProviderType:
312
350
  """Normalize provider values to ProviderType enum."""
@@ -371,16 +409,49 @@ class ConfigManager:
371
409
  provider_enum = self._ensure_provider_enum(provider)
372
410
  return (self._get_provider_config(config, provider_enum), False)
373
411
 
374
- def get_user_id(self) -> str:
375
- """Get the user ID from configuration.
412
+ def get_shotgun_instance_id(self) -> str:
413
+ """Get the shotgun instance ID from configuration.
376
414
 
377
415
  Returns:
378
- The unique user ID string
416
+ The unique shotgun instance ID string
379
417
  """
380
418
  config = self.load()
381
- return config.user_id
419
+ return config.shotgun_instance_id
420
+
421
+ def update_shotgun_account(
422
+ self, api_key: str | None = None, supabase_jwt: str | None = None
423
+ ) -> None:
424
+ """Update Shotgun Account configuration.
425
+
426
+ Args:
427
+ api_key: LiteLLM proxy API key (optional)
428
+ supabase_jwt: Supabase authentication JWT (optional)
429
+ """
430
+ config = self.load()
431
+
432
+ if api_key is not None:
433
+ config.shotgun.api_key = SecretStr(api_key) if api_key else None
434
+
435
+ if supabase_jwt is not None:
436
+ config.shotgun.supabase_jwt = (
437
+ SecretStr(supabase_jwt) if supabase_jwt else None
438
+ )
439
+
440
+ self.save(config)
441
+ logger.info("Updated Shotgun Account configuration")
442
+
443
+
444
+ # Global singleton instance
445
+ _config_manager_instance: ConfigManager | None = None
382
446
 
383
447
 
384
448
  def get_config_manager() -> ConfigManager:
385
- """Get the global ConfigManager instance."""
386
- return ConfigManager()
449
+ """Get the global singleton ConfigManager instance.
450
+
451
+ Returns:
452
+ The singleton ConfigManager instance
453
+ """
454
+ global _config_manager_instance
455
+ if _config_manager_instance is None:
456
+ _config_manager_instance = ConfigManager()
457
+ return _config_manager_instance
@@ -149,6 +149,9 @@ class ShotgunAccountConfig(BaseModel):
149
149
  """Configuration for Shotgun Account (LiteLLM proxy)."""
150
150
 
151
151
  api_key: SecretStr | None = None
152
+ supabase_jwt: SecretStr | None = Field(
153
+ default=None, description="Supabase authentication JWT"
154
+ )
152
155
 
153
156
 
154
157
  class ShotgunConfig(BaseModel):
@@ -162,5 +165,11 @@ class ShotgunConfig(BaseModel):
162
165
  default=None,
163
166
  description="User-selected model",
164
167
  )
165
- user_id: str = Field(description="Unique anonymous user identifier")
166
- config_version: int = Field(default=2, description="Configuration schema version")
168
+ shotgun_instance_id: str = Field(
169
+ description="Unique shotgun instance identifier (also used for anonymous telemetry)",
170
+ )
171
+ config_version: int = Field(default=3, description="Configuration schema version")
172
+ shown_welcome_screen: bool = Field(
173
+ default=False,
174
+ description="Whether the welcome screen has been shown to the user",
175
+ )
@@ -139,7 +139,8 @@ def get_provider_model(
139
139
  ValueError: If provider is not configured properly or model not found
140
140
  """
141
141
  config_manager = get_config_manager()
142
- config = config_manager.load()
142
+ # Use cached config for read-only access (performance)
143
+ config = config_manager.load(force_reload=False)
143
144
 
144
145
  # Priority 1: Check if Shotgun key exists - if so, use it for ANY model
145
146
  shotgun_api_key = _get_api_key(config.shotgun.api_key)
shotgun/cli/config.py CHANGED
@@ -233,14 +233,14 @@ def _mask_value(value: str) -> str:
233
233
 
234
234
 
235
235
  @app.command()
236
- def get_user_id() -> None:
237
- """Get the anonymous user ID from configuration."""
236
+ def get_shotgun_instance_id() -> None:
237
+ """Get the anonymous shotgun instance ID from configuration."""
238
238
  config_manager = get_config_manager()
239
239
 
240
240
  try:
241
- user_id = config_manager.get_user_id()
242
- console.print(f"[green]User ID:[/green] {user_id}")
241
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
242
+ console.print(f"[green]Shotgun Instance ID:[/green] {shotgun_instance_id}")
243
243
  except Exception as e:
244
- logger.error(f"Error getting user ID: {e}")
245
- console.print(f"❌ Failed to get user ID: {str(e)}", style="red")
244
+ logger.error(f"Error getting shotgun instance ID: {e}")
245
+ console.print(f"❌ Failed to get shotgun instance ID: {str(e)}", style="red")
246
246
  raise typer.Exit(1) from e
shotgun/cli/feedback.py CHANGED
@@ -30,7 +30,7 @@ def send_feedback(
30
30
  """Initialize Shotgun configuration."""
31
31
  config_manager = get_config_manager()
32
32
  config_manager.load()
33
- user_id = config_manager.get_user_id()
33
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
34
34
 
35
35
  if not description:
36
36
  console.print(
@@ -39,7 +39,9 @@ def send_feedback(
39
39
  )
40
40
  raise typer.Exit(1)
41
41
 
42
- feedback = Feedback(kind=kind, description=description, user_id=user_id)
42
+ feedback = Feedback(
43
+ kind=kind, description=description, shotgun_instance_id=shotgun_instance_id
44
+ )
43
45
 
44
46
  submit_feedback_survey(feedback)
45
47
 
@@ -18,15 +18,12 @@ from shotgun.logging_config import get_logger
18
18
  logger = get_logger(__name__)
19
19
 
20
20
 
21
- # Default ignore patterns
22
- IGNORE_PATTERNS = {
21
+ # Directories that should never be traversed during indexing
22
+ BASE_IGNORE_DIRECTORIES = {
23
23
  ".git",
24
24
  "venv",
25
25
  ".venv",
26
26
  "__pycache__",
27
- "node_modules",
28
- "build",
29
- "dist",
30
27
  ".eggs",
31
28
  ".pytest_cache",
32
29
  ".mypy_cache",
@@ -36,6 +33,29 @@ IGNORE_PATTERNS = {
36
33
  ".vscode",
37
34
  }
38
35
 
36
+ # Well-known build output directories to skip when determining source files
37
+ BUILD_ARTIFACT_DIRECTORIES = {
38
+ "node_modules",
39
+ ".next",
40
+ ".nuxt",
41
+ ".vite",
42
+ ".yarn",
43
+ ".svelte-kit",
44
+ ".output",
45
+ ".turbo",
46
+ ".parcel-cache",
47
+ ".vercel",
48
+ ".serverless",
49
+ "build",
50
+ "dist",
51
+ "out",
52
+ "tmp",
53
+ "coverage",
54
+ }
55
+
56
+ # Default ignore patterns combines base directories and build artifacts
57
+ IGNORE_PATTERNS = BASE_IGNORE_DIRECTORIES | BUILD_ARTIFACT_DIRECTORIES
58
+
39
59
 
40
60
  class Ingestor:
41
61
  """Handles all communication and ingestion with the Kuzu database."""
@@ -51,14 +51,14 @@ def setup_posthog_observability() -> bool:
51
51
  # Store the client for later use
52
52
  _posthog_client = posthog
53
53
 
54
- # Set user context with anonymous user ID from config
54
+ # Set user context with anonymous shotgun instance ID from config
55
55
  try:
56
56
  config_manager = get_config_manager()
57
- user_id = config_manager.get_user_id()
57
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
58
58
 
59
59
  # Identify the user in PostHog
60
60
  posthog.identify( # type: ignore[attr-defined]
61
- distinct_id=user_id,
61
+ distinct_id=shotgun_instance_id,
62
62
  properties={
63
63
  "version": __version__,
64
64
  "environment": environment,
@@ -69,7 +69,9 @@ def setup_posthog_observability() -> bool:
69
69
  posthog.disabled = False
70
70
  posthog.personal_api_key = None # Not needed for event tracking
71
71
 
72
- logger.debug("PostHog user identified with anonymous ID: %s", user_id)
72
+ logger.debug(
73
+ "PostHog user identified with anonymous ID: %s", shotgun_instance_id
74
+ )
73
75
  except Exception as e:
74
76
  logger.warning("Failed to set user context: %s", e)
75
77
 
@@ -99,9 +101,9 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
99
101
  return
100
102
 
101
103
  try:
102
- # Get user ID for tracking
104
+ # Get shotgun instance ID for tracking
103
105
  config_manager = get_config_manager()
104
- user_id = config_manager.get_user_id()
106
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
105
107
 
106
108
  # Add version and environment to properties
107
109
  if properties is None:
@@ -116,7 +118,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
116
118
 
117
119
  # Track the event using PostHog's capture method
118
120
  _posthog_client.capture(
119
- distinct_id=user_id, event=event_name, properties=properties
121
+ distinct_id=shotgun_instance_id, event=event_name, properties=properties
120
122
  )
121
123
  logger.debug("Tracked PostHog event: %s", event_name)
122
124
  except Exception as e:
@@ -146,7 +148,7 @@ class FeedbackKind(StrEnum):
146
148
  class Feedback(BaseModel):
147
149
  kind: FeedbackKind
148
150
  description: str
149
- user_id: str
151
+ shotgun_instance_id: str
150
152
 
151
153
 
152
154
  SURVEY_ID = "01999f81-9486-0000-4fa6-9632959f92f3"
@@ -59,13 +59,13 @@ def setup_sentry_observability() -> bool:
59
59
  profiles_sample_rate=0.1 if environment == "production" else 1.0,
60
60
  )
61
61
 
62
- # Set user context with anonymous user ID from config
62
+ # Set user context with anonymous shotgun instance ID from config
63
63
  try:
64
64
  from shotgun.agents.config import get_config_manager
65
65
 
66
66
  config_manager = get_config_manager()
67
- user_id = config_manager.get_user_id()
68
- sentry_sdk.set_user({"id": user_id})
67
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
68
+ sentry_sdk.set_user({"id": shotgun_instance_id})
69
69
  logger.debug("Sentry user context set with anonymous ID")
70
70
  except Exception as e:
71
71
  logger.warning("Failed to set Sentry user context: %s", e)
@@ -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,17 @@
1
+ """Constants for Shotgun Web API."""
2
+
3
+ import os
4
+
5
+ # Shotgun Web API base URL
6
+ # Default to production URL, can be overridden with environment variable
7
+ SHOTGUN_WEB_BASE_URL = os.environ.get(
8
+ "SHOTGUN_WEB_BASE_URL", "https://api-701197220809.us-east1.run.app"
9
+ )
10
+
11
+ # API endpoints
12
+ UNIFICATION_TOKEN_CREATE_PATH = "/api/unification/token/create" # noqa: S105
13
+ UNIFICATION_TOKEN_STATUS_PATH = "/api/unification/token/{token}/status" # noqa: S105
14
+
15
+ # Polling configuration
16
+ DEFAULT_POLL_INTERVAL_SECONDS = 3
17
+ DEFAULT_TOKEN_TIMEOUT_SECONDS = 1800 # 30 minutes
@@ -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
@@ -72,12 +72,15 @@ def setup_logfire_observability() -> bool:
72
72
  from shotgun.agents.config import get_config_manager
73
73
 
74
74
  config_manager = get_config_manager()
75
- user_id = config_manager.get_user_id()
75
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
76
76
 
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)
77
+ # Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
78
+ ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
79
79
  context.attach(ctx)
80
- logger.debug("Logfire user context set with user_id: %s", user_id)
80
+ logger.debug(
81
+ "Logfire user context set with shotgun_instance_id: %s",
82
+ shotgun_instance_id,
83
+ )
81
84
  except Exception as e:
82
85
  logger.warning("Failed to set Logfire user context: %s", e)
83
86