shotgun-sh 0.2.11__py3-none-any.whl → 0.2.11.dev2__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 +28 -194
- shotgun/agents/common.py +8 -14
- shotgun/agents/config/manager.py +33 -64
- shotgun/agents/config/models.py +1 -25
- shotgun/agents/config/provider.py +2 -2
- shotgun/agents/context_analyzer/analyzer.py +24 -2
- shotgun/agents/conversation_manager.py +19 -35
- shotgun/agents/export.py +2 -2
- shotgun/agents/history/history_processors.py +3 -99
- shotgun/agents/history/token_counting/anthropic.py +1 -17
- shotgun/agents/history/token_counting/base.py +3 -14
- shotgun/agents/history/token_counting/openai.py +1 -11
- shotgun/agents/history/token_counting/sentencepiece_counter.py +0 -8
- shotgun/agents/history/token_counting/tokenizer_cache.py +1 -3
- shotgun/agents/history/token_counting/utils.py +3 -0
- 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/file_read.py +2 -5
- shotgun/agents/tools/file_management.py +7 -11
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +2 -2
- shotgun/agents/tools/web_search/gemini.py +1 -1
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +11 -16
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -2
- shotgun/cli/compact.py +3 -3
- shotgun/cli/config.py +5 -8
- shotgun/cli/context.py +2 -2
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +2 -4
- 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/codebase/core/change_detector.py +3 -5
- shotgun/codebase/core/code_retrieval.py +2 -4
- shotgun/codebase/core/ingestor.py +8 -10
- shotgun/codebase/core/manager.py +3 -3
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/logging_config.py +17 -10
- shotgun/main.py +1 -3
- shotgun/posthog_telemetry.py +4 -14
- shotgun/sentry_telemetry.py +2 -22
- shotgun/telemetry.py +1 -3
- shotgun/tui/app.py +65 -71
- shotgun/tui/components/context_indicator.py +0 -43
- shotgun/tui/containers.py +17 -15
- shotgun/tui/dependencies.py +2 -2
- shotgun/tui/screens/chat/chat_screen.py +40 -164
- shotgun/tui/screens/chat/help_text.py +15 -16
- shotgun/tui/screens/chat_screen/command_providers.py +0 -10
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/model_picker.py +20 -21
- shotgun/tui/screens/provider_config.py +27 -50
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +11 -14
- shotgun/tui/services/conversation_service.py +14 -16
- shotgun/tui/utils/mode_progress.py +7 -14
- shotgun/tui/widgets/widget_coordinator.py +0 -15
- shotgun/utils/file_system_utils.py +0 -19
- {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/METADATA +1 -2
- {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/RECORD +69 -73
- shotgun/exceptions.py +0 -32
- shotgun/tui/screens/github_issue.py +0 -102
- shotgun/tui/screens/onboarding.py +0 -431
- shotgun/utils/marketing.py +0 -110
- {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,8 +6,6 @@ These tools are restricted to the .shotgun directory for security.
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
-
import aiofiles
|
|
10
|
-
import aiofiles.os
|
|
11
9
|
from pydantic_ai import RunContext
|
|
12
10
|
|
|
13
11
|
from shotgun.agents.models import AgentDeps, AgentType, FileOperationType
|
|
@@ -183,11 +181,10 @@ async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
|
183
181
|
try:
|
|
184
182
|
file_path = _validate_shotgun_path(filename)
|
|
185
183
|
|
|
186
|
-
if not
|
|
184
|
+
if not file_path.exists():
|
|
187
185
|
raise FileNotFoundError(f"File not found: {filename}")
|
|
188
186
|
|
|
189
|
-
|
|
190
|
-
content = await f.read()
|
|
187
|
+
content = file_path.read_text(encoding="utf-8")
|
|
191
188
|
logger.debug("📄 Read %d characters from %s", len(content), filename)
|
|
192
189
|
return content
|
|
193
190
|
|
|
@@ -236,22 +233,21 @@ async def write_file(
|
|
|
236
233
|
else:
|
|
237
234
|
operation = (
|
|
238
235
|
FileOperationType.CREATED
|
|
239
|
-
if not
|
|
236
|
+
if not file_path.exists()
|
|
240
237
|
else FileOperationType.UPDATED
|
|
241
238
|
)
|
|
242
239
|
|
|
243
240
|
# Ensure parent directory exists
|
|
244
|
-
|
|
241
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
245
242
|
|
|
246
243
|
# Write content
|
|
247
244
|
if mode == "a":
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
with open(file_path, "a", encoding="utf-8") as f:
|
|
246
|
+
f.write(content)
|
|
250
247
|
logger.debug("📄 Appended %d characters to %s", len(content), filename)
|
|
251
248
|
result = f"Successfully appended {len(content)} characters to {filename}"
|
|
252
249
|
else:
|
|
253
|
-
|
|
254
|
-
await f.write(content)
|
|
250
|
+
file_path.write_text(content, encoding="utf-8")
|
|
255
251
|
logger.debug("📄 Wrote %d characters to %s", len(content), filename)
|
|
256
252
|
result = f"Successfully wrote {len(content)} characters to {filename}"
|
|
257
253
|
|
|
@@ -26,7 +26,7 @@ logger = get_logger(__name__)
|
|
|
26
26
|
WebSearchTool = Callable[[str], Awaitable[str]]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
30
30
|
"""Get list of available web search tools based on configured API keys.
|
|
31
31
|
|
|
32
32
|
Works with both Shotgun Account (via LiteLLM proxy) and BYOK (individual provider keys).
|
|
@@ -43,25 +43,25 @@ async def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
|
43
43
|
|
|
44
44
|
# Check if using Shotgun Account
|
|
45
45
|
config_manager = get_config_manager()
|
|
46
|
-
config =
|
|
46
|
+
config = config_manager.load()
|
|
47
47
|
has_shotgun_key = config.shotgun.api_key is not None
|
|
48
48
|
|
|
49
49
|
if has_shotgun_key:
|
|
50
50
|
logger.debug("🔑 Shotgun Account - only Gemini web search available")
|
|
51
51
|
|
|
52
52
|
# Gemini: Only search tool available for Shotgun Account
|
|
53
|
-
if
|
|
53
|
+
if is_provider_available(ProviderType.GOOGLE):
|
|
54
54
|
logger.debug("✅ Gemini web search tool available")
|
|
55
55
|
tools.append(gemini_web_search_tool)
|
|
56
56
|
|
|
57
57
|
# Anthropic: Not available for Shotgun Account (Gemini-only for Shotgun)
|
|
58
|
-
if
|
|
58
|
+
if is_provider_available(ProviderType.ANTHROPIC):
|
|
59
59
|
logger.debug(
|
|
60
60
|
"⚠️ Anthropic web search requires BYOK (Shotgun Account uses Gemini only)"
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
# OpenAI: Not available for Shotgun Account (Responses API incompatible with proxy)
|
|
64
|
-
if
|
|
64
|
+
if is_provider_available(ProviderType.OPENAI):
|
|
65
65
|
logger.debug(
|
|
66
66
|
"⚠️ OpenAI web search requires BYOK (Responses API not supported via proxy)"
|
|
67
67
|
)
|
|
@@ -69,15 +69,15 @@ async def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
|
69
69
|
# BYOK mode: Load all available tools based on individual provider keys
|
|
70
70
|
logger.debug("🔑 BYOK mode - checking all provider web search tools")
|
|
71
71
|
|
|
72
|
-
if
|
|
72
|
+
if is_provider_available(ProviderType.OPENAI):
|
|
73
73
|
logger.debug("✅ OpenAI web search tool available")
|
|
74
74
|
tools.append(openai_web_search_tool)
|
|
75
75
|
|
|
76
|
-
if
|
|
76
|
+
if is_provider_available(ProviderType.ANTHROPIC):
|
|
77
77
|
logger.debug("✅ Anthropic web search tool available")
|
|
78
78
|
tools.append(anthropic_web_search_tool)
|
|
79
79
|
|
|
80
|
-
if
|
|
80
|
+
if is_provider_available(ProviderType.GOOGLE):
|
|
81
81
|
logger.debug("✅ Gemini web search tool available")
|
|
82
82
|
tools.append(gemini_web_search_tool)
|
|
83
83
|
|
|
@@ -46,7 +46,7 @@ async def anthropic_web_search_tool(query: str) -> str:
|
|
|
46
46
|
|
|
47
47
|
# Get model configuration (supports both Shotgun and BYOK)
|
|
48
48
|
try:
|
|
49
|
-
model_config =
|
|
49
|
+
model_config = get_provider_model(ProviderType.ANTHROPIC)
|
|
50
50
|
except ValueError as e:
|
|
51
51
|
error_msg = f"Anthropic API key not configured: {str(e)}"
|
|
52
52
|
logger.error("❌ %s", error_msg)
|
|
@@ -141,7 +141,7 @@ async def main() -> None:
|
|
|
141
141
|
# Check if API key is available
|
|
142
142
|
try:
|
|
143
143
|
if callable(get_provider_model):
|
|
144
|
-
model_config =
|
|
144
|
+
model_config = get_provider_model(ProviderType.ANTHROPIC)
|
|
145
145
|
if not model_config.api_key:
|
|
146
146
|
raise ValueError("No API key configured")
|
|
147
147
|
except (ValueError, Exception):
|
|
@@ -46,7 +46,7 @@ async def gemini_web_search_tool(query: str) -> str:
|
|
|
46
46
|
|
|
47
47
|
# Get model configuration (supports both Shotgun and BYOK)
|
|
48
48
|
try:
|
|
49
|
-
model_config =
|
|
49
|
+
model_config = get_provider_model(ModelName.GEMINI_2_5_FLASH)
|
|
50
50
|
except ValueError as e:
|
|
51
51
|
error_msg = f"Gemini API key not configured: {str(e)}"
|
|
52
52
|
logger.error("❌ %s", error_msg)
|
|
@@ -43,7 +43,7 @@ async def openai_web_search_tool(query: str) -> str:
|
|
|
43
43
|
|
|
44
44
|
# Get API key from centralized configuration
|
|
45
45
|
try:
|
|
46
|
-
model_config =
|
|
46
|
+
model_config = get_provider_model(ProviderType.OPENAI)
|
|
47
47
|
api_key = model_config.api_key
|
|
48
48
|
except ValueError as e:
|
|
49
49
|
error_msg = f"OpenAI API key not configured: {str(e)}"
|
|
@@ -4,7 +4,7 @@ from shotgun.agents.config import get_provider_model
|
|
|
4
4
|
from shotgun.agents.config.models import ProviderType
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
def is_provider_available(provider: ProviderType) -> bool:
|
|
8
8
|
"""Check if a provider has API key configured.
|
|
9
9
|
|
|
10
10
|
Args:
|
|
@@ -14,7 +14,7 @@ async def is_provider_available(provider: ProviderType) -> bool:
|
|
|
14
14
|
True if the provider has valid credentials configured (from config or env)
|
|
15
15
|
"""
|
|
16
16
|
try:
|
|
17
|
-
|
|
17
|
+
get_provider_model(provider)
|
|
18
18
|
return True
|
|
19
19
|
except ValueError:
|
|
20
20
|
return False
|
shotgun/agents/usage_manager.py
CHANGED
|
@@ -6,8 +6,6 @@ from logging import getLogger
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import TypeAlias
|
|
8
8
|
|
|
9
|
-
import aiofiles
|
|
10
|
-
import aiofiles.os
|
|
11
9
|
from genai_prices import calc_price
|
|
12
10
|
from pydantic import BaseModel, Field
|
|
13
11
|
from pydantic_ai import RunUsage
|
|
@@ -50,10 +48,9 @@ class SessionUsageManager:
|
|
|
50
48
|
self._model_providers: dict[ModelName, ProviderType] = {}
|
|
51
49
|
self._usage_log: list[UsageLogEntry] = []
|
|
52
50
|
self._usage_path: Path = get_shotgun_home() / "usage.json"
|
|
53
|
-
|
|
54
|
-
# Caller should use: manager = SessionUsageManager(); await manager.restore_usage_state()
|
|
51
|
+
self.restore_usage_state()
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
def add_usage(
|
|
57
54
|
self, usage: RunUsage, *, model_name: ModelName, provider: ProviderType
|
|
58
55
|
) -> None:
|
|
59
56
|
self.usage[model_name] += usage
|
|
@@ -61,7 +58,7 @@ class SessionUsageManager:
|
|
|
61
58
|
self._usage_log.append(
|
|
62
59
|
UsageLogEntry(model_name=model_name, usage=usage, provider=provider)
|
|
63
60
|
)
|
|
64
|
-
|
|
61
|
+
self.persist_usage_state()
|
|
65
62
|
|
|
66
63
|
def get_usage_report(self) -> dict[ModelName, RunUsage]:
|
|
67
64
|
return self.usage.copy()
|
|
@@ -81,7 +78,7 @@ class SessionUsageManager:
|
|
|
81
78
|
def build_usage_hint(self) -> str | None:
|
|
82
79
|
return format_usage_hint(self.get_usage_breakdown())
|
|
83
80
|
|
|
84
|
-
|
|
81
|
+
def persist_usage_state(self) -> None:
|
|
85
82
|
state = UsageState(
|
|
86
83
|
usage=dict(self.usage.items()),
|
|
87
84
|
model_providers=self._model_providers.copy(),
|
|
@@ -89,25 +86,23 @@ class SessionUsageManager:
|
|
|
89
86
|
)
|
|
90
87
|
|
|
91
88
|
try:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await f.write(json_content)
|
|
89
|
+
self._usage_path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
with self._usage_path.open("w", encoding="utf-8") as f:
|
|
91
|
+
json.dump(state.model_dump(mode="json"), f, indent=2)
|
|
96
92
|
logger.debug("Usage state persisted to %s", self._usage_path)
|
|
97
93
|
except Exception as exc:
|
|
98
94
|
logger.error(
|
|
99
95
|
"Failed to persist usage state to %s: %s", self._usage_path, exc
|
|
100
96
|
)
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
if not
|
|
98
|
+
def restore_usage_state(self) -> None:
|
|
99
|
+
if not self._usage_path.exists():
|
|
104
100
|
logger.debug("No usage state file found at %s", self._usage_path)
|
|
105
101
|
return
|
|
106
102
|
|
|
107
103
|
try:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
data = json.loads(content)
|
|
104
|
+
with self._usage_path.open(encoding="utf-8") as f:
|
|
105
|
+
data = json.load(f)
|
|
111
106
|
|
|
112
107
|
state = UsageState.model_validate(data)
|
|
113
108
|
except Exception as exc:
|
shotgun/build_constants.py
CHANGED
|
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
|
|
|
12
12
|
POSTHOG_PROJECT_ID = '191396'
|
|
13
13
|
|
|
14
14
|
# Logfire configuration embedded at build time (only for dev builds)
|
|
15
|
-
LOGFIRE_ENABLED = ''
|
|
16
|
-
LOGFIRE_TOKEN = ''
|
|
15
|
+
LOGFIRE_ENABLED = 'true'
|
|
16
|
+
LOGFIRE_TOKEN = 'pylf_v1_us_RwZMlJm1tX6j0PL5RWWbmZpzK2hLBNtFWStNKlySfjh8'
|
|
17
17
|
|
|
18
18
|
# Build metadata
|
|
19
19
|
BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
|
shotgun/cli/clear.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Clear command for shotgun CLI."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
6
5
|
import typer
|
|
@@ -38,7 +37,7 @@ def clear() -> None:
|
|
|
38
37
|
|
|
39
38
|
# Clear the conversation
|
|
40
39
|
manager = ConversationManager(conversation_file)
|
|
41
|
-
|
|
40
|
+
manager.clear()
|
|
42
41
|
|
|
43
42
|
console.print(
|
|
44
43
|
"[green]✓[/green] Conversation cleared successfully", style="bold"
|
shotgun/cli/compact.py
CHANGED
|
@@ -79,7 +79,7 @@ async def compact_conversation() -> dict[str, Any]:
|
|
|
79
79
|
|
|
80
80
|
# Load conversation
|
|
81
81
|
manager = ConversationManager(conversation_file)
|
|
82
|
-
conversation =
|
|
82
|
+
conversation = manager.load()
|
|
83
83
|
|
|
84
84
|
if not conversation:
|
|
85
85
|
raise ValueError("Conversation file is empty or corrupted")
|
|
@@ -91,7 +91,7 @@ async def compact_conversation() -> dict[str, Any]:
|
|
|
91
91
|
raise ValueError("No agent messages found in conversation")
|
|
92
92
|
|
|
93
93
|
# Get model config
|
|
94
|
-
model_config =
|
|
94
|
+
model_config = get_provider_model()
|
|
95
95
|
|
|
96
96
|
# Calculate before metrics
|
|
97
97
|
original_message_count = len(agent_messages)
|
|
@@ -133,7 +133,7 @@ async def compact_conversation() -> dict[str, Any]:
|
|
|
133
133
|
|
|
134
134
|
# Save compacted conversation
|
|
135
135
|
conversation.set_agent_messages(compacted_messages)
|
|
136
|
-
|
|
136
|
+
manager.save(conversation)
|
|
137
137
|
|
|
138
138
|
logger.info(
|
|
139
139
|
f"Compacted conversation: {original_message_count} → {compacted_message_count} messages "
|
shotgun/cli/config.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Configuration management CLI commands."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import json
|
|
5
4
|
from typing import Annotated, Any
|
|
6
5
|
|
|
@@ -45,7 +44,7 @@ def init(
|
|
|
45
44
|
console.print()
|
|
46
45
|
|
|
47
46
|
# Initialize with defaults
|
|
48
|
-
|
|
47
|
+
config_manager.initialize()
|
|
49
48
|
|
|
50
49
|
# Ask for provider
|
|
51
50
|
provider_choices = ["openai", "anthropic", "google"]
|
|
@@ -77,7 +76,7 @@ def init(
|
|
|
77
76
|
|
|
78
77
|
if api_key:
|
|
79
78
|
# update_provider will automatically set selected_model for first provider
|
|
80
|
-
|
|
79
|
+
config_manager.update_provider(provider, api_key=api_key)
|
|
81
80
|
|
|
82
81
|
console.print(
|
|
83
82
|
f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
|
|
@@ -85,7 +84,7 @@ def init(
|
|
|
85
84
|
console.print("🎯 You can now use Shotgun with your configured provider!")
|
|
86
85
|
|
|
87
86
|
else:
|
|
88
|
-
|
|
87
|
+
config_manager.initialize()
|
|
89
88
|
console.print(f"✅ Configuration initialized at {config_manager.config_path}")
|
|
90
89
|
|
|
91
90
|
|
|
@@ -113,7 +112,7 @@ def set(
|
|
|
113
112
|
|
|
114
113
|
try:
|
|
115
114
|
if api_key:
|
|
116
|
-
|
|
115
|
+
config_manager.update_provider(provider, api_key=api_key)
|
|
117
116
|
|
|
118
117
|
console.print(f"✅ Configuration updated for {provider}")
|
|
119
118
|
|
|
@@ -134,10 +133,8 @@ def get(
|
|
|
134
133
|
] = False,
|
|
135
134
|
) -> None:
|
|
136
135
|
"""Display current configuration."""
|
|
137
|
-
import asyncio
|
|
138
|
-
|
|
139
136
|
config_manager = get_config_manager()
|
|
140
|
-
config =
|
|
137
|
+
config = config_manager.load()
|
|
141
138
|
|
|
142
139
|
if json_output:
|
|
143
140
|
# Convert to dict and mask secrets
|
shotgun/cli/context.py
CHANGED
|
@@ -79,7 +79,7 @@ async def analyze_context() -> ContextAnalysisOutput:
|
|
|
79
79
|
|
|
80
80
|
# Load conversation
|
|
81
81
|
manager = ConversationManager(conversation_file)
|
|
82
|
-
conversation =
|
|
82
|
+
conversation = manager.load()
|
|
83
83
|
|
|
84
84
|
if not conversation:
|
|
85
85
|
raise ValueError("Conversation file is empty or corrupted")
|
|
@@ -91,7 +91,7 @@ async def analyze_context() -> ContextAnalysisOutput:
|
|
|
91
91
|
raise ValueError("No agent messages found in conversation")
|
|
92
92
|
|
|
93
93
|
# Get model config (use default provider settings)
|
|
94
|
-
model_config =
|
|
94
|
+
model_config = get_provider_model()
|
|
95
95
|
|
|
96
96
|
# Debug: Log the model being used
|
|
97
97
|
logger.debug(f"Using model: {model_config.name.value}")
|
shotgun/cli/export.py
CHANGED
|
@@ -63,7 +63,7 @@ def export(
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
# Create the export agent with deps and provider
|
|
66
|
-
agent, deps =
|
|
66
|
+
agent, deps = create_export_agent(agent_runtime_options, provider)
|
|
67
67
|
|
|
68
68
|
# Start export process
|
|
69
69
|
logger.info("🎯 Starting export...")
|
shotgun/cli/feedback.py
CHANGED
|
@@ -28,11 +28,9 @@ def send_feedback(
|
|
|
28
28
|
],
|
|
29
29
|
) -> None:
|
|
30
30
|
"""Initialize Shotgun configuration."""
|
|
31
|
-
import asyncio
|
|
32
|
-
|
|
33
31
|
config_manager = get_config_manager()
|
|
34
|
-
|
|
35
|
-
shotgun_instance_id =
|
|
32
|
+
config_manager.load()
|
|
33
|
+
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
36
34
|
|
|
37
35
|
if not description:
|
|
38
36
|
console.print(
|
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 =
|
|
58
|
+
agent, deps = 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 =
|
|
76
|
+
agent, deps = 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 =
|
|
54
|
+
agent, deps = 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 =
|
|
63
|
+
agent, deps = create_tasks_agent(agent_runtime_options, provider)
|
|
64
64
|
|
|
65
65
|
# Start task creation process
|
|
66
66
|
logger.info("🎯 Starting task creation...")
|
|
@@ -6,7 +6,6 @@ from enum import Enum
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
|
-
import aiofiles
|
|
10
9
|
import kuzu
|
|
11
10
|
|
|
12
11
|
from shotgun.logging_config import get_logger
|
|
@@ -302,7 +301,7 @@ class ChangeDetector:
|
|
|
302
301
|
# Direct substring match
|
|
303
302
|
return pattern in filepath
|
|
304
303
|
|
|
305
|
-
|
|
304
|
+
def _calculate_file_hash(self, filepath: Path) -> str:
|
|
306
305
|
"""Calculate hash of file contents.
|
|
307
306
|
|
|
308
307
|
Args:
|
|
@@ -312,9 +311,8 @@ class ChangeDetector:
|
|
|
312
311
|
SHA256 hash of file contents
|
|
313
312
|
"""
|
|
314
313
|
try:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return hashlib.sha256(content).hexdigest()
|
|
314
|
+
with open(filepath, "rb") as f:
|
|
315
|
+
return hashlib.sha256(f.read()).hexdigest()
|
|
318
316
|
except Exception as e:
|
|
319
317
|
logger.error(f"Failed to calculate hash for {filepath}: {e}")
|
|
320
318
|
return ""
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
-
import aiofiles
|
|
7
6
|
from pydantic import BaseModel
|
|
8
7
|
|
|
9
8
|
from shotgun.logging_config import get_logger
|
|
@@ -142,9 +141,8 @@ async def retrieve_code_by_qualified_name(
|
|
|
142
141
|
|
|
143
142
|
# Read the file and extract the snippet
|
|
144
143
|
try:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
all_lines = content.splitlines(keepends=True)
|
|
144
|
+
with full_path.open("r", encoding="utf-8") as f:
|
|
145
|
+
all_lines = f.readlines()
|
|
148
146
|
|
|
149
147
|
# Extract the relevant lines (1-indexed to 0-indexed)
|
|
150
148
|
snippet_lines = all_lines[start_line - 1 : end_line]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Kuzu graph ingestor for building code knowledge graphs."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import hashlib
|
|
5
4
|
import os
|
|
6
5
|
import time
|
|
@@ -9,7 +8,6 @@ from collections import defaultdict
|
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
12
|
-
import aiofiles
|
|
13
11
|
import kuzu
|
|
14
12
|
from tree_sitter import Node, Parser, QueryCursor
|
|
15
13
|
|
|
@@ -621,7 +619,7 @@ class SimpleGraphBuilder:
|
|
|
621
619
|
# Don't let progress callback errors crash the build
|
|
622
620
|
logger.debug(f"Progress callback error: {e}")
|
|
623
621
|
|
|
624
|
-
|
|
622
|
+
def run(self) -> None:
|
|
625
623
|
"""Run the three-pass graph building process."""
|
|
626
624
|
logger.info(f"Building graph for project: {self.project_name}")
|
|
627
625
|
|
|
@@ -631,7 +629,7 @@ class SimpleGraphBuilder:
|
|
|
631
629
|
|
|
632
630
|
# Pass 2: Definitions
|
|
633
631
|
logger.info("Pass 2: Processing files and extracting definitions...")
|
|
634
|
-
|
|
632
|
+
self._process_files()
|
|
635
633
|
|
|
636
634
|
# Pass 3: Relationships
|
|
637
635
|
logger.info("Pass 3: Processing relationships (calls, imports)...")
|
|
@@ -773,7 +771,7 @@ class SimpleGraphBuilder:
|
|
|
773
771
|
phase_complete=True,
|
|
774
772
|
)
|
|
775
773
|
|
|
776
|
-
|
|
774
|
+
def _process_files(self) -> None:
|
|
777
775
|
"""Second pass: Process files and extract definitions."""
|
|
778
776
|
# First pass: Count total files
|
|
779
777
|
total_files = 0
|
|
@@ -809,7 +807,7 @@ class SimpleGraphBuilder:
|
|
|
809
807
|
lang_config = get_language_config(ext)
|
|
810
808
|
|
|
811
809
|
if lang_config and lang_config.name in self.parsers:
|
|
812
|
-
|
|
810
|
+
self._process_single_file(filepath, lang_config.name)
|
|
813
811
|
file_count += 1
|
|
814
812
|
|
|
815
813
|
# Report progress after each file
|
|
@@ -834,7 +832,7 @@ class SimpleGraphBuilder:
|
|
|
834
832
|
phase_complete=True,
|
|
835
833
|
)
|
|
836
834
|
|
|
837
|
-
|
|
835
|
+
def _process_single_file(self, filepath: Path, language: str) -> None:
|
|
838
836
|
"""Process a single file."""
|
|
839
837
|
relative_path = filepath.relative_to(self.repo_path)
|
|
840
838
|
relative_path_str = str(relative_path).replace(os.sep, "/")
|
|
@@ -875,8 +873,8 @@ class SimpleGraphBuilder:
|
|
|
875
873
|
|
|
876
874
|
# Parse file
|
|
877
875
|
try:
|
|
878
|
-
|
|
879
|
-
content =
|
|
876
|
+
with open(filepath, "rb") as f:
|
|
877
|
+
content = f.read()
|
|
880
878
|
|
|
881
879
|
parser = self.parsers[language]
|
|
882
880
|
tree = parser.parse(content)
|
|
@@ -1638,7 +1636,7 @@ class CodebaseIngestor:
|
|
|
1638
1636
|
)
|
|
1639
1637
|
if self.project_name:
|
|
1640
1638
|
builder.project_name = self.project_name
|
|
1641
|
-
|
|
1639
|
+
builder.run()
|
|
1642
1640
|
|
|
1643
1641
|
logger.info(f"Graph successfully created at: {self.db_path}")
|
|
1644
1642
|
|
shotgun/codebase/core/manager.py
CHANGED
|
@@ -769,7 +769,7 @@ class CodebaseGraphManager:
|
|
|
769
769
|
|
|
770
770
|
lang_config = get_language_config(full_path.suffix)
|
|
771
771
|
if lang_config and lang_config.name in parsers:
|
|
772
|
-
|
|
772
|
+
builder._process_single_file(full_path, lang_config.name)
|
|
773
773
|
stats["nodes_modified"] += 1 # Approximate
|
|
774
774
|
|
|
775
775
|
# Process additions
|
|
@@ -784,7 +784,7 @@ class CodebaseGraphManager:
|
|
|
784
784
|
|
|
785
785
|
lang_config = get_language_config(full_path.suffix)
|
|
786
786
|
if lang_config and lang_config.name in parsers:
|
|
787
|
-
|
|
787
|
+
builder._process_single_file(full_path, lang_config.name)
|
|
788
788
|
stats["nodes_added"] += 1 # Approximate
|
|
789
789
|
|
|
790
790
|
# Flush all pending operations
|
|
@@ -1751,7 +1751,7 @@ class CodebaseGraphManager:
|
|
|
1751
1751
|
)
|
|
1752
1752
|
|
|
1753
1753
|
# Build the graph
|
|
1754
|
-
|
|
1754
|
+
builder.run()
|
|
1755
1755
|
|
|
1756
1756
|
# Run build in thread pool
|
|
1757
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 =
|
|
37
|
+
model_config = get_provider_model()
|
|
38
38
|
|
|
39
39
|
# Create an agent with structured output for Cypher generation
|
|
40
40
|
cypher_agent = Agent(
|