shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.dev1__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.
Files changed (175) hide show
  1. shotgun/agents/agent_manager.py +382 -60
  2. shotgun/agents/common.py +15 -9
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/constants.py +0 -6
  6. shotgun/agents/config/manager.py +383 -82
  7. shotgun/agents/config/models.py +122 -18
  8. shotgun/agents/config/provider.py +81 -15
  9. shotgun/agents/config/streaming_test.py +119 -0
  10. shotgun/agents/context_analyzer/__init__.py +28 -0
  11. shotgun/agents/context_analyzer/analyzer.py +475 -0
  12. shotgun/agents/context_analyzer/constants.py +9 -0
  13. shotgun/agents/context_analyzer/formatter.py +115 -0
  14. shotgun/agents/context_analyzer/models.py +212 -0
  15. shotgun/agents/conversation/__init__.py +18 -0
  16. shotgun/agents/conversation/filters.py +164 -0
  17. shotgun/agents/conversation/history/chunking.py +278 -0
  18. shotgun/agents/{history → conversation/history}/compaction.py +36 -5
  19. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  20. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  21. shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
  22. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
  23. shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
  24. shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
  25. shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
  26. shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
  27. shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
  28. shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
  29. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
  30. shotgun/agents/error/__init__.py +11 -0
  31. shotgun/agents/error/models.py +19 -0
  32. shotgun/agents/export.py +2 -2
  33. shotgun/agents/plan.py +2 -2
  34. shotgun/agents/research.py +3 -3
  35. shotgun/agents/runner.py +230 -0
  36. shotgun/agents/specify.py +2 -2
  37. shotgun/agents/tasks.py +2 -2
  38. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  39. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  40. shotgun/agents/tools/codebase/file_read.py +11 -2
  41. shotgun/agents/tools/codebase/query_graph.py +6 -0
  42. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  43. shotgun/agents/tools/file_management.py +27 -7
  44. shotgun/agents/tools/registry.py +217 -0
  45. shotgun/agents/tools/web_search/__init__.py +8 -8
  46. shotgun/agents/tools/web_search/anthropic.py +8 -2
  47. shotgun/agents/tools/web_search/gemini.py +7 -1
  48. shotgun/agents/tools/web_search/openai.py +8 -2
  49. shotgun/agents/tools/web_search/utils.py +2 -2
  50. shotgun/agents/usage_manager.py +16 -11
  51. shotgun/api_endpoints.py +7 -3
  52. shotgun/build_constants.py +2 -2
  53. shotgun/cli/clear.py +53 -0
  54. shotgun/cli/compact.py +188 -0
  55. shotgun/cli/config.py +8 -5
  56. shotgun/cli/context.py +154 -0
  57. shotgun/cli/error_handler.py +24 -0
  58. shotgun/cli/export.py +34 -34
  59. shotgun/cli/feedback.py +4 -2
  60. shotgun/cli/models.py +1 -0
  61. shotgun/cli/plan.py +34 -34
  62. shotgun/cli/research.py +18 -10
  63. shotgun/cli/spec/__init__.py +5 -0
  64. shotgun/cli/spec/backup.py +81 -0
  65. shotgun/cli/spec/commands.py +132 -0
  66. shotgun/cli/spec/models.py +48 -0
  67. shotgun/cli/spec/pull_service.py +219 -0
  68. shotgun/cli/specify.py +20 -19
  69. shotgun/cli/tasks.py +34 -34
  70. shotgun/cli/update.py +16 -2
  71. shotgun/codebase/core/change_detector.py +5 -3
  72. shotgun/codebase/core/code_retrieval.py +4 -2
  73. shotgun/codebase/core/ingestor.py +163 -15
  74. shotgun/codebase/core/manager.py +13 -4
  75. shotgun/codebase/core/nl_query.py +1 -1
  76. shotgun/codebase/models.py +2 -0
  77. shotgun/exceptions.py +357 -0
  78. shotgun/llm_proxy/__init__.py +17 -0
  79. shotgun/llm_proxy/client.py +215 -0
  80. shotgun/llm_proxy/models.py +137 -0
  81. shotgun/logging_config.py +60 -27
  82. shotgun/main.py +77 -11
  83. shotgun/posthog_telemetry.py +38 -29
  84. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
  85. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  86. shotgun/prompts/agents/plan.j2 +16 -0
  87. shotgun/prompts/agents/research.j2 +16 -3
  88. shotgun/prompts/agents/specify.j2 +54 -1
  89. shotgun/prompts/agents/state/system_state.j2 +0 -2
  90. shotgun/prompts/agents/tasks.j2 +16 -0
  91. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  92. shotgun/prompts/history/combine_summaries.j2 +53 -0
  93. shotgun/sdk/codebase.py +14 -3
  94. shotgun/sentry_telemetry.py +163 -16
  95. shotgun/settings.py +243 -0
  96. shotgun/shotgun_web/__init__.py +67 -1
  97. shotgun/shotgun_web/client.py +42 -1
  98. shotgun/shotgun_web/constants.py +46 -0
  99. shotgun/shotgun_web/exceptions.py +29 -0
  100. shotgun/shotgun_web/models.py +390 -0
  101. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  102. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  103. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  104. shotgun/shotgun_web/shared_specs/models.py +71 -0
  105. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  106. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  107. shotgun/shotgun_web/specs_client.py +703 -0
  108. shotgun/shotgun_web/supabase_client.py +31 -0
  109. shotgun/telemetry.py +10 -33
  110. shotgun/tui/app.py +310 -46
  111. shotgun/tui/commands/__init__.py +1 -1
  112. shotgun/tui/components/context_indicator.py +179 -0
  113. shotgun/tui/components/mode_indicator.py +70 -0
  114. shotgun/tui/components/status_bar.py +48 -0
  115. shotgun/tui/containers.py +91 -0
  116. shotgun/tui/dependencies.py +39 -0
  117. shotgun/tui/layout.py +5 -0
  118. shotgun/tui/protocols.py +45 -0
  119. shotgun/tui/screens/chat/__init__.py +5 -0
  120. shotgun/tui/screens/chat/chat.tcss +54 -0
  121. shotgun/tui/screens/chat/chat_screen.py +1531 -0
  122. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
  123. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  124. shotgun/tui/screens/chat/help_text.py +40 -0
  125. shotgun/tui/screens/chat/prompt_history.py +48 -0
  126. shotgun/tui/screens/chat.tcss +11 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +91 -4
  128. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  129. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  130. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  131. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  132. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  133. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  134. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  135. shotgun/tui/screens/confirmation_dialog.py +191 -0
  136. shotgun/tui/screens/directory_setup.py +45 -41
  137. shotgun/tui/screens/feedback.py +14 -7
  138. shotgun/tui/screens/github_issue.py +111 -0
  139. shotgun/tui/screens/model_picker.py +77 -32
  140. shotgun/tui/screens/onboarding.py +580 -0
  141. shotgun/tui/screens/pipx_migration.py +205 -0
  142. shotgun/tui/screens/provider_config.py +116 -35
  143. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  144. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  145. shotgun/tui/screens/shared_specs/models.py +56 -0
  146. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  147. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  148. shotgun/tui/screens/shotgun_auth.py +112 -18
  149. shotgun/tui/screens/spec_pull.py +288 -0
  150. shotgun/tui/screens/welcome.py +137 -11
  151. shotgun/tui/services/__init__.py +5 -0
  152. shotgun/tui/services/conversation_service.py +187 -0
  153. shotgun/tui/state/__init__.py +7 -0
  154. shotgun/tui/state/processing_state.py +185 -0
  155. shotgun/tui/utils/mode_progress.py +14 -7
  156. shotgun/tui/widgets/__init__.py +5 -0
  157. shotgun/tui/widgets/widget_coordinator.py +263 -0
  158. shotgun/utils/file_system_utils.py +22 -2
  159. shotgun/utils/marketing.py +110 -0
  160. shotgun/utils/update_checker.py +69 -14
  161. shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
  162. shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
  163. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  164. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
  165. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
  166. shotgun/tui/screens/chat.py +0 -996
  167. shotgun/tui/screens/chat_screen/history.py +0 -335
  168. shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
  169. shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
  170. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  171. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  172. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  173. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  174. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  175. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
shotgun/cli/update.py CHANGED
@@ -45,7 +45,7 @@ def update(
45
45
 
46
46
  This command will:
47
47
  - Check PyPI for the latest version
48
- - Detect your installation method (pipx, pip, or venv)
48
+ - Detect your installation method (uvx, uv-tool, pipx, pip, or venv)
49
49
  - Perform the appropriate upgrade command
50
50
 
51
51
  Examples:
@@ -93,6 +93,8 @@ def update(
93
93
  )
94
94
  console.print(
95
95
  "Use --force to update anyway, or install the stable version with:\n"
96
+ " uv tool install shotgun-sh\n"
97
+ " or\n"
96
98
  " pipx install shotgun-sh\n"
97
99
  " or\n"
98
100
  " pip install shotgun-sh",
@@ -134,7 +136,19 @@ def update(
134
136
  console.print(f"\n[red]✗[/red] {message}", style="bold red")
135
137
 
136
138
  # Provide manual update instructions
137
- if method == "pipx":
139
+ if method == "uvx":
140
+ console.print(
141
+ "\n[yellow]Run uvx again to use the latest version:[/yellow]\n"
142
+ " uvx shotgun-sh\n"
143
+ "\n[yellow]Or install permanently:[/yellow]\n"
144
+ " uv tool install shotgun-sh"
145
+ )
146
+ elif method == "uv-tool":
147
+ console.print(
148
+ "\n[yellow]Try updating manually:[/yellow]\n"
149
+ " uv tool upgrade shotgun-sh"
150
+ )
151
+ elif method == "pipx":
138
152
  console.print(
139
153
  "\n[yellow]Try updating manually:[/yellow]\n"
140
154
  " pipx upgrade shotgun-sh"
@@ -6,6 +6,7 @@ from enum import Enum
6
6
  from pathlib import Path
7
7
  from typing import Any, cast
8
8
 
9
+ import aiofiles
9
10
  import kuzu
10
11
 
11
12
  from shotgun.logging_config import get_logger
@@ -301,7 +302,7 @@ class ChangeDetector:
301
302
  # Direct substring match
302
303
  return pattern in filepath
303
304
 
304
- def _calculate_file_hash(self, filepath: Path) -> str:
305
+ async def _calculate_file_hash(self, filepath: Path) -> str:
305
306
  """Calculate hash of file contents.
306
307
 
307
308
  Args:
@@ -311,8 +312,9 @@ class ChangeDetector:
311
312
  SHA256 hash of file contents
312
313
  """
313
314
  try:
314
- with open(filepath, "rb") as f:
315
- return hashlib.sha256(f.read()).hexdigest()
315
+ async with aiofiles.open(filepath, "rb") as f:
316
+ content = await f.read()
317
+ return hashlib.sha256(content).hexdigest()
316
318
  except Exception as e:
317
319
  logger.error(f"Failed to calculate hash for {filepath}: {e}")
318
320
  return ""
@@ -3,6 +3,7 @@
3
3
  from pathlib import Path
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ import aiofiles
6
7
  from pydantic import BaseModel
7
8
 
8
9
  from shotgun.logging_config import get_logger
@@ -141,8 +142,9 @@ async def retrieve_code_by_qualified_name(
141
142
 
142
143
  # Read the file and extract the snippet
143
144
  try:
144
- with full_path.open("r", encoding="utf-8") as f:
145
- all_lines = f.readlines()
145
+ async with aiofiles.open(full_path, encoding="utf-8") as f:
146
+ content = await f.read()
147
+ all_lines = content.splitlines(keepends=True)
146
148
 
147
149
  # Extract the relevant lines (1-indexed to 0-indexed)
148
150
  snippet_lines = all_lines[start_line - 1 : end_line]
@@ -1,13 +1,16 @@
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
6
7
  import uuid
7
8
  from collections import defaultdict
9
+ from collections.abc import Callable
8
10
  from pathlib import Path
9
11
  from typing import Any
10
12
 
13
+ import aiofiles
11
14
  import kuzu
12
15
  from tree_sitter import Node, Parser, QueryCursor
13
16
 
@@ -196,11 +199,21 @@ class Ingestor:
196
199
  return True
197
200
  return False
198
201
 
199
- def flush_nodes(self) -> None:
200
- """Flush pending node insertions to the database."""
202
+ def flush_nodes(
203
+ self,
204
+ progress_callback: Callable[[int, int], None] | None = None,
205
+ ) -> None:
206
+ """Flush pending node insertions to the database.
207
+
208
+ Args:
209
+ progress_callback: Optional callback(current, total) for progress reporting
210
+ """
201
211
  if not self.node_buffer:
202
212
  return
203
213
 
214
+ total_nodes = len(self.node_buffer)
215
+ processed = 0
216
+
204
217
  # Group nodes by label
205
218
  nodes_by_label: dict[str, list[dict[str, Any]]] = defaultdict(list)
206
219
  for label, properties in self.node_buffer:
@@ -237,9 +250,18 @@ class Ingestor:
237
250
  params = dict(zip(prop_names, prop_values, strict=False))
238
251
  self.conn.execute(query, params)
239
252
 
253
+ # Report progress
254
+ processed += 1
255
+ if progress_callback and processed % 10 == 0:
256
+ progress_callback(processed, total_nodes)
257
+
240
258
  except Exception as e:
241
259
  logger.error(f"Failed to insert {label} nodes: {e}")
242
260
 
261
+ # Final progress report
262
+ if progress_callback:
263
+ progress_callback(total_nodes, total_nodes)
264
+
243
265
  # Log node counts by type
244
266
  node_type_counts: dict[str, int] = {}
245
267
  for label, _ in self.node_buffer:
@@ -278,11 +300,21 @@ class Ingestor:
278
300
 
279
301
  # Don't auto-flush relationships - wait for explicit flush_all() to ensure nodes exist first
280
302
 
281
- def flush_relationships(self) -> None:
282
- """Flush pending relationship insertions to the database."""
303
+ def flush_relationships(
304
+ self,
305
+ progress_callback: Callable[[int, int], None] | None = None,
306
+ ) -> None:
307
+ """Flush pending relationship insertions to the database.
308
+
309
+ Args:
310
+ progress_callback: Optional callback(current, total) for progress reporting
311
+ """
283
312
  if not self.relationship_buffer:
284
313
  return
285
314
 
315
+ total_rels = len(self.relationship_buffer)
316
+ processed = 0
317
+
286
318
  # Group relationships by type
287
319
  rels_by_type: dict[
288
320
  str, list[tuple[str, str, Any, str, str, str, Any, dict[str, Any] | None]]
@@ -297,7 +329,7 @@ class Ingestor:
297
329
  to_label,
298
330
  to_key,
299
331
  to_value,
300
- properties,
332
+ _properties,
301
333
  ) = rel_data
302
334
 
303
335
  # Determine actual table name
@@ -321,7 +353,7 @@ class Ingestor:
321
353
  to_label,
322
354
  to_key,
323
355
  to_value,
324
- properties,
356
+ _properties,
325
357
  ) = rel_data
326
358
 
327
359
  # Build MATCH and MERGE query (use MERGE to avoid duplicate relationships)
@@ -335,6 +367,11 @@ class Ingestor:
335
367
  try:
336
368
  self.conn.execute(query, params)
337
369
  success_count += 1
370
+
371
+ # Report progress
372
+ processed += 1
373
+ if progress_callback and processed % 10 == 0:
374
+ progress_callback(processed, total_rels)
338
375
  except Exception as e:
339
376
  logger.error(
340
377
  f"Failed to create single relationship {table_name}: {from_label}({from_value}) -> {to_label}({to_value})"
@@ -358,6 +395,10 @@ class Ingestor:
358
395
  # Don't swallow the exception - let it propagate
359
396
  raise
360
397
 
398
+ # Final progress report
399
+ if progress_callback:
400
+ progress_callback(total_rels, total_rels)
401
+
361
402
  # Log summary of flushed relationships
362
403
  logger.info(
363
404
  f"Flushed {len(self.relationship_buffer)} relationships: {relationship_counts}"
@@ -584,6 +625,9 @@ class SimpleGraphBuilder:
584
625
  self.ignore_dirs = self.ignore_dirs.union(set(exclude_patterns))
585
626
  self.progress_callback = progress_callback
586
627
 
628
+ # Generate unique session ID for correlating timing events in PostHog
629
+ self._index_session_id = str(uuid.uuid4())[:8]
630
+
587
631
  # Caches
588
632
  self.structural_elements: dict[Path, str | None] = {}
589
633
  self.ast_cache: dict[Path, tuple[Node, str]] = {}
@@ -619,25 +663,129 @@ class SimpleGraphBuilder:
619
663
  # Don't let progress callback errors crash the build
620
664
  logger.debug(f"Progress callback error: {e}")
621
665
 
622
- def run(self) -> None:
666
+ def _log_timing(
667
+ self,
668
+ phase: str,
669
+ duration: float,
670
+ items: int,
671
+ extra_props: dict[str, Any] | None = None,
672
+ ) -> None:
673
+ """Log timing data to PostHog for analysis."""
674
+ from shotgun.posthog_telemetry import track_event
675
+
676
+ properties: dict[str, Any] = {
677
+ "session_id": self._index_session_id,
678
+ "phase": phase,
679
+ "duration_seconds": round(duration, 3),
680
+ "item_count": items,
681
+ }
682
+ if extra_props:
683
+ properties.update(extra_props)
684
+
685
+ track_event("codebase_index_phase_completed", properties)
686
+
687
+ def _log_summary(
688
+ self,
689
+ total_duration: float,
690
+ total_files: int,
691
+ total_nodes: int,
692
+ total_relationships: int,
693
+ ) -> None:
694
+ """Log indexing summary event to PostHog."""
695
+ from shotgun.posthog_telemetry import track_event
696
+
697
+ track_event(
698
+ "codebase_index_completed",
699
+ {
700
+ "session_id": self._index_session_id,
701
+ "total_duration_seconds": round(total_duration, 3),
702
+ "total_files": total_files,
703
+ "total_nodes": total_nodes,
704
+ "total_relationships": total_relationships,
705
+ },
706
+ )
707
+
708
+ async def run(self) -> None:
623
709
  """Run the three-pass graph building process."""
624
710
  logger.info(f"Building graph for project: {self.project_name}")
625
711
 
626
712
  # Pass 1: Structure
627
713
  logger.info("Pass 1: Identifying packages and folders...")
714
+ t0 = time.time()
628
715
  self._identify_structure()
716
+ t1 = time.time()
717
+ self._log_timing("structure", t1 - t0, len(self.structural_elements))
629
718
 
630
719
  # Pass 2: Definitions
631
720
  logger.info("Pass 2: Processing files and extracting definitions...")
632
- self._process_files()
721
+ t2 = time.time()
722
+ await self._process_files()
723
+ t3 = time.time()
724
+ self._log_timing(
725
+ "definitions",
726
+ t3 - t2,
727
+ len(self.ast_cache),
728
+ {"file_count": len(self.ast_cache)},
729
+ )
633
730
 
634
731
  # Pass 3: Relationships
635
732
  logger.info("Pass 3: Processing relationships (calls, imports)...")
733
+ t4 = time.time()
636
734
  self._process_relationships()
735
+ t5 = time.time()
736
+ self._log_timing("relationships", t5 - t4, len(self.ast_cache))
637
737
 
638
738
  # Flush all pending operations
639
739
  logger.info("Flushing all data to database...")
640
- self.ingestor.flush_all()
740
+ t6 = time.time()
741
+ node_count = len(self.ingestor.node_buffer)
742
+
743
+ # Create progress callback for flush_nodes
744
+ def node_progress(current: int, total: int) -> None:
745
+ self._report_progress(
746
+ "flush_nodes", "Flushing nodes to database", current, total
747
+ )
748
+
749
+ self.ingestor.flush_nodes(progress_callback=node_progress)
750
+ self._report_progress(
751
+ "flush_nodes", "Flushing nodes to database", node_count, node_count, True
752
+ )
753
+ t7 = time.time()
754
+ self._log_timing("flush_nodes", t7 - t6, node_count, {"node_count": node_count})
755
+
756
+ rel_count = len(self.ingestor.relationship_buffer)
757
+
758
+ # Create progress callback for flush_relationships
759
+ def rel_progress(current: int, total: int) -> None:
760
+ self._report_progress(
761
+ "flush_relationships",
762
+ "Flushing relationships to database",
763
+ current,
764
+ total,
765
+ )
766
+
767
+ self.ingestor.flush_relationships(progress_callback=rel_progress)
768
+ self._report_progress(
769
+ "flush_relationships",
770
+ "Flushing relationships to database",
771
+ rel_count,
772
+ rel_count,
773
+ True,
774
+ )
775
+ t8 = time.time()
776
+ self._log_timing(
777
+ "flush_relationships", t8 - t7, rel_count, {"relationship_count": rel_count}
778
+ )
779
+
780
+ # Track summary event with totals (no PII - only numeric metadata)
781
+ total_duration = t8 - t0
782
+ self._log_summary(
783
+ total_duration=total_duration,
784
+ total_files=len(self.ast_cache),
785
+ total_nodes=node_count,
786
+ total_relationships=rel_count,
787
+ )
788
+
641
789
  logger.info("Graph building complete!")
642
790
 
643
791
  def _identify_structure(self) -> None:
@@ -771,7 +919,7 @@ class SimpleGraphBuilder:
771
919
  phase_complete=True,
772
920
  )
773
921
 
774
- def _process_files(self) -> None:
922
+ async def _process_files(self) -> None:
775
923
  """Second pass: Process files and extract definitions."""
776
924
  # First pass: Count total files
777
925
  total_files = 0
@@ -807,7 +955,7 @@ class SimpleGraphBuilder:
807
955
  lang_config = get_language_config(ext)
808
956
 
809
957
  if lang_config and lang_config.name in self.parsers:
810
- self._process_single_file(filepath, lang_config.name)
958
+ await self._process_single_file(filepath, lang_config.name)
811
959
  file_count += 1
812
960
 
813
961
  # Report progress after each file
@@ -832,7 +980,7 @@ class SimpleGraphBuilder:
832
980
  phase_complete=True,
833
981
  )
834
982
 
835
- def _process_single_file(self, filepath: Path, language: str) -> None:
983
+ async def _process_single_file(self, filepath: Path, language: str) -> None:
836
984
  """Process a single file."""
837
985
  relative_path = filepath.relative_to(self.repo_path)
838
986
  relative_path_str = str(relative_path).replace(os.sep, "/")
@@ -873,8 +1021,8 @@ class SimpleGraphBuilder:
873
1021
 
874
1022
  # Parse file
875
1023
  try:
876
- with open(filepath, "rb") as f:
877
- content = f.read()
1024
+ async with aiofiles.open(filepath, "rb") as f:
1025
+ content = await f.read()
878
1026
 
879
1027
  parser = self.parsers[language]
880
1028
  tree = parser.parse(content)
@@ -1636,7 +1784,7 @@ class CodebaseIngestor:
1636
1784
  )
1637
1785
  if self.project_name:
1638
1786
  builder.project_name = self.project_name
1639
- builder.run()
1787
+ asyncio.run(builder.run())
1640
1788
 
1641
1789
  logger.info(f"Graph successfully created at: {self.db_path}")
1642
1790
 
@@ -371,7 +371,16 @@ class CodebaseGraphManager:
371
371
  )
372
372
  import shutil
373
373
 
374
- shutil.rmtree(graph_path)
374
+ # Handle both files and directories (kuzu v0.11.2+ uses files)
375
+ if graph_path.is_file():
376
+ graph_path.unlink() # Delete file
377
+ # Also delete WAL file if it exists
378
+ wal_path = graph_path.with_suffix(graph_path.suffix + ".wal")
379
+ if wal_path.exists():
380
+ wal_path.unlink()
381
+ logger.debug(f"Deleted WAL file: {wal_path}")
382
+ else:
383
+ shutil.rmtree(graph_path) # Delete directory
375
384
 
376
385
  # Import the builder from local core module
377
386
  from shotgun.codebase.core import CodebaseIngestor
@@ -760,7 +769,7 @@ class CodebaseGraphManager:
760
769
 
761
770
  lang_config = get_language_config(full_path.suffix)
762
771
  if lang_config and lang_config.name in parsers:
763
- builder._process_single_file(full_path, lang_config.name)
772
+ await builder._process_single_file(full_path, lang_config.name)
764
773
  stats["nodes_modified"] += 1 # Approximate
765
774
 
766
775
  # Process additions
@@ -775,7 +784,7 @@ class CodebaseGraphManager:
775
784
 
776
785
  lang_config = get_language_config(full_path.suffix)
777
786
  if lang_config and lang_config.name in parsers:
778
- builder._process_single_file(full_path, lang_config.name)
787
+ await builder._process_single_file(full_path, lang_config.name)
779
788
  stats["nodes_added"] += 1 # Approximate
780
789
 
781
790
  # Flush all pending operations
@@ -1742,7 +1751,7 @@ class CodebaseGraphManager:
1742
1751
  )
1743
1752
 
1744
1753
  # Build the graph
1745
- builder.run()
1754
+ asyncio.run(builder.run())
1746
1755
 
1747
1756
  # Run build in thread pool
1748
1757
  await anyio.to_thread.run_sync(_build_graph)
@@ -34,7 +34,7 @@ async def llm_cypher_prompt(
34
34
  Returns:
35
35
  CypherGenerationResponse with cypher_query, can_generate flag, and reason if not
36
36
  """
37
- model_config = get_provider_model()
37
+ model_config = await get_provider_model()
38
38
 
39
39
  # Create an agent with structured output for Cypher generation
40
40
  cypher_agent = Agent(
@@ -29,6 +29,8 @@ class ProgressPhase(StrEnum):
29
29
  STRUCTURE = "structure" # Identifying packages and folders
30
30
  DEFINITIONS = "definitions" # Processing files and extracting definitions
31
31
  RELATIONSHIPS = "relationships" # Processing relationships (calls, imports)
32
+ FLUSH_NODES = "flush_nodes" # Flushing nodes to database
33
+ FLUSH_RELATIONSHIPS = "flush_relationships" # Flushing relationships to database
32
34
 
33
35
 
34
36
  class IndexProgress(BaseModel):