shotgun-sh 0.1.16.dev2__py3-none-any.whl → 0.2.1__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 (55) hide show
  1. shotgun/agents/common.py +4 -5
  2. shotgun/agents/config/constants.py +23 -6
  3. shotgun/agents/config/manager.py +239 -76
  4. shotgun/agents/config/models.py +74 -84
  5. shotgun/agents/config/provider.py +174 -85
  6. shotgun/agents/history/compaction.py +1 -1
  7. shotgun/agents/history/history_processors.py +18 -9
  8. shotgun/agents/history/token_counting/__init__.py +31 -0
  9. shotgun/agents/history/token_counting/anthropic.py +89 -0
  10. shotgun/agents/history/token_counting/base.py +67 -0
  11. shotgun/agents/history/token_counting/openai.py +80 -0
  12. shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
  13. shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
  14. shotgun/agents/history/token_counting/utils.py +147 -0
  15. shotgun/agents/history/token_estimation.py +12 -12
  16. shotgun/agents/llm.py +62 -0
  17. shotgun/agents/models.py +2 -2
  18. shotgun/agents/tools/web_search/__init__.py +42 -15
  19. shotgun/agents/tools/web_search/anthropic.py +54 -50
  20. shotgun/agents/tools/web_search/gemini.py +31 -20
  21. shotgun/agents/tools/web_search/openai.py +4 -4
  22. shotgun/build_constants.py +2 -2
  23. shotgun/cli/config.py +34 -63
  24. shotgun/cli/feedback.py +4 -2
  25. shotgun/cli/models.py +2 -2
  26. shotgun/codebase/core/ingestor.py +47 -8
  27. shotgun/codebase/core/manager.py +7 -3
  28. shotgun/codebase/models.py +4 -4
  29. shotgun/llm_proxy/__init__.py +16 -0
  30. shotgun/llm_proxy/clients.py +39 -0
  31. shotgun/llm_proxy/constants.py +8 -0
  32. shotgun/main.py +6 -0
  33. shotgun/posthog_telemetry.py +15 -11
  34. shotgun/sentry_telemetry.py +3 -3
  35. shotgun/shotgun_web/__init__.py +19 -0
  36. shotgun/shotgun_web/client.py +138 -0
  37. shotgun/shotgun_web/constants.py +17 -0
  38. shotgun/shotgun_web/models.py +47 -0
  39. shotgun/telemetry.py +7 -4
  40. shotgun/tui/app.py +26 -8
  41. shotgun/tui/screens/chat.py +2 -8
  42. shotgun/tui/screens/chat_screen/command_providers.py +118 -11
  43. shotgun/tui/screens/chat_screen/history.py +3 -1
  44. shotgun/tui/screens/feedback.py +2 -2
  45. shotgun/tui/screens/model_picker.py +327 -0
  46. shotgun/tui/screens/provider_config.py +118 -28
  47. shotgun/tui/screens/shotgun_auth.py +295 -0
  48. shotgun/tui/screens/welcome.py +176 -0
  49. shotgun/utils/env_utils.py +12 -0
  50. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/METADATA +2 -2
  51. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/RECORD +54 -37
  52. shotgun/agents/history/token_counting.py +0 -429
  53. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/WHEEL +0 -0
  54. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/entry_points.txt +0 -0
  55. {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
12
12
  POSTHOG_PROJECT_ID = '191396'
13
13
 
14
14
  # Logfire configuration embedded at build time (only for dev builds)
15
- LOGFIRE_ENABLED = 'true'
16
- LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
15
+ LOGFIRE_ENABLED = ''
16
+ LOGFIRE_TOKEN = ''
17
17
 
18
18
  # Build metadata
19
19
  BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
shotgun/cli/config.py CHANGED
@@ -9,6 +9,7 @@ from rich.table import Table
9
9
 
10
10
  from shotgun.agents.config import ProviderType, get_config_manager
11
11
  from shotgun.logging_config import get_logger
12
+ from shotgun.utils.env_utils import is_shotgun_account_enabled
12
13
 
13
14
  logger = get_logger(__name__)
14
15
  console = Console()
@@ -43,11 +44,11 @@ def init(
43
44
  console.print()
44
45
 
45
46
  # Initialize with defaults
46
- config = config_manager.initialize()
47
+ config_manager.initialize()
47
48
 
48
- # Ask for default provider
49
+ # Ask for provider
49
50
  provider_choices = ["openai", "anthropic", "google"]
50
- console.print("Choose your default AI provider:")
51
+ console.print("Choose your AI provider:")
51
52
  for i, provider in enumerate(provider_choices, 1):
52
53
  console.print(f" {i}. {provider}")
53
54
 
@@ -55,7 +56,7 @@ def init(
55
56
  try:
56
57
  choice = typer.prompt("Enter choice (1-3)", type=int)
57
58
  if 1 <= choice <= 3:
58
- config.default_provider = ProviderType(provider_choices[choice - 1])
59
+ provider = ProviderType(provider_choices[choice - 1])
59
60
  break
60
61
  else:
61
62
  console.print(
@@ -65,7 +66,6 @@ def init(
65
66
  console.print("❌ Please enter a valid number.", style="red")
66
67
 
67
68
  # Ask for API key for the selected provider
68
- provider = config.default_provider
69
69
  console.print(f"\n🔑 Setting up {provider.upper()} API key...")
70
70
 
71
71
  api_key = typer.prompt(
@@ -75,9 +75,9 @@ def init(
75
75
  )
76
76
 
77
77
  if api_key:
78
+ # update_provider will automatically set selected_model for first provider
78
79
  config_manager.update_provider(provider, api_key=api_key)
79
80
 
80
- config_manager.save()
81
81
  console.print(
82
82
  f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
83
83
  )
@@ -98,16 +98,12 @@ def set(
98
98
  str | None,
99
99
  typer.Option("--api-key", "-k", help="API key for the provider"),
100
100
  ] = None,
101
- default: Annotated[
102
- bool,
103
- typer.Option("--default", "-d", help="Set this provider as default"),
104
- ] = False,
105
101
  ) -> None:
106
102
  """Set configuration for a specific provider."""
107
103
  config_manager = get_config_manager()
108
104
 
109
- # If no API key provided via option and not just setting default, prompt for it
110
- if api_key is None and not default:
105
+ # If no API key provided via option, prompt for it
106
+ if api_key is None:
111
107
  api_key = typer.prompt(
112
108
  f"Enter your {provider.upper()} API key",
113
109
  hide_input=True,
@@ -118,11 +114,6 @@ def set(
118
114
  if api_key:
119
115
  config_manager.update_provider(provider, api_key=api_key)
120
116
 
121
- if default:
122
- config = config_manager.load()
123
- config.default_provider = provider
124
- config_manager.save(config)
125
-
126
117
  console.print(f"✅ Configuration updated for {provider}")
127
118
 
128
119
  except Exception as e:
@@ -130,41 +121,6 @@ def set(
130
121
  raise typer.Exit(1) from e
131
122
 
132
123
 
133
- @app.command()
134
- def set_default(
135
- provider: Annotated[
136
- ProviderType,
137
- typer.Argument(
138
- help="AI provider to set as default (openai, anthropic, google)"
139
- ),
140
- ],
141
- ) -> None:
142
- """Set the default AI provider without modifying API keys."""
143
- config_manager = get_config_manager()
144
-
145
- try:
146
- config = config_manager.load()
147
-
148
- # Check if the provider has an API key configured
149
- provider_config = getattr(config, provider.value)
150
- if not provider_config.api_key:
151
- console.print(
152
- f"⚠️ Warning: {provider.upper()} does not have an API key configured.",
153
- style="yellow",
154
- )
155
- console.print(f"Use 'shotgun config set {provider}' to configure it.")
156
-
157
- # Set as default
158
- config.default_provider = provider
159
- config_manager.save(config)
160
-
161
- console.print(f"✅ Default provider set to: {provider}")
162
-
163
- except Exception as e:
164
- console.print(f"❌ Failed to set default provider: {e}", style="red")
165
- raise typer.Exit(1) from e
166
-
167
-
168
124
  @app.command()
169
125
  def get(
170
126
  provider: Annotated[
@@ -201,16 +157,23 @@ def _show_full_config(config: Any) -> None:
201
157
  table.add_column("Setting", style="cyan")
202
158
  table.add_column("Value", style="white")
203
159
 
204
- # Default provider
205
- table.add_row("Default Provider", f"[bold]{config.default_provider}[/bold]")
160
+ # Selected model
161
+ selected_model = config.selected_model or "None (will auto-detect)"
162
+ table.add_row("Selected Model", f"[bold]{selected_model}[/bold]")
206
163
  table.add_row("", "") # Separator
207
164
 
208
165
  # Provider configurations
209
- for provider_name, provider_config in [
166
+ providers_to_show = [
210
167
  ("OpenAI", config.openai),
211
168
  ("Anthropic", config.anthropic),
212
169
  ("Google", config.google),
213
- ]:
170
+ ]
171
+
172
+ # Only show Shotgun Account if feature flag is enabled
173
+ if is_shotgun_account_enabled():
174
+ providers_to_show.append(("Shotgun Account", config.shotgun))
175
+
176
+ for provider_name, provider_config in providers_to_show:
214
177
  table.add_row(f"[bold]{provider_name}[/bold]", "")
215
178
 
216
179
  # API Key
@@ -231,6 +194,8 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
231
194
  provider_config = config.anthropic
232
195
  elif provider_str == "google":
233
196
  provider_config = config.google
197
+ elif provider_str == "shotgun":
198
+ provider_config = config.shotgun
234
199
  else:
235
200
  console.print(f"❌ Unknown provider: {provider}", style="red")
236
201
  return
@@ -248,7 +213,13 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
248
213
 
249
214
  def _mask_secrets(data: dict[str, Any]) -> None:
250
215
  """Mask secrets in configuration data."""
251
- for provider in ["openai", "anthropic", "google"]:
216
+ providers = ["openai", "anthropic", "google"]
217
+
218
+ # Only mask shotgun if feature flag is enabled
219
+ if is_shotgun_account_enabled():
220
+ providers.append("shotgun")
221
+
222
+ for provider in providers:
252
223
  if provider in data and isinstance(data[provider], dict):
253
224
  if "api_key" in data[provider] and data[provider]["api_key"]:
254
225
  data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
@@ -262,14 +233,14 @@ def _mask_value(value: str) -> str:
262
233
 
263
234
 
264
235
  @app.command()
265
- def get_user_id() -> None:
266
- """Get the anonymous user ID from configuration."""
236
+ def get_shotgun_instance_id() -> None:
237
+ """Get the anonymous shotgun instance ID from configuration."""
267
238
  config_manager = get_config_manager()
268
239
 
269
240
  try:
270
- user_id = config_manager.get_user_id()
271
- 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}")
272
243
  except Exception as e:
273
- logger.error(f"Error getting user ID: {e}")
274
- 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")
275
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
 
shotgun/cli/models.py CHANGED
@@ -1,9 +1,9 @@
1
1
  """Common models for CLI commands."""
2
2
 
3
- from enum import Enum
3
+ from enum import StrEnum
4
4
 
5
5
 
6
- class OutputFormat(str, Enum):
6
+ class OutputFormat(StrEnum):
7
7
  """Output format options for CLI commands."""
8
8
 
9
9
  TEXT = "text"
@@ -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,46 @@ 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
+
59
+ # Directory prefixes that should always be ignored
60
+ IGNORED_DIRECTORY_PREFIXES = (".",)
61
+
62
+
63
+ def should_ignore_directory(name: str, ignore_patterns: set[str] | None = None) -> bool:
64
+ """Return True if the directory name should be ignored."""
65
+ patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
66
+ if name in patterns:
67
+ return True
68
+ return name.startswith(IGNORED_DIRECTORY_PREFIXES)
69
+
70
+
71
+ def is_path_ignored(path: Path, ignore_patterns: set[str] | None = None) -> bool:
72
+ """Return True if any part of the path should be ignored."""
73
+ patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
74
+ return any(should_ignore_directory(part, patterns) for part in path.parts)
75
+
39
76
 
40
77
  class Ingestor:
41
78
  """Handles all communication and ingestion with the Kuzu database."""
@@ -607,7 +644,9 @@ class SimpleGraphBuilder:
607
644
  """First pass: Walk directory to find packages and folders."""
608
645
  dir_count = 0
609
646
  for root_str, dirs, _ in os.walk(self.repo_path, topdown=True):
610
- dirs[:] = [d for d in dirs if d not in self.ignore_dirs]
647
+ dirs[:] = [
648
+ d for d in dirs if not should_ignore_directory(d, self.ignore_dirs)
649
+ ]
611
650
  root = Path(root_str)
612
651
  relative_root = root.relative_to(self.repo_path)
613
652
 
@@ -740,7 +779,7 @@ class SimpleGraphBuilder:
740
779
  root = Path(root_str)
741
780
 
742
781
  # Skip ignored directories
743
- if any(part in self.ignore_dirs for part in root.parts):
782
+ if is_path_ignored(root, self.ignore_dirs):
744
783
  continue
745
784
 
746
785
  for filename in files:
@@ -757,7 +796,7 @@ class SimpleGraphBuilder:
757
796
  root = Path(root_str)
758
797
 
759
798
  # Skip ignored directories
760
- if any(part in self.ignore_dirs for part in root.parts):
799
+ if is_path_ignored(root, self.ignore_dirs):
761
800
  continue
762
801
 
763
802
  for filename in files:
@@ -51,9 +51,13 @@ class CodebaseFileHandler(FileSystemEventHandler):
51
51
  self.pending_changes: list[FileChange] = []
52
52
  self._lock = anyio.Lock()
53
53
  # Import default ignore patterns from ingestor
54
- from shotgun.codebase.core.ingestor import IGNORE_PATTERNS
54
+ from shotgun.codebase.core.ingestor import (
55
+ IGNORE_PATTERNS,
56
+ should_ignore_directory,
57
+ )
55
58
 
56
59
  self.ignore_patterns = ignore_patterns or IGNORE_PATTERNS
60
+ self._should_ignore_directory = should_ignore_directory
57
61
 
58
62
  def on_any_event(self, event: FileSystemEvent) -> None:
59
63
  """Handle any file system event."""
@@ -71,7 +75,7 @@ class CodebaseFileHandler(FileSystemEventHandler):
71
75
 
72
76
  # Check if any parent directory should be ignored
73
77
  for parent in path.parents:
74
- if parent.name in self.ignore_patterns:
78
+ if self._should_ignore_directory(parent.name, self.ignore_patterns):
75
79
  logger.debug(
76
80
  f"Ignoring file in ignored directory: {parent.name} - path: {src_path_str}"
77
81
  )
@@ -106,7 +110,7 @@ class CodebaseFileHandler(FileSystemEventHandler):
106
110
  )
107
111
  dest_path = Path(dest_path_str)
108
112
  for parent in dest_path.parents:
109
- if parent.name in self.ignore_patterns:
113
+ if self._should_ignore_directory(parent.name, self.ignore_patterns):
110
114
  logger.debug(
111
115
  f"Ignoring move to ignored directory: {parent.name} - dest_path: {dest_path_str}"
112
116
  )
@@ -1,13 +1,13 @@
1
1
  """Data models for codebase service."""
2
2
 
3
3
  from collections.abc import Callable
4
- from enum import Enum
4
+ from enum import StrEnum
5
5
  from typing import Any
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
 
9
9
 
10
- class GraphStatus(str, Enum):
10
+ class GraphStatus(StrEnum):
11
11
  """Status of a code knowledge graph."""
12
12
 
13
13
  READY = "READY" # Graph is ready for queries
@@ -16,14 +16,14 @@ class GraphStatus(str, Enum):
16
16
  ERROR = "ERROR" # Last operation failed
17
17
 
18
18
 
19
- class QueryType(str, Enum):
19
+ class QueryType(StrEnum):
20
20
  """Type of query being executed."""
21
21
 
22
22
  NATURAL_LANGUAGE = "natural_language"
23
23
  CYPHER = "cypher"
24
24
 
25
25
 
26
- class ProgressPhase(str, Enum):
26
+ class ProgressPhase(StrEnum):
27
27
  """Phase of codebase indexing progress."""
28
28
 
29
29
  STRUCTURE = "structure" # Identifying packages and folders
@@ -0,0 +1,16 @@
1
+ """LiteLLM proxy client utilities and configuration."""
2
+
3
+ from .clients import create_anthropic_proxy_client, create_litellm_provider
4
+ from .constants import (
5
+ LITELLM_PROXY_ANTHROPIC_BASE,
6
+ LITELLM_PROXY_BASE_URL,
7
+ LITELLM_PROXY_OPENAI_BASE,
8
+ )
9
+
10
+ __all__ = [
11
+ "LITELLM_PROXY_BASE_URL",
12
+ "LITELLM_PROXY_ANTHROPIC_BASE",
13
+ "LITELLM_PROXY_OPENAI_BASE",
14
+ "create_litellm_provider",
15
+ "create_anthropic_proxy_client",
16
+ ]
@@ -0,0 +1,39 @@
1
+ """Client creation utilities for LiteLLM proxy."""
2
+
3
+ from anthropic import Anthropic
4
+ from pydantic_ai.providers.litellm import LiteLLMProvider
5
+
6
+ from .constants import LITELLM_PROXY_ANTHROPIC_BASE, LITELLM_PROXY_BASE_URL
7
+
8
+
9
+ def create_litellm_provider(api_key: str) -> LiteLLMProvider:
10
+ """Create LiteLLM provider for Shotgun Account.
11
+
12
+ Args:
13
+ api_key: Shotgun API key
14
+
15
+ Returns:
16
+ Configured LiteLLM provider pointing to Shotgun's proxy
17
+ """
18
+ return LiteLLMProvider(
19
+ api_base=LITELLM_PROXY_BASE_URL,
20
+ api_key=api_key,
21
+ )
22
+
23
+
24
+ def create_anthropic_proxy_client(api_key: str) -> Anthropic:
25
+ """Create Anthropic client configured for LiteLLM proxy.
26
+
27
+ This client will proxy token counting requests through the
28
+ LiteLLM proxy to Anthropic's actual token counting API.
29
+
30
+ Args:
31
+ api_key: Shotgun API key
32
+
33
+ Returns:
34
+ Anthropic client configured to use LiteLLM proxy
35
+ """
36
+ return Anthropic(
37
+ api_key=api_key,
38
+ base_url=LITELLM_PROXY_ANTHROPIC_BASE,
39
+ )
@@ -0,0 +1,8 @@
1
+ """LiteLLM proxy constants and configuration."""
2
+
3
+ # Shotgun's LiteLLM proxy base URL
4
+ LITELLM_PROXY_BASE_URL = "https://litellm-701197220809.us-east1.run.app"
5
+
6
+ # Provider-specific endpoints
7
+ LITELLM_PROXY_ANTHROPIC_BASE = f"{LITELLM_PROXY_BASE_URL}/anthropic"
8
+ LITELLM_PROXY_OPENAI_BASE = LITELLM_PROXY_BASE_URL
shotgun/main.py CHANGED
@@ -1,5 +1,11 @@
1
1
  """Main CLI application for shotgun."""
2
2
 
3
+ # NOTE: These are before we import any Google library to stop the noisy gRPC logs.
4
+ import os # noqa: I001
5
+
6
+ os.environ["GRPC_VERBOSITY"] = "ERROR"
7
+ os.environ["GLOG_minloglevel"] = "2"
8
+
3
9
  import logging
4
10
 
5
11
  # CRITICAL: Add NullHandler to root logger before ANY other imports.
@@ -1,6 +1,6 @@
1
1
  """PostHog analytics setup for Shotgun."""
2
2
 
3
- from enum import Enum
3
+ from enum import StrEnum
4
4
  from typing import Any
5
5
 
6
6
  import posthog
@@ -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:
@@ -137,7 +139,7 @@ def shutdown() -> None:
137
139
  _posthog_client = None
138
140
 
139
141
 
140
- class FeedbackKind(str, Enum):
142
+ class FeedbackKind(StrEnum):
141
143
  BUG = "bug"
142
144
  FEATURE = "feature"
143
145
  OTHER = "other"
@@ -146,7 +148,7 @@ class FeedbackKind(str, Enum):
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"
@@ -178,7 +180,9 @@ def submit_feedback_survey(feedback: Feedback) -> None:
178
180
  ],
179
181
  f"$survey_response_{Q_KIND_ID}": feedback.kind,
180
182
  f"$survey_response_{Q_DESCRIPTION_ID}": feedback.description,
181
- "provider": config.default_provider.value,
183
+ "selected_model": config.selected_model.value
184
+ if config.selected_model
185
+ else None,
182
186
  "config_version": config.config_version,
183
187
  "last_10_messages": last_10_messages, # last 10 messages
184
188
  },
@@ -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
+ ]