shotgun-sh 0.2.3.dev2__py3-none-any.whl → 0.2.11.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.

Files changed (132) hide show
  1. shotgun/agents/agent_manager.py +664 -75
  2. shotgun/agents/common.py +76 -70
  3. shotgun/agents/config/constants.py +0 -6
  4. shotgun/agents/config/manager.py +78 -36
  5. shotgun/agents/config/models.py +41 -1
  6. shotgun/agents/config/provider.py +70 -15
  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 +9 -4
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +14 -2
  18. shotgun/agents/history/token_counting/anthropic.py +49 -11
  19. shotgun/agents/history/token_counting/base.py +14 -3
  20. shotgun/agents/history/token_counting/openai.py +8 -0
  21. shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
  22. shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
  23. shotgun/agents/history/token_counting/utils.py +0 -3
  24. shotgun/agents/models.py +50 -2
  25. shotgun/agents/plan.py +6 -7
  26. shotgun/agents/research.py +7 -8
  27. shotgun/agents/specify.py +6 -7
  28. shotgun/agents/tasks.py +6 -7
  29. shotgun/agents/tools/__init__.py +0 -2
  30. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  31. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  32. shotgun/agents/tools/codebase/file_read.py +11 -2
  33. shotgun/agents/tools/codebase/query_graph.py +6 -0
  34. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  35. shotgun/agents/tools/file_management.py +82 -16
  36. shotgun/agents/tools/registry.py +217 -0
  37. shotgun/agents/tools/web_search/__init__.py +30 -18
  38. shotgun/agents/tools/web_search/anthropic.py +26 -5
  39. shotgun/agents/tools/web_search/gemini.py +23 -11
  40. shotgun/agents/tools/web_search/openai.py +22 -13
  41. shotgun/agents/tools/web_search/utils.py +2 -2
  42. shotgun/agents/usage_manager.py +16 -11
  43. shotgun/api_endpoints.py +7 -3
  44. shotgun/build_constants.py +1 -1
  45. shotgun/cli/clear.py +53 -0
  46. shotgun/cli/compact.py +186 -0
  47. shotgun/cli/config.py +8 -5
  48. shotgun/cli/context.py +111 -0
  49. shotgun/cli/export.py +1 -1
  50. shotgun/cli/feedback.py +4 -2
  51. shotgun/cli/models.py +1 -0
  52. shotgun/cli/plan.py +1 -1
  53. shotgun/cli/research.py +1 -1
  54. shotgun/cli/specify.py +1 -1
  55. shotgun/cli/tasks.py +1 -1
  56. shotgun/cli/update.py +16 -2
  57. shotgun/codebase/core/change_detector.py +5 -3
  58. shotgun/codebase/core/code_retrieval.py +4 -2
  59. shotgun/codebase/core/ingestor.py +10 -8
  60. shotgun/codebase/core/manager.py +13 -4
  61. shotgun/codebase/core/nl_query.py +1 -1
  62. shotgun/llm_proxy/__init__.py +5 -2
  63. shotgun/llm_proxy/clients.py +12 -7
  64. shotgun/logging_config.py +18 -27
  65. shotgun/main.py +73 -11
  66. shotgun/posthog_telemetry.py +23 -7
  67. shotgun/prompts/agents/export.j2 +18 -1
  68. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  69. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  70. shotgun/prompts/agents/plan.j2 +1 -1
  71. shotgun/prompts/agents/research.j2 +1 -1
  72. shotgun/prompts/agents/specify.j2 +270 -3
  73. shotgun/prompts/agents/state/system_state.j2 +4 -0
  74. shotgun/prompts/agents/tasks.j2 +1 -1
  75. shotgun/prompts/loader.py +2 -2
  76. shotgun/prompts/tools/web_search.j2 +14 -0
  77. shotgun/sentry_telemetry.py +7 -16
  78. shotgun/settings.py +238 -0
  79. shotgun/telemetry.py +18 -33
  80. shotgun/tui/app.py +243 -43
  81. shotgun/tui/commands/__init__.py +1 -1
  82. shotgun/tui/components/context_indicator.py +179 -0
  83. shotgun/tui/components/mode_indicator.py +70 -0
  84. shotgun/tui/components/status_bar.py +48 -0
  85. shotgun/tui/containers.py +91 -0
  86. shotgun/tui/dependencies.py +39 -0
  87. shotgun/tui/protocols.py +45 -0
  88. shotgun/tui/screens/chat/__init__.py +5 -0
  89. shotgun/tui/screens/chat/chat.tcss +54 -0
  90. shotgun/tui/screens/chat/chat_screen.py +1202 -0
  91. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  92. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  93. shotgun/tui/screens/chat/help_text.py +40 -0
  94. shotgun/tui/screens/chat/prompt_history.py +48 -0
  95. shotgun/tui/screens/chat.tcss +11 -0
  96. shotgun/tui/screens/chat_screen/command_providers.py +78 -2
  97. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  98. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  99. shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
  100. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  101. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  102. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  103. shotgun/tui/screens/confirmation_dialog.py +151 -0
  104. shotgun/tui/screens/feedback.py +4 -4
  105. shotgun/tui/screens/github_issue.py +102 -0
  106. shotgun/tui/screens/model_picker.py +49 -24
  107. shotgun/tui/screens/onboarding.py +431 -0
  108. shotgun/tui/screens/pipx_migration.py +153 -0
  109. shotgun/tui/screens/provider_config.py +50 -27
  110. shotgun/tui/screens/shotgun_auth.py +2 -2
  111. shotgun/tui/screens/welcome.py +32 -10
  112. shotgun/tui/services/__init__.py +5 -0
  113. shotgun/tui/services/conversation_service.py +184 -0
  114. shotgun/tui/state/__init__.py +7 -0
  115. shotgun/tui/state/processing_state.py +185 -0
  116. shotgun/tui/utils/mode_progress.py +14 -7
  117. shotgun/tui/widgets/__init__.py +5 -0
  118. shotgun/tui/widgets/widget_coordinator.py +262 -0
  119. shotgun/utils/datetime_utils.py +77 -0
  120. shotgun/utils/file_system_utils.py +22 -2
  121. shotgun/utils/marketing.py +110 -0
  122. shotgun/utils/update_checker.py +69 -14
  123. shotgun_sh-0.2.11.dev5.dist-info/METADATA +130 -0
  124. shotgun_sh-0.2.11.dev5.dist-info/RECORD +193 -0
  125. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/entry_points.txt +1 -0
  126. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/licenses/LICENSE +1 -1
  127. shotgun/agents/tools/user_interaction.py +0 -37
  128. shotgun/tui/screens/chat.py +0 -804
  129. shotgun/tui/screens/chat_screen/history.py +0 -352
  130. shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
  131. shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
  132. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/WHEEL +0 -0
shotgun/cli/plan.py CHANGED
@@ -55,7 +55,7 @@ def plan(
55
55
  )
56
56
 
57
57
  # Create the plan agent with deps and provider
58
- agent, deps = create_plan_agent(agent_runtime_options, provider)
58
+ agent, deps = asyncio.run(create_plan_agent(agent_runtime_options, provider))
59
59
 
60
60
  # Start planning process
61
61
  logger.info("🎯 Starting planning...")
shotgun/cli/research.py CHANGED
@@ -73,7 +73,7 @@ async def async_research(
73
73
  agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
74
74
 
75
75
  # Create the research agent with deps and provider
76
- agent, deps = create_research_agent(agent_runtime_options, provider)
76
+ agent, deps = await create_research_agent(agent_runtime_options, provider)
77
77
 
78
78
  # Start research process
79
79
  logger.info("🔬 Starting research...")
shotgun/cli/specify.py CHANGED
@@ -51,7 +51,7 @@ def specify(
51
51
  )
52
52
 
53
53
  # Create the specify agent with deps and provider
54
- agent, deps = create_specify_agent(agent_runtime_options, provider)
54
+ agent, deps = asyncio.run(create_specify_agent(agent_runtime_options, provider))
55
55
 
56
56
  # Start specification process
57
57
  logger.info("📋 Starting specification generation...")
shotgun/cli/tasks.py CHANGED
@@ -60,7 +60,7 @@ def tasks(
60
60
  )
61
61
 
62
62
  # Create the tasks agent with deps and provider
63
- agent, deps = create_tasks_agent(agent_runtime_options, provider)
63
+ agent, deps = asyncio.run(create_tasks_agent(agent_runtime_options, provider))
64
64
 
65
65
  # Start task creation process
66
66
  logger.info("🎯 Starting task creation...")
shotgun/cli/update.py CHANGED
@@ -45,7 +45,7 @@ def update(
45
45
 
46
46
  This command will:
47
47
  - Check PyPI for the latest version
48
- - Detect your installation method (pipx, pip, or venv)
48
+ - Detect your installation method (uvx, uv-tool, pipx, pip, or venv)
49
49
  - Perform the appropriate upgrade command
50
50
 
51
51
  Examples:
@@ -93,6 +93,8 @@ def update(
93
93
  )
94
94
  console.print(
95
95
  "Use --force to update anyway, or install the stable version with:\n"
96
+ " uv tool install shotgun-sh\n"
97
+ " or\n"
96
98
  " pipx install shotgun-sh\n"
97
99
  " or\n"
98
100
  " pip install shotgun-sh",
@@ -134,7 +136,19 @@ def update(
134
136
  console.print(f"\n[red]✗[/red] {message}", style="bold red")
135
137
 
136
138
  # Provide manual update instructions
137
- if method == "pipx":
139
+ if method == "uvx":
140
+ console.print(
141
+ "\n[yellow]Run uvx again to use the latest version:[/yellow]\n"
142
+ " uvx shotgun-sh\n"
143
+ "\n[yellow]Or install permanently:[/yellow]\n"
144
+ " uv tool install shotgun-sh"
145
+ )
146
+ elif method == "uv-tool":
147
+ console.print(
148
+ "\n[yellow]Try updating manually:[/yellow]\n"
149
+ " uv tool upgrade shotgun-sh"
150
+ )
151
+ elif method == "pipx":
138
152
  console.print(
139
153
  "\n[yellow]Try updating manually:[/yellow]\n"
140
154
  " pipx upgrade shotgun-sh"
@@ -6,6 +6,7 @@ from enum import Enum
6
6
  from pathlib import Path
7
7
  from typing import Any, cast
8
8
 
9
+ import aiofiles
9
10
  import kuzu
10
11
 
11
12
  from shotgun.logging_config import get_logger
@@ -301,7 +302,7 @@ class ChangeDetector:
301
302
  # Direct substring match
302
303
  return pattern in filepath
303
304
 
304
- def _calculate_file_hash(self, filepath: Path) -> str:
305
+ async def _calculate_file_hash(self, filepath: Path) -> str:
305
306
  """Calculate hash of file contents.
306
307
 
307
308
  Args:
@@ -311,8 +312,9 @@ class ChangeDetector:
311
312
  SHA256 hash of file contents
312
313
  """
313
314
  try:
314
- with open(filepath, "rb") as f:
315
- return hashlib.sha256(f.read()).hexdigest()
315
+ async with aiofiles.open(filepath, "rb") as f:
316
+ content = await f.read()
317
+ return hashlib.sha256(content).hexdigest()
316
318
  except Exception as e:
317
319
  logger.error(f"Failed to calculate hash for {filepath}: {e}")
318
320
  return ""
@@ -3,6 +3,7 @@
3
3
  from pathlib import Path
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ import aiofiles
6
7
  from pydantic import BaseModel
7
8
 
8
9
  from shotgun.logging_config import get_logger
@@ -141,8 +142,9 @@ async def retrieve_code_by_qualified_name(
141
142
 
142
143
  # Read the file and extract the snippet
143
144
  try:
144
- with full_path.open("r", encoding="utf-8") as f:
145
- all_lines = f.readlines()
145
+ async with aiofiles.open(full_path, encoding="utf-8") as f:
146
+ content = await f.read()
147
+ all_lines = content.splitlines(keepends=True)
146
148
 
147
149
  # Extract the relevant lines (1-indexed to 0-indexed)
148
150
  snippet_lines = all_lines[start_line - 1 : end_line]
@@ -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
 
@@ -619,7 +621,7 @@ class SimpleGraphBuilder:
619
621
  # Don't let progress callback errors crash the build
620
622
  logger.debug(f"Progress callback error: {e}")
621
623
 
622
- def run(self) -> None:
624
+ async def run(self) -> None:
623
625
  """Run the three-pass graph building process."""
624
626
  logger.info(f"Building graph for project: {self.project_name}")
625
627
 
@@ -629,7 +631,7 @@ class SimpleGraphBuilder:
629
631
 
630
632
  # Pass 2: Definitions
631
633
  logger.info("Pass 2: Processing files and extracting definitions...")
632
- self._process_files()
634
+ await self._process_files()
633
635
 
634
636
  # Pass 3: Relationships
635
637
  logger.info("Pass 3: Processing relationships (calls, imports)...")
@@ -771,7 +773,7 @@ class SimpleGraphBuilder:
771
773
  phase_complete=True,
772
774
  )
773
775
 
774
- def _process_files(self) -> None:
776
+ async def _process_files(self) -> None:
775
777
  """Second pass: Process files and extract definitions."""
776
778
  # First pass: Count total files
777
779
  total_files = 0
@@ -807,7 +809,7 @@ class SimpleGraphBuilder:
807
809
  lang_config = get_language_config(ext)
808
810
 
809
811
  if lang_config and lang_config.name in self.parsers:
810
- self._process_single_file(filepath, lang_config.name)
812
+ await self._process_single_file(filepath, lang_config.name)
811
813
  file_count += 1
812
814
 
813
815
  # Report progress after each file
@@ -832,7 +834,7 @@ class SimpleGraphBuilder:
832
834
  phase_complete=True,
833
835
  )
834
836
 
835
- def _process_single_file(self, filepath: Path, language: str) -> None:
837
+ async def _process_single_file(self, filepath: Path, language: str) -> None:
836
838
  """Process a single file."""
837
839
  relative_path = filepath.relative_to(self.repo_path)
838
840
  relative_path_str = str(relative_path).replace(os.sep, "/")
@@ -873,8 +875,8 @@ class SimpleGraphBuilder:
873
875
 
874
876
  # Parse file
875
877
  try:
876
- with open(filepath, "rb") as f:
877
- content = f.read()
878
+ async with aiofiles.open(filepath, "rb") as f:
879
+ content = await f.read()
878
880
 
879
881
  parser = self.parsers[language]
880
882
  tree = parser.parse(content)
@@ -1636,7 +1638,7 @@ class CodebaseIngestor:
1636
1638
  )
1637
1639
  if self.project_name:
1638
1640
  builder.project_name = self.project_name
1639
- builder.run()
1641
+ asyncio.run(builder.run())
1640
1642
 
1641
1643
  logger.info(f"Graph successfully created at: {self.db_path}")
1642
1644
 
@@ -371,7 +371,16 @@ class CodebaseGraphManager:
371
371
  )
372
372
  import shutil
373
373
 
374
- 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
375
384
 
376
385
  # Import the builder from local core module
377
386
  from shotgun.codebase.core import CodebaseIngestor
@@ -760,7 +769,7 @@ class CodebaseGraphManager:
760
769
 
761
770
  lang_config = get_language_config(full_path.suffix)
762
771
  if lang_config and lang_config.name in parsers:
763
- builder._process_single_file(full_path, lang_config.name)
772
+ await builder._process_single_file(full_path, lang_config.name)
764
773
  stats["nodes_modified"] += 1 # Approximate
765
774
 
766
775
  # Process additions
@@ -775,7 +784,7 @@ class CodebaseGraphManager:
775
784
 
776
785
  lang_config = get_language_config(full_path.suffix)
777
786
  if lang_config and lang_config.name in parsers:
778
- builder._process_single_file(full_path, lang_config.name)
787
+ await builder._process_single_file(full_path, lang_config.name)
779
788
  stats["nodes_added"] += 1 # Approximate
780
789
 
781
790
  # Flush all pending operations
@@ -1742,7 +1751,7 @@ class CodebaseGraphManager:
1742
1751
  )
1743
1752
 
1744
1753
  # Build the graph
1745
- builder.run()
1754
+ asyncio.run(builder.run())
1746
1755
 
1747
1756
  # Run build in thread pool
1748
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,6 +1,9 @@
1
1
  """LiteLLM proxy client utilities and configuration."""
2
2
 
3
- from .clients import create_anthropic_proxy_client, create_litellm_provider
3
+ from .clients import (
4
+ create_anthropic_proxy_provider,
5
+ create_litellm_provider,
6
+ )
4
7
  from .constants import (
5
8
  LITELLM_PROXY_ANTHROPIC_BASE,
6
9
  LITELLM_PROXY_BASE_URL,
@@ -12,5 +15,5 @@ __all__ = [
12
15
  "LITELLM_PROXY_ANTHROPIC_BASE",
13
16
  "LITELLM_PROXY_OPENAI_BASE",
14
17
  "create_litellm_provider",
15
- "create_anthropic_proxy_client",
18
+ "create_anthropic_proxy_provider",
16
19
  ]
@@ -1,6 +1,6 @@
1
1
  """Client creation utilities for LiteLLM proxy."""
2
2
 
3
- from anthropic import Anthropic
3
+ from pydantic_ai.providers.anthropic import AnthropicProvider
4
4
  from pydantic_ai.providers.litellm import LiteLLMProvider
5
5
 
6
6
  from .constants import LITELLM_PROXY_ANTHROPIC_BASE, LITELLM_PROXY_BASE_URL
@@ -21,19 +21,24 @@ def create_litellm_provider(api_key: str) -> LiteLLMProvider:
21
21
  )
22
22
 
23
23
 
24
- def create_anthropic_proxy_client(api_key: str) -> Anthropic:
25
- """Create Anthropic client configured for LiteLLM proxy.
24
+ def create_anthropic_proxy_provider(api_key: str) -> AnthropicProvider:
25
+ """Create Anthropic provider configured for LiteLLM proxy.
26
26
 
27
- This client will proxy token counting requests through the
28
- LiteLLM proxy to Anthropic's actual token counting API.
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.
29
34
 
30
35
  Args:
31
36
  api_key: Shotgun API key
32
37
 
33
38
  Returns:
34
- Anthropic client configured to use LiteLLM proxy
39
+ AnthropicProvider configured to use LiteLLM proxy /anthropic endpoint
35
40
  """
36
- return Anthropic(
41
+ return AnthropicProvider(
37
42
  api_key=api_key,
38
43
  base_url=LITELLM_PROXY_ANTHROPIC_BASE,
39
44
  )
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
@@ -23,8 +23,11 @@ from dotenv import load_dotenv
23
23
  from shotgun import __version__
24
24
  from shotgun.agents.config import get_config_manager
25
25
  from shotgun.cli import (
26
+ clear,
26
27
  codebase,
28
+ compact,
27
29
  config,
30
+ context,
28
31
  export,
29
32
  feedback,
30
33
  plan,
@@ -53,8 +56,10 @@ logger.debug("Logfire observability enabled: %s", _logfire_enabled)
53
56
 
54
57
  # Initialize configuration
55
58
  try:
59
+ import asyncio
60
+
56
61
  config_manager = get_config_manager()
57
- config_manager.load() # Ensure config is loaded at startup
62
+ asyncio.run(config_manager.load()) # Ensure config is loaded at startup
58
63
  except Exception as e:
59
64
  logger.debug("Configuration initialization warning: %s", e)
60
65
 
@@ -78,6 +83,9 @@ app.add_typer(config.app, name="config", help="Manage Shotgun configuration")
78
83
  app.add_typer(
79
84
  codebase.app, name="codebase", help="Manage and query code knowledge graphs"
80
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")
81
89
  app.add_typer(research.app, name="research", help="Perform research with agentic loops")
82
90
  app.add_typer(plan.app, name="plan", help="Generate structured plans")
83
91
  app.add_typer(specify.app, name="specify", help="Generate comprehensive specifications")
@@ -125,6 +133,41 @@ def main(
125
133
  help="Continue previous TUI conversation",
126
134
  ),
127
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,
128
171
  ) -> None:
129
172
  """Shotgun - AI-powered CLI tool."""
130
173
  logger.debug("Starting shotgun CLI application")
@@ -134,16 +177,35 @@ def main(
134
177
  perform_auto_update_async(no_update_check=no_update_check)
135
178
 
136
179
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
137
- logger.debug("Launching shotgun TUI application")
138
- try:
139
- tui_app.run(
140
- no_update_check=no_update_check, continue_session=continue_session
141
- )
142
- finally:
143
- # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
144
- from shotgun.posthog_telemetry import shutdown
145
-
146
- 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()
147
209
  raise typer.Exit()
148
210
 
149
211
  # For CLI commands, register PostHog shutdown handler
@@ -10,6 +10,7 @@ from shotgun import __version__
10
10
  from shotgun.agents.config import get_config_manager
11
11
  from shotgun.agents.conversation_manager import ConversationManager
12
12
  from shotgun.logging_config import get_early_logger
13
+ from shotgun.settings import settings
13
14
 
14
15
  # Use early logger to prevent automatic StreamHandler creation
15
16
  logger = get_early_logger(__name__)
@@ -32,10 +33,15 @@ def setup_posthog_observability() -> bool:
32
33
  logger.debug("PostHog is already initialized, skipping")
33
34
  return True
34
35
 
35
- # Hardcoded PostHog configuration
36
- api_key = "phc_KKnChzZUKeNqZDOTJ6soCBWNQSx3vjiULdwTR9H5Mcr"
36
+ # Get API key from settings (handles build constants + env vars automatically)
37
+ api_key = settings.telemetry.posthog_api_key
37
38
 
38
- logger.debug("Using hardcoded PostHog configuration")
39
+ # If no API key is available, skip PostHog initialization
40
+ if not api_key:
41
+ logger.debug("No PostHog API key available, skipping initialization")
42
+ return False
43
+
44
+ logger.debug("Using PostHog API key from settings")
39
45
 
40
46
  # Determine environment based on version
41
47
  # Dev versions contain "dev", "rc", "alpha", or "beta"
@@ -53,8 +59,10 @@ def setup_posthog_observability() -> bool:
53
59
 
54
60
  # Set user context with anonymous shotgun instance ID from config
55
61
  try:
62
+ import asyncio
63
+
56
64
  config_manager = get_config_manager()
57
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
65
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
58
66
 
59
67
  # Identify the user in PostHog
60
68
  posthog.identify( # type: ignore[attr-defined]
@@ -101,9 +109,11 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
101
109
  return
102
110
 
103
111
  try:
112
+ import asyncio
113
+
104
114
  # Get shotgun instance ID for tracking
105
115
  config_manager = get_config_manager()
106
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
116
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
107
117
 
108
118
  # Add version and environment to properties
109
119
  if properties is None:
@@ -162,10 +172,16 @@ def submit_feedback_survey(feedback: Feedback) -> None:
162
172
  logger.debug("PostHog not initialized, skipping feedback survey")
163
173
  return
164
174
 
175
+ import asyncio
176
+
165
177
  config_manager = get_config_manager()
166
- config = config_manager.load()
178
+ config = asyncio.run(config_manager.load())
167
179
  conversation_manager = ConversationManager()
168
- conversation = conversation_manager.load()
180
+ conversation = None
181
+ try:
182
+ conversation = asyncio.run(conversation_manager.load())
183
+ except Exception as e:
184
+ logger.debug(f"Failed to load conversation history: {e}")
169
185
  last_10_messages = []
170
186
  if conversation is not None:
171
187
  last_10_messages = conversation.get_agent_messages()[:10]
@@ -124,6 +124,7 @@ content_tasks = read_file('tasks.md') # Read implementation details
124
124
  - `plan.md` - Extract development approach and stages
125
125
  - `tasks.md` - Understand implementation tasks and structure
126
126
  2. **Map content to agents.md standard sections**:
127
+ - **Research, Specifications, and Planning**: ALWAYS include this section first. Check which pipeline files exist in `.shotgun/` (research.md, specification.md, plan.md, tasks.md) and list only the ones that exist. If none exist, omit this section.
127
128
  - **Project Overview**: Brief description and key technologies from specification.md
128
129
  - **Dev Environment Setup**: Installation, dependencies, dev server commands
129
130
  - **Code Style Guidelines**: Coding conventions and patterns from research.md
@@ -170,6 +171,14 @@ For additional specialized exports (only if specifically requested):
170
171
  <CORRECT_CONTENT_TEMPLATE>
171
172
  # Agents.md - [Project Name]
172
173
 
174
+ ## Research, Specifications, and Planning
175
+
176
+ The `.shotgun/` folder contains background research, specifications, and implementation planning files. Refer to these files for additional context:
177
+ - `research.md` - Codebase analysis and research findings
178
+ - `specification.md` - Project requirements and specifications
179
+ - `plan.md` - Development plan and implementation approach
180
+ - `tasks.md` - Task breakdown and implementation progress
181
+
173
182
  ## Project Overview
174
183
  - Brief description of what the project does
175
184
  - Key technologies and frameworks used
@@ -253,6 +262,14 @@ This project is about [making assumptions without reading files]...
253
262
  <GOOD_CONTENT_EXAMPLE>
254
263
  # Agents.md - E-commerce API Project
255
264
 
265
+ ## Research, Specifications, and Planning
266
+
267
+ The `.shotgun/` folder contains background research, specifications, and implementation planning files. Refer to these files for additional context:
268
+ - `research.md` - Codebase analysis and research findings
269
+ - `specification.md` - Project requirements and specifications
270
+ - `plan.md` - Development plan and implementation approach
271
+ - `tasks.md` - Task breakdown and implementation progress
272
+
256
273
  ## Project Overview
257
274
  - REST API for product catalog management with authentication
258
275
  - Built with Python/FastAPI for high performance async operations
@@ -316,7 +333,7 @@ This project is about [making assumptions without reading files]...
316
333
  USER INTERACTION - CLARIFY EXPORT REQUIREMENTS:
317
334
 
318
335
  - ALWAYS ask clarifying questions when export requirements are unclear
319
- - Use ask_user tool to gather specific details about:
336
+ - Use clarifying questions to gather specific details about:
320
337
  - Target format and file type preferences
321
338
  - Intended use case and audience for the export
322
339
  - Specific content sections to include/exclude from files