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.
- shotgun/agents/agent_manager.py +664 -75
- shotgun/agents/common.py +76 -70
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +78 -36
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +70 -15
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +57 -19
- shotgun/agents/export.py +6 -7
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +14 -2
- shotgun/agents/history/token_counting/anthropic.py +49 -11
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +8 -0
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +6 -7
- shotgun/agents/research.py +7 -8
- shotgun/agents/specify.py +6 -7
- shotgun/agents/tasks.py +6 -7
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +82 -16
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +30 -18
- shotgun/agents/tools/web_search/anthropic.py +26 -5
- shotgun/agents/tools/web_search/gemini.py +23 -11
- shotgun/agents/tools/web_search/openai.py +22 -13
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +1 -1
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/llm_proxy/__init__.py +5 -2
- shotgun/llm_proxy/clients.py +12 -7
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +23 -7
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sentry_telemetry.py +7 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +18 -33
- shotgun/tui/app.py +243 -43
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1202 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +78 -2
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +49 -24
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +32 -10
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +262 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.11.dev5.dist-info/METADATA +130 -0
- shotgun_sh-0.2.11.dev5.dist-info/RECORD +193 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -352
- shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
- shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
- {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 == "
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
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
|
|
shotgun/codebase/core/manager.py
CHANGED
|
@@ -371,7 +371,16 @@ class CodebaseGraphManager:
|
|
|
371
371
|
)
|
|
372
372
|
import shutil
|
|
373
373
|
|
|
374
|
-
|
|
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(
|
shotgun/llm_proxy/__init__.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""LiteLLM proxy client utilities and configuration."""
|
|
2
2
|
|
|
3
|
-
from .clients import
|
|
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
|
-
"
|
|
18
|
+
"create_anthropic_proxy_provider",
|
|
16
19
|
]
|
shotgun/llm_proxy/clients.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Client creation utilities for LiteLLM proxy."""
|
|
2
2
|
|
|
3
|
-
from anthropic import
|
|
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
|
|
25
|
-
"""Create Anthropic
|
|
24
|
+
def create_anthropic_proxy_provider(api_key: str) -> AnthropicProvider:
|
|
25
|
+
"""Create Anthropic provider configured for LiteLLM proxy.
|
|
26
26
|
|
|
27
|
-
This
|
|
28
|
-
LiteLLM proxy
|
|
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
|
-
|
|
39
|
+
AnthropicProvider configured to use LiteLLM proxy /anthropic endpoint
|
|
35
40
|
"""
|
|
36
|
-
return
|
|
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
|
|
79
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
130
|
-
file_handler = logging.
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
shotgun/posthog_telemetry.py
CHANGED
|
@@ -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
|
-
#
|
|
36
|
-
api_key =
|
|
36
|
+
# Get API key from settings (handles build constants + env vars automatically)
|
|
37
|
+
api_key = settings.telemetry.posthog_api_key
|
|
37
38
|
|
|
38
|
-
|
|
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 =
|
|
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]
|
shotgun/prompts/agents/export.j2
CHANGED
|
@@ -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
|
|
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
|