shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.2.17__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.
- shotgun/agents/agent_manager.py +354 -46
- shotgun/agents/common.py +14 -8
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +66 -35
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +33 -5
- 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 +2 -0
- shotgun/agents/conversation_manager.py +35 -19
- shotgun/agents/export.py +2 -2
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/history_processors.py +113 -5
- shotgun/agents/history/token_counting/anthropic.py +17 -1
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- 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/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -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 +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +7 -1
- 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 +3 -3
- 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/exceptions.py +32 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +37 -28
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +10 -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 +1254 -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 +115 -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 +14 -11
- 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 +263 -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.17.dist-info/METADATA +465 -0
- shotgun_sh-0.2.17.dist-info/RECORD +194 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
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/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
|
+
)
|
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__)
|
|
@@ -17,6 +18,9 @@ logger = get_early_logger(__name__)
|
|
|
17
18
|
# Global PostHog client instance
|
|
18
19
|
_posthog_client = None
|
|
19
20
|
|
|
21
|
+
# Cache the shotgun instance ID to avoid async calls during event tracking
|
|
22
|
+
_shotgun_instance_id: str | None = None
|
|
23
|
+
|
|
20
24
|
|
|
21
25
|
def setup_posthog_observability() -> bool:
|
|
22
26
|
"""Set up PostHog analytics for usage tracking.
|
|
@@ -24,7 +28,7 @@ def setup_posthog_observability() -> bool:
|
|
|
24
28
|
Returns:
|
|
25
29
|
True if PostHog was successfully set up, False otherwise
|
|
26
30
|
"""
|
|
27
|
-
global _posthog_client
|
|
31
|
+
global _posthog_client, _shotgun_instance_id
|
|
28
32
|
|
|
29
33
|
try:
|
|
30
34
|
# Check if PostHog is already initialized
|
|
@@ -32,10 +36,15 @@ def setup_posthog_observability() -> bool:
|
|
|
32
36
|
logger.debug("PostHog is already initialized, skipping")
|
|
33
37
|
return True
|
|
34
38
|
|
|
35
|
-
#
|
|
36
|
-
api_key =
|
|
39
|
+
# Get API key from settings (handles build constants + env vars automatically)
|
|
40
|
+
api_key = settings.telemetry.posthog_api_key
|
|
41
|
+
|
|
42
|
+
# If no API key is available, skip PostHog initialization
|
|
43
|
+
if not api_key:
|
|
44
|
+
logger.debug("No PostHog API key available, skipping initialization")
|
|
45
|
+
return False
|
|
37
46
|
|
|
38
|
-
logger.debug("Using
|
|
47
|
+
logger.debug("Using PostHog API key from settings")
|
|
39
48
|
|
|
40
49
|
# Determine environment based on version
|
|
41
50
|
# Dev versions contain "dev", "rc", "alpha", or "beta"
|
|
@@ -51,29 +60,20 @@ def setup_posthog_observability() -> bool:
|
|
|
51
60
|
# Store the client for later use
|
|
52
61
|
_posthog_client = posthog
|
|
53
62
|
|
|
54
|
-
#
|
|
63
|
+
# Cache the shotgun instance ID for later use (avoids async issues)
|
|
55
64
|
try:
|
|
56
|
-
|
|
57
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
58
|
-
|
|
59
|
-
# Identify the user in PostHog
|
|
60
|
-
posthog.identify( # type: ignore[attr-defined]
|
|
61
|
-
distinct_id=shotgun_instance_id,
|
|
62
|
-
properties={
|
|
63
|
-
"version": __version__,
|
|
64
|
-
"environment": environment,
|
|
65
|
-
},
|
|
66
|
-
)
|
|
65
|
+
import asyncio
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
posthog.personal_api_key = None # Not needed for event tracking
|
|
67
|
+
config_manager = get_config_manager()
|
|
68
|
+
_shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
71
69
|
|
|
72
70
|
logger.debug(
|
|
73
|
-
"PostHog
|
|
71
|
+
"PostHog initialized with shotgun instance ID: %s",
|
|
72
|
+
_shotgun_instance_id,
|
|
74
73
|
)
|
|
75
74
|
except Exception as e:
|
|
76
|
-
logger.warning("Failed to
|
|
75
|
+
logger.warning("Failed to load shotgun instance ID: %s", e)
|
|
76
|
+
# Continue anyway - we'll try to get it during event tracking
|
|
77
77
|
|
|
78
78
|
logger.debug(
|
|
79
79
|
"PostHog analytics configured successfully (environment: %s, version: %s)",
|
|
@@ -94,16 +94,19 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
94
94
|
event_name: Name of the event to track
|
|
95
95
|
properties: Optional properties to include with the event
|
|
96
96
|
"""
|
|
97
|
-
global _posthog_client
|
|
97
|
+
global _posthog_client, _shotgun_instance_id
|
|
98
98
|
|
|
99
99
|
if _posthog_client is None:
|
|
100
100
|
logger.debug("PostHog not initialized, skipping event: %s", event_name)
|
|
101
101
|
return
|
|
102
102
|
|
|
103
103
|
try:
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
# Use cached instance ID (loaded during setup)
|
|
105
|
+
if _shotgun_instance_id is None:
|
|
106
|
+
logger.warning(
|
|
107
|
+
"Shotgun instance ID not available, skipping event: %s", event_name
|
|
108
|
+
)
|
|
109
|
+
return
|
|
107
110
|
|
|
108
111
|
# Add version and environment to properties
|
|
109
112
|
if properties is None:
|
|
@@ -118,7 +121,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
118
121
|
|
|
119
122
|
# Track the event using PostHog's capture method
|
|
120
123
|
_posthog_client.capture(
|
|
121
|
-
distinct_id=
|
|
124
|
+
distinct_id=_shotgun_instance_id, event=event_name, properties=properties
|
|
122
125
|
)
|
|
123
126
|
logger.debug("Tracked PostHog event: %s", event_name)
|
|
124
127
|
except Exception as e:
|
|
@@ -162,10 +165,16 @@ def submit_feedback_survey(feedback: Feedback) -> None:
|
|
|
162
165
|
logger.debug("PostHog not initialized, skipping feedback survey")
|
|
163
166
|
return
|
|
164
167
|
|
|
168
|
+
import asyncio
|
|
169
|
+
|
|
165
170
|
config_manager = get_config_manager()
|
|
166
|
-
config = config_manager.load()
|
|
171
|
+
config = asyncio.run(config_manager.load())
|
|
167
172
|
conversation_manager = ConversationManager()
|
|
168
|
-
conversation =
|
|
173
|
+
conversation = None
|
|
174
|
+
try:
|
|
175
|
+
conversation = asyncio.run(conversation_manager.load())
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.debug(f"Failed to load conversation history: {e}")
|
|
169
178
|
last_10_messages = []
|
|
170
179
|
if conversation is not None:
|
|
171
180
|
last_10_messages = conversation.get_agent_messages()[:10]
|
|
@@ -7,10 +7,11 @@ Your extensive expertise spans, among other things:
|
|
|
7
7
|
## KEY RULES
|
|
8
8
|
|
|
9
9
|
{% if interactive_mode %}
|
|
10
|
-
0. Always ask CLARIFYING QUESTIONS using structured output
|
|
10
|
+
0. Always ask CLARIFYING QUESTIONS using structured output before doing work.
|
|
11
11
|
- Return your response with the clarifying_questions field populated
|
|
12
|
-
- Do not make assumptions about what the user wants
|
|
12
|
+
- Do not make assumptions about what the user wants, get a clear understanding first.
|
|
13
13
|
- Questions should be clear, specific, and answerable
|
|
14
|
+
- Do not ask too many questions that might overwhelm the user; prioritize the most important ones.
|
|
14
15
|
{% endif %}
|
|
15
16
|
1. Above all, prefer using tools to do the work and NEVER respond with text.
|
|
16
17
|
2. IMPORTANT: Always ask for review and go ahead to move forward after using write_file().
|