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,5 +1,6 @@
1
1
  """Kuzu graph ingestor for building code knowledge graphs."""
2
2
 
3
+ import asyncio
3
4
  import hashlib
4
5
  import os
5
6
  import time
@@ -8,6 +9,7 @@ from collections import defaultdict
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
12
+ import aiofiles
11
13
  import kuzu
12
14
  from tree_sitter import Node, Parser, QueryCursor
13
15
 
@@ -18,15 +20,12 @@ from shotgun.logging_config import get_logger
18
20
  logger = get_logger(__name__)
19
21
 
20
22
 
21
- # Default ignore patterns
22
- IGNORE_PATTERNS = {
23
+ # Directories that should never be traversed during indexing
24
+ BASE_IGNORE_DIRECTORIES = {
23
25
  ".git",
24
26
  "venv",
25
27
  ".venv",
26
28
  "__pycache__",
27
- "node_modules",
28
- "build",
29
- "dist",
30
29
  ".eggs",
31
30
  ".pytest_cache",
32
31
  ".mypy_cache",
@@ -36,6 +35,46 @@ IGNORE_PATTERNS = {
36
35
  ".vscode",
37
36
  }
38
37
 
38
+ # Well-known build output directories to skip when determining source files
39
+ BUILD_ARTIFACT_DIRECTORIES = {
40
+ "node_modules",
41
+ ".next",
42
+ ".nuxt",
43
+ ".vite",
44
+ ".yarn",
45
+ ".svelte-kit",
46
+ ".output",
47
+ ".turbo",
48
+ ".parcel-cache",
49
+ ".vercel",
50
+ ".serverless",
51
+ "build",
52
+ "dist",
53
+ "out",
54
+ "tmp",
55
+ "coverage",
56
+ }
57
+
58
+ # Default ignore patterns combines base directories and build artifacts
59
+ IGNORE_PATTERNS = BASE_IGNORE_DIRECTORIES | BUILD_ARTIFACT_DIRECTORIES
60
+
61
+ # Directory prefixes that should always be ignored
62
+ IGNORED_DIRECTORY_PREFIXES = (".",)
63
+
64
+
65
+ def should_ignore_directory(name: str, ignore_patterns: set[str] | None = None) -> bool:
66
+ """Return True if the directory name should be ignored."""
67
+ patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
68
+ if name in patterns:
69
+ return True
70
+ return name.startswith(IGNORED_DIRECTORY_PREFIXES)
71
+
72
+
73
+ def is_path_ignored(path: Path, ignore_patterns: set[str] | None = None) -> bool:
74
+ """Return True if any part of the path should be ignored."""
75
+ patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
76
+ return any(should_ignore_directory(part, patterns) for part in path.parts)
77
+
39
78
 
40
79
  class Ingestor:
41
80
  """Handles all communication and ingestion with the Kuzu database."""
@@ -582,7 +621,7 @@ class SimpleGraphBuilder:
582
621
  # Don't let progress callback errors crash the build
583
622
  logger.debug(f"Progress callback error: {e}")
584
623
 
585
- def run(self) -> None:
624
+ async def run(self) -> None:
586
625
  """Run the three-pass graph building process."""
587
626
  logger.info(f"Building graph for project: {self.project_name}")
588
627
 
@@ -592,7 +631,7 @@ class SimpleGraphBuilder:
592
631
 
593
632
  # Pass 2: Definitions
594
633
  logger.info("Pass 2: Processing files and extracting definitions...")
595
- self._process_files()
634
+ await self._process_files()
596
635
 
597
636
  # Pass 3: Relationships
598
637
  logger.info("Pass 3: Processing relationships (calls, imports)...")
@@ -607,7 +646,9 @@ class SimpleGraphBuilder:
607
646
  """First pass: Walk directory to find packages and folders."""
608
647
  dir_count = 0
609
648
  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]
649
+ dirs[:] = [
650
+ d for d in dirs if not should_ignore_directory(d, self.ignore_dirs)
651
+ ]
611
652
  root = Path(root_str)
612
653
  relative_root = root.relative_to(self.repo_path)
613
654
 
@@ -732,7 +773,7 @@ class SimpleGraphBuilder:
732
773
  phase_complete=True,
733
774
  )
734
775
 
735
- def _process_files(self) -> None:
776
+ async def _process_files(self) -> None:
736
777
  """Second pass: Process files and extract definitions."""
737
778
  # First pass: Count total files
738
779
  total_files = 0
@@ -740,7 +781,7 @@ class SimpleGraphBuilder:
740
781
  root = Path(root_str)
741
782
 
742
783
  # Skip ignored directories
743
- if any(part in self.ignore_dirs for part in root.parts):
784
+ if is_path_ignored(root, self.ignore_dirs):
744
785
  continue
745
786
 
746
787
  for filename in files:
@@ -757,7 +798,7 @@ class SimpleGraphBuilder:
757
798
  root = Path(root_str)
758
799
 
759
800
  # Skip ignored directories
760
- if any(part in self.ignore_dirs for part in root.parts):
801
+ if is_path_ignored(root, self.ignore_dirs):
761
802
  continue
762
803
 
763
804
  for filename in files:
@@ -768,7 +809,7 @@ class SimpleGraphBuilder:
768
809
  lang_config = get_language_config(ext)
769
810
 
770
811
  if lang_config and lang_config.name in self.parsers:
771
- self._process_single_file(filepath, lang_config.name)
812
+ await self._process_single_file(filepath, lang_config.name)
772
813
  file_count += 1
773
814
 
774
815
  # Report progress after each file
@@ -793,7 +834,7 @@ class SimpleGraphBuilder:
793
834
  phase_complete=True,
794
835
  )
795
836
 
796
- def _process_single_file(self, filepath: Path, language: str) -> None:
837
+ async def _process_single_file(self, filepath: Path, language: str) -> None:
797
838
  """Process a single file."""
798
839
  relative_path = filepath.relative_to(self.repo_path)
799
840
  relative_path_str = str(relative_path).replace(os.sep, "/")
@@ -834,8 +875,8 @@ class SimpleGraphBuilder:
834
875
 
835
876
  # Parse file
836
877
  try:
837
- with open(filepath, "rb") as f:
838
- content = f.read()
878
+ async with aiofiles.open(filepath, "rb") as f:
879
+ content = await f.read()
839
880
 
840
881
  parser = self.parsers[language]
841
882
  tree = parser.parse(content)
@@ -1597,7 +1638,7 @@ class CodebaseIngestor:
1597
1638
  )
1598
1639
  if self.project_name:
1599
1640
  builder.project_name = self.project_name
1600
- builder.run()
1641
+ asyncio.run(builder.run())
1601
1642
 
1602
1643
  logger.info(f"Graph successfully created at: {self.db_path}")
1603
1644
 
@@ -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
  )
@@ -367,7 +371,16 @@ class CodebaseGraphManager:
367
371
  )
368
372
  import shutil
369
373
 
370
- shutil.rmtree(graph_path)
374
+ # Handle both files and directories (kuzu v0.11.2+ uses files)
375
+ if graph_path.is_file():
376
+ graph_path.unlink() # Delete file
377
+ # Also delete WAL file if it exists
378
+ wal_path = graph_path.with_suffix(graph_path.suffix + ".wal")
379
+ if wal_path.exists():
380
+ wal_path.unlink()
381
+ logger.debug(f"Deleted WAL file: {wal_path}")
382
+ else:
383
+ shutil.rmtree(graph_path) # Delete directory
371
384
 
372
385
  # Import the builder from local core module
373
386
  from shotgun.codebase.core import CodebaseIngestor
@@ -756,7 +769,7 @@ class CodebaseGraphManager:
756
769
 
757
770
  lang_config = get_language_config(full_path.suffix)
758
771
  if lang_config and lang_config.name in parsers:
759
- builder._process_single_file(full_path, lang_config.name)
772
+ await builder._process_single_file(full_path, lang_config.name)
760
773
  stats["nodes_modified"] += 1 # Approximate
761
774
 
762
775
  # Process additions
@@ -771,7 +784,7 @@ class CodebaseGraphManager:
771
784
 
772
785
  lang_config = get_language_config(full_path.suffix)
773
786
  if lang_config and lang_config.name in parsers:
774
- builder._process_single_file(full_path, lang_config.name)
787
+ await builder._process_single_file(full_path, lang_config.name)
775
788
  stats["nodes_added"] += 1 # Approximate
776
789
 
777
790
  # Flush all pending operations
@@ -1738,7 +1751,7 @@ class CodebaseGraphManager:
1738
1751
  )
1739
1752
 
1740
1753
  # Build the graph
1741
- builder.run()
1754
+ asyncio.run(builder.run())
1742
1755
 
1743
1756
  # Run build in thread pool
1744
1757
  await anyio.to_thread.run_sync(_build_graph)
@@ -34,7 +34,7 @@ async def llm_cypher_prompt(
34
34
  Returns:
35
35
  CypherGenerationResponse with cypher_query, can_generate flag, and reason if not
36
36
  """
37
- model_config = get_provider_model()
37
+ model_config = await get_provider_model()
38
38
 
39
39
  # Create an agent with structured output for Cypher generation
40
40
  cypher_agent = Agent(
@@ -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
shotgun/exceptions.py ADDED
@@ -0,0 +1,32 @@
1
+ """General exceptions for Shotgun application."""
2
+
3
+
4
+ class ErrorNotPickedUpBySentry(Exception): # noqa: N818
5
+ """Base for user-actionable errors that shouldn't be sent to Sentry.
6
+
7
+ These errors represent expected user conditions requiring action
8
+ rather than bugs that need tracking.
9
+ """
10
+
11
+
12
+ class ContextSizeLimitExceeded(ErrorNotPickedUpBySentry):
13
+ """Raised when conversation context exceeds the model's limits.
14
+
15
+ This is a user-actionable error - they need to either:
16
+ 1. Switch to a larger context model
17
+ 2. Switch to a larger model, compact their conversation, then switch back
18
+ 3. Clear the conversation and start fresh
19
+ """
20
+
21
+ def __init__(self, model_name: str, max_tokens: int):
22
+ """Initialize the exception.
23
+
24
+ Args:
25
+ model_name: Name of the model whose limit was exceeded
26
+ max_tokens: Maximum tokens allowed by the model
27
+ """
28
+ self.model_name = model_name
29
+ self.max_tokens = max_tokens
30
+ super().__init__(
31
+ f"Context too large for {model_name} (limit: {max_tokens:,} tokens)"
32
+ )
@@ -0,0 +1,19 @@
1
+ """LiteLLM proxy client utilities and configuration."""
2
+
3
+ from .clients import (
4
+ create_anthropic_proxy_provider,
5
+ create_litellm_provider,
6
+ )
7
+ from .constants import (
8
+ LITELLM_PROXY_ANTHROPIC_BASE,
9
+ LITELLM_PROXY_BASE_URL,
10
+ LITELLM_PROXY_OPENAI_BASE,
11
+ )
12
+
13
+ __all__ = [
14
+ "LITELLM_PROXY_BASE_URL",
15
+ "LITELLM_PROXY_ANTHROPIC_BASE",
16
+ "LITELLM_PROXY_OPENAI_BASE",
17
+ "create_litellm_provider",
18
+ "create_anthropic_proxy_provider",
19
+ ]
@@ -0,0 +1,44 @@
1
+ """Client creation utilities for LiteLLM proxy."""
2
+
3
+ from pydantic_ai.providers.anthropic import AnthropicProvider
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_provider(api_key: str) -> AnthropicProvider:
25
+ """Create Anthropic provider configured for LiteLLM proxy.
26
+
27
+ This provider uses native Anthropic API format while routing through
28
+ the LiteLLM proxy. This preserves Anthropic-specific features like
29
+ tool_choice and web search.
30
+
31
+ The provider's .client attribute provides access to the async Anthropic
32
+ client (AsyncAnthropic), which should be used for all API operations
33
+ including token counting.
34
+
35
+ Args:
36
+ api_key: Shotgun API key
37
+
38
+ Returns:
39
+ AnthropicProvider configured to use LiteLLM proxy /anthropic endpoint
40
+ """
41
+ return AnthropicProvider(
42
+ api_key=api_key,
43
+ base_url=LITELLM_PROXY_ANTHROPIC_BASE,
44
+ )
@@ -0,0 +1,15 @@
1
+ """LiteLLM proxy constants and configuration."""
2
+
3
+ # Import from centralized API endpoints module
4
+ from shotgun.api_endpoints import (
5
+ LITELLM_PROXY_ANTHROPIC_BASE,
6
+ LITELLM_PROXY_BASE_URL,
7
+ LITELLM_PROXY_OPENAI_BASE,
8
+ )
9
+
10
+ # Re-export for backward compatibility
11
+ __all__ = [
12
+ "LITELLM_PROXY_BASE_URL",
13
+ "LITELLM_PROXY_ANTHROPIC_BASE",
14
+ "LITELLM_PROXY_OPENAI_BASE",
15
+ ]
shotgun/logging_config.py CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  import logging
4
4
  import logging.handlers
5
- import os
6
5
  import sys
6
+ from datetime import datetime, timezone
7
7
  from pathlib import Path
8
8
 
9
+ from shotgun.settings import settings
9
10
  from shotgun.utils.env_utils import is_truthy
10
11
 
12
+ # Generate a single timestamp for this run to be used across all loggers
13
+ _RUN_TIMESTAMP = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
14
+
11
15
 
12
16
  def get_log_directory() -> Path:
13
17
  """Get the log directory path, creating it if necessary.
@@ -66,21 +70,16 @@ def setup_logger(
66
70
  logger = logging.getLogger(name)
67
71
 
68
72
  # Check if we already have a file handler
69
- has_file_handler = any(
70
- isinstance(h, logging.handlers.TimedRotatingFileHandler)
71
- for h in logger.handlers
72
- )
73
+ has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
73
74
 
74
75
  # If we already have a file handler, just return the logger
75
76
  if has_file_handler:
76
77
  return logger
77
78
 
78
- # Get log level from environment variable, default to INFO
79
- env_level = os.getenv("SHOTGUN_LOG_LEVEL", "INFO").upper()
80
- if env_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
81
- env_level = "INFO"
79
+ # Get log level from settings (already validated and uppercased)
80
+ log_level = settings.logging.log_level
82
81
 
83
- logger.setLevel(getattr(logging, env_level))
82
+ logger.setLevel(getattr(logging, log_level))
84
83
 
85
84
  # Default format string
86
85
  if format_string is None:
@@ -102,13 +101,13 @@ def setup_logger(
102
101
  # Check if console logging is enabled (default: off)
103
102
  # Force console logging OFF if Logfire is enabled in dev build
104
103
  console_logging_enabled = (
105
- is_truthy(os.getenv("LOGGING_TO_CONSOLE", "false")) and not is_logfire_dev_build
104
+ settings.logging.logging_to_console and not is_logfire_dev_build
106
105
  )
107
106
 
108
107
  if console_logging_enabled:
109
108
  # Create console handler
110
109
  console_handler = logging.StreamHandler(sys.stdout)
111
- console_handler.setLevel(getattr(logging, env_level))
110
+ console_handler.setLevel(getattr(logging, log_level))
112
111
 
113
112
  # Use colored formatter for console
114
113
  console_formatter = ColoredFormatter(format_string, datefmt="%H:%M:%S")
@@ -118,26 +117,21 @@ def setup_logger(
118
117
  logger.addHandler(console_handler)
119
118
 
120
119
  # Check if file logging is enabled (default: on)
121
- file_logging_enabled = is_truthy(os.getenv("LOGGING_TO_FILE", "true"))
120
+ file_logging_enabled = settings.logging.logging_to_file
122
121
 
123
122
  if file_logging_enabled:
124
123
  try:
125
- # Create file handler with rotation
124
+ # Create file handler with ISO8601 timestamp for each run
126
125
  log_dir = get_log_directory()
127
- log_file = log_dir / "shotgun.log"
126
+ log_file = log_dir / f"shotgun-{_RUN_TIMESTAMP}.log"
128
127
 
129
- # Use TimedRotatingFileHandler - rotates daily and keeps 7 days of logs
130
- file_handler = logging.handlers.TimedRotatingFileHandler(
128
+ # Use regular FileHandler - each run gets its own isolated log file
129
+ file_handler = logging.FileHandler(
131
130
  filename=log_file,
132
- when="midnight", # Rotate at midnight
133
- interval=1, # Every 1 day
134
- backupCount=7, # Keep 7 days of logs
135
131
  encoding="utf-8",
136
132
  )
137
133
 
138
- # Also set max file size (10MB) using RotatingFileHandler as fallback
139
- # Note: We'll use TimedRotatingFileHandler which handles both time and size
140
- file_handler.setLevel(getattr(logging, env_level))
134
+ file_handler.setLevel(getattr(logging, log_level))
141
135
 
142
136
  # Use standard formatter for file (no colors)
143
137
  file_formatter = logging.Formatter(
@@ -191,10 +185,7 @@ def get_logger(name: str) -> logging.Logger:
191
185
  logger = logging.getLogger(name)
192
186
 
193
187
  # Check if we have a file handler already
194
- has_file_handler = any(
195
- isinstance(h, logging.handlers.TimedRotatingFileHandler)
196
- for h in logger.handlers
197
- )
188
+ has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
198
189
 
199
190
  # If no file handler, set up the logger (will add file handler)
200
191
  if not has_file_handler:
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.
@@ -16,7 +22,20 @@ from dotenv import load_dotenv
16
22
 
17
23
  from shotgun import __version__
18
24
  from shotgun.agents.config import get_config_manager
19
- from shotgun.cli import codebase, config, export, plan, research, specify, tasks, update
25
+ from shotgun.cli import (
26
+ clear,
27
+ codebase,
28
+ compact,
29
+ config,
30
+ context,
31
+ export,
32
+ feedback,
33
+ plan,
34
+ research,
35
+ specify,
36
+ tasks,
37
+ update,
38
+ )
20
39
  from shotgun.logging_config import configure_root_logger, get_logger
21
40
  from shotgun.posthog_telemetry import setup_posthog_observability
22
41
  from shotgun.sentry_telemetry import setup_sentry_observability
@@ -37,8 +56,10 @@ logger.debug("Logfire observability enabled: %s", _logfire_enabled)
37
56
 
38
57
  # Initialize configuration
39
58
  try:
59
+ import asyncio
60
+
40
61
  config_manager = get_config_manager()
41
- config_manager.load() # Ensure config is loaded at startup
62
+ asyncio.run(config_manager.load()) # Ensure config is loaded at startup
42
63
  except Exception as e:
43
64
  logger.debug("Configuration initialization warning: %s", e)
44
65
 
@@ -62,12 +83,16 @@ app.add_typer(config.app, name="config", help="Manage Shotgun configuration")
62
83
  app.add_typer(
63
84
  codebase.app, name="codebase", help="Manage and query code knowledge graphs"
64
85
  )
86
+ app.add_typer(context.app, name="context", help="Analyze conversation context usage")
87
+ app.add_typer(compact.app, name="compact", help="Compact conversation history")
88
+ app.add_typer(clear.app, name="clear", help="Clear conversation history")
65
89
  app.add_typer(research.app, name="research", help="Perform research with agentic loops")
66
90
  app.add_typer(plan.app, name="plan", help="Generate structured plans")
67
91
  app.add_typer(specify.app, name="specify", help="Generate comprehensive specifications")
68
92
  app.add_typer(tasks.app, name="tasks", help="Generate task lists with agentic approach")
69
93
  app.add_typer(export.app, name="export", help="Export artifacts to various formats")
70
94
  app.add_typer(update.app, name="update", help="Check for and install updates")
95
+ app.add_typer(feedback.app, name="feedback", help="Send us feedback")
71
96
 
72
97
 
73
98
  def version_callback(value: bool) -> None:
@@ -108,6 +133,41 @@ def main(
108
133
  help="Continue previous TUI conversation",
109
134
  ),
110
135
  ] = False,
136
+ web: Annotated[
137
+ bool,
138
+ typer.Option(
139
+ "--web",
140
+ help="Serve TUI as web application",
141
+ ),
142
+ ] = False,
143
+ port: Annotated[
144
+ int,
145
+ typer.Option(
146
+ "--port",
147
+ help="Port for web server (only used with --web)",
148
+ ),
149
+ ] = 8000,
150
+ host: Annotated[
151
+ str,
152
+ typer.Option(
153
+ "--host",
154
+ help="Host address for web server (only used with --web)",
155
+ ),
156
+ ] = "localhost",
157
+ public_url: Annotated[
158
+ str | None,
159
+ typer.Option(
160
+ "--public-url",
161
+ help="Public URL if behind proxy (only used with --web)",
162
+ ),
163
+ ] = None,
164
+ force_reindex: Annotated[
165
+ bool,
166
+ typer.Option(
167
+ "--force-reindex",
168
+ help="Force re-indexing of codebase (ignores existing index)",
169
+ ),
170
+ ] = False,
111
171
  ) -> None:
112
172
  """Shotgun - AI-powered CLI tool."""
113
173
  logger.debug("Starting shotgun CLI application")
@@ -117,16 +177,35 @@ def main(
117
177
  perform_auto_update_async(no_update_check=no_update_check)
118
178
 
119
179
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
120
- logger.debug("Launching shotgun TUI application")
121
- try:
122
- tui_app.run(
123
- no_update_check=no_update_check, continue_session=continue_session
124
- )
125
- finally:
126
- # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
127
- from shotgun.posthog_telemetry import shutdown
128
-
129
- shutdown()
180
+ if web:
181
+ logger.debug("Launching shotgun TUI as web application")
182
+ try:
183
+ tui_app.serve(
184
+ host=host,
185
+ port=port,
186
+ public_url=public_url,
187
+ no_update_check=no_update_check,
188
+ continue_session=continue_session,
189
+ force_reindex=force_reindex,
190
+ )
191
+ finally:
192
+ # Ensure PostHog is shut down cleanly even if server exits unexpectedly
193
+ from shotgun.posthog_telemetry import shutdown
194
+
195
+ shutdown()
196
+ else:
197
+ logger.debug("Launching shotgun TUI application")
198
+ try:
199
+ tui_app.run(
200
+ no_update_check=no_update_check,
201
+ continue_session=continue_session,
202
+ force_reindex=force_reindex,
203
+ )
204
+ finally:
205
+ # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
206
+ from shotgun.posthog_telemetry import shutdown
207
+
208
+ shutdown()
130
209
  raise typer.Exit()
131
210
 
132
211
  # For CLI commands, register PostHog shutdown handler