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.

Files changed (73) hide show
  1. shotgun/agents/agent_manager.py +28 -194
  2. shotgun/agents/common.py +8 -14
  3. shotgun/agents/config/manager.py +33 -64
  4. shotgun/agents/config/models.py +1 -25
  5. shotgun/agents/config/provider.py +2 -2
  6. shotgun/agents/context_analyzer/analyzer.py +24 -2
  7. shotgun/agents/conversation_manager.py +19 -35
  8. shotgun/agents/export.py +2 -2
  9. shotgun/agents/history/history_processors.py +3 -99
  10. shotgun/agents/history/token_counting/anthropic.py +1 -17
  11. shotgun/agents/history/token_counting/base.py +3 -14
  12. shotgun/agents/history/token_counting/openai.py +1 -11
  13. shotgun/agents/history/token_counting/sentencepiece_counter.py +0 -8
  14. shotgun/agents/history/token_counting/tokenizer_cache.py +1 -3
  15. shotgun/agents/history/token_counting/utils.py +3 -0
  16. shotgun/agents/plan.py +2 -2
  17. shotgun/agents/research.py +3 -3
  18. shotgun/agents/specify.py +2 -2
  19. shotgun/agents/tasks.py +2 -2
  20. shotgun/agents/tools/codebase/file_read.py +2 -5
  21. shotgun/agents/tools/file_management.py +7 -11
  22. shotgun/agents/tools/web_search/__init__.py +8 -8
  23. shotgun/agents/tools/web_search/anthropic.py +2 -2
  24. shotgun/agents/tools/web_search/gemini.py +1 -1
  25. shotgun/agents/tools/web_search/openai.py +1 -1
  26. shotgun/agents/tools/web_search/utils.py +2 -2
  27. shotgun/agents/usage_manager.py +11 -16
  28. shotgun/build_constants.py +2 -2
  29. shotgun/cli/clear.py +1 -2
  30. shotgun/cli/compact.py +3 -3
  31. shotgun/cli/config.py +5 -8
  32. shotgun/cli/context.py +2 -2
  33. shotgun/cli/export.py +1 -1
  34. shotgun/cli/feedback.py +2 -4
  35. shotgun/cli/plan.py +1 -1
  36. shotgun/cli/research.py +1 -1
  37. shotgun/cli/specify.py +1 -1
  38. shotgun/cli/tasks.py +1 -1
  39. shotgun/codebase/core/change_detector.py +3 -5
  40. shotgun/codebase/core/code_retrieval.py +2 -4
  41. shotgun/codebase/core/ingestor.py +8 -10
  42. shotgun/codebase/core/manager.py +3 -3
  43. shotgun/codebase/core/nl_query.py +1 -1
  44. shotgun/logging_config.py +17 -10
  45. shotgun/main.py +1 -3
  46. shotgun/posthog_telemetry.py +4 -14
  47. shotgun/sentry_telemetry.py +2 -22
  48. shotgun/telemetry.py +1 -3
  49. shotgun/tui/app.py +65 -71
  50. shotgun/tui/components/context_indicator.py +0 -43
  51. shotgun/tui/containers.py +17 -15
  52. shotgun/tui/dependencies.py +2 -2
  53. shotgun/tui/screens/chat/chat_screen.py +40 -164
  54. shotgun/tui/screens/chat/help_text.py +15 -16
  55. shotgun/tui/screens/chat_screen/command_providers.py +0 -10
  56. shotgun/tui/screens/feedback.py +4 -4
  57. shotgun/tui/screens/model_picker.py +20 -21
  58. shotgun/tui/screens/provider_config.py +27 -50
  59. shotgun/tui/screens/shotgun_auth.py +2 -2
  60. shotgun/tui/screens/welcome.py +11 -14
  61. shotgun/tui/services/conversation_service.py +14 -16
  62. shotgun/tui/utils/mode_progress.py +7 -14
  63. shotgun/tui/widgets/widget_coordinator.py +0 -15
  64. shotgun/utils/file_system_utils.py +0 -19
  65. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/METADATA +1 -2
  66. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/RECORD +69 -73
  67. shotgun/exceptions.py +0 -32
  68. shotgun/tui/screens/github_issue.py +0 -102
  69. shotgun/tui/screens/onboarding.py +0 -431
  70. shotgun/utils/marketing.py +0 -110
  71. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/WHEEL +0 -0
  72. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/entry_points.txt +0 -0
  73. {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 await aiofiles.os.path.exists(file_path):
184
+ if not file_path.exists():
187
185
  raise FileNotFoundError(f"File not found: {filename}")
188
186
 
189
- async with aiofiles.open(file_path, encoding="utf-8") as f:
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 await aiofiles.os.path.exists(file_path)
236
+ if not file_path.exists()
240
237
  else FileOperationType.UPDATED
241
238
  )
242
239
 
243
240
  # Ensure parent directory exists
244
- await aiofiles.os.makedirs(file_path.parent, exist_ok=True)
241
+ file_path.parent.mkdir(parents=True, exist_ok=True)
245
242
 
246
243
  # Write content
247
244
  if mode == "a":
248
- async with aiofiles.open(file_path, "a", encoding="utf-8") as f:
249
- await f.write(content)
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
- async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
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
- async def get_available_web_search_tools() -> list[WebSearchTool]:
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 = await config_manager.load()
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 await is_provider_available(ProviderType.GOOGLE):
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 await is_provider_available(ProviderType.ANTHROPIC):
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 await is_provider_available(ProviderType.OPENAI):
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 await is_provider_available(ProviderType.OPENAI):
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 await is_provider_available(ProviderType.ANTHROPIC):
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 await is_provider_available(ProviderType.GOOGLE):
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 = await get_provider_model(ProviderType.ANTHROPIC)
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 = await get_provider_model(ProviderType.ANTHROPIC)
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 = await get_provider_model(ModelName.GEMINI_2_5_FLASH)
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 = await get_provider_model(ProviderType.OPENAI)
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
- async def is_provider_available(provider: ProviderType) -> bool:
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
- await get_provider_model(provider)
17
+ get_provider_model(provider)
18
18
  return True
19
19
  except ValueError:
20
20
  return False
@@ -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
- # Note: restore_usage_state needs to be called asynchronously after init
54
- # Caller should use: manager = SessionUsageManager(); await manager.restore_usage_state()
51
+ self.restore_usage_state()
55
52
 
56
- async def add_usage(
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
- await self.persist_usage_state()
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
- async def persist_usage_state(self) -> None:
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
- await aiofiles.os.makedirs(self._usage_path.parent, exist_ok=True)
93
- json_content = json.dumps(state.model_dump(mode="json"), indent=2)
94
- async with aiofiles.open(self._usage_path, "w", encoding="utf-8") as f:
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
- async def restore_usage_state(self) -> None:
103
- if not await aiofiles.os.path.exists(self._usage_path):
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
- async with aiofiles.open(self._usage_path, encoding="utf-8") as f:
109
- content = await f.read()
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:
@@ -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
- asyncio.run(manager.clear())
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 = await manager.load()
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 = await get_provider_model()
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
- await manager.save(conversation)
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
- asyncio.run(config_manager.initialize())
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
- asyncio.run(config_manager.update_provider(provider, api_key=api_key))
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
- asyncio.run(config_manager.initialize())
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
- asyncio.run(config_manager.update_provider(provider, api_key=api_key))
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 = asyncio.run(config_manager.load())
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 = await manager.load()
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 = await get_provider_model()
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 = asyncio.run(create_export_agent(agent_runtime_options, provider))
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
- asyncio.run(config_manager.load())
35
- shotgun_instance_id = asyncio.run(config_manager.get_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 = asyncio.run(create_plan_agent(agent_runtime_options, provider))
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 = await create_research_agent(agent_runtime_options, provider)
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 = asyncio.run(create_specify_agent(agent_runtime_options, provider))
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 = asyncio.run(create_tasks_agent(agent_runtime_options, provider))
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
- async def _calculate_file_hash(self, filepath: Path) -> str:
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
- async with aiofiles.open(filepath, "rb") as f:
316
- content = await f.read()
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
- async with aiofiles.open(full_path, encoding="utf-8") as f:
146
- content = await f.read()
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
- async def run(self) -> None:
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
- await self._process_files()
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
- async def _process_files(self) -> None:
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
- await self._process_single_file(filepath, lang_config.name)
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
- async def _process_single_file(self, filepath: Path, language: str) -> None:
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
- async with aiofiles.open(filepath, "rb") as f:
879
- content = await f.read()
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
- asyncio.run(builder.run())
1639
+ builder.run()
1642
1640
 
1643
1641
  logger.info(f"Graph successfully created at: {self.db_path}")
1644
1642
 
@@ -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
- await builder._process_single_file(full_path, lang_config.name)
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
- await builder._process_single_file(full_path, lang_config.name)
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
- asyncio.run(builder.run())
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 = await get_provider_model()
37
+ model_config = get_provider_model()
38
38
 
39
39
  # Create an agent with structured output for Cypher generation
40
40
  cypher_agent = Agent(