shotgun-sh 0.1.0__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 (130) hide show
  1. shotgun/__init__.py +5 -0
  2. shotgun/agents/__init__.py +1 -0
  3. shotgun/agents/agent_manager.py +651 -0
  4. shotgun/agents/common.py +549 -0
  5. shotgun/agents/config/__init__.py +13 -0
  6. shotgun/agents/config/constants.py +17 -0
  7. shotgun/agents/config/manager.py +294 -0
  8. shotgun/agents/config/models.py +185 -0
  9. shotgun/agents/config/provider.py +206 -0
  10. shotgun/agents/conversation_history.py +106 -0
  11. shotgun/agents/conversation_manager.py +105 -0
  12. shotgun/agents/export.py +96 -0
  13. shotgun/agents/history/__init__.py +5 -0
  14. shotgun/agents/history/compaction.py +85 -0
  15. shotgun/agents/history/constants.py +19 -0
  16. shotgun/agents/history/context_extraction.py +108 -0
  17. shotgun/agents/history/history_building.py +104 -0
  18. shotgun/agents/history/history_processors.py +426 -0
  19. shotgun/agents/history/message_utils.py +84 -0
  20. shotgun/agents/history/token_counting.py +429 -0
  21. shotgun/agents/history/token_estimation.py +138 -0
  22. shotgun/agents/messages.py +35 -0
  23. shotgun/agents/models.py +275 -0
  24. shotgun/agents/plan.py +98 -0
  25. shotgun/agents/research.py +108 -0
  26. shotgun/agents/specify.py +98 -0
  27. shotgun/agents/tasks.py +96 -0
  28. shotgun/agents/tools/__init__.py +34 -0
  29. shotgun/agents/tools/codebase/__init__.py +28 -0
  30. shotgun/agents/tools/codebase/codebase_shell.py +256 -0
  31. shotgun/agents/tools/codebase/directory_lister.py +141 -0
  32. shotgun/agents/tools/codebase/file_read.py +144 -0
  33. shotgun/agents/tools/codebase/models.py +252 -0
  34. shotgun/agents/tools/codebase/query_graph.py +67 -0
  35. shotgun/agents/tools/codebase/retrieve_code.py +81 -0
  36. shotgun/agents/tools/file_management.py +218 -0
  37. shotgun/agents/tools/user_interaction.py +37 -0
  38. shotgun/agents/tools/web_search/__init__.py +60 -0
  39. shotgun/agents/tools/web_search/anthropic.py +144 -0
  40. shotgun/agents/tools/web_search/gemini.py +85 -0
  41. shotgun/agents/tools/web_search/openai.py +98 -0
  42. shotgun/agents/tools/web_search/utils.py +20 -0
  43. shotgun/build_constants.py +20 -0
  44. shotgun/cli/__init__.py +1 -0
  45. shotgun/cli/codebase/__init__.py +5 -0
  46. shotgun/cli/codebase/commands.py +202 -0
  47. shotgun/cli/codebase/models.py +21 -0
  48. shotgun/cli/config.py +275 -0
  49. shotgun/cli/export.py +81 -0
  50. shotgun/cli/models.py +10 -0
  51. shotgun/cli/plan.py +73 -0
  52. shotgun/cli/research.py +85 -0
  53. shotgun/cli/specify.py +69 -0
  54. shotgun/cli/tasks.py +78 -0
  55. shotgun/cli/update.py +152 -0
  56. shotgun/cli/utils.py +25 -0
  57. shotgun/codebase/__init__.py +12 -0
  58. shotgun/codebase/core/__init__.py +46 -0
  59. shotgun/codebase/core/change_detector.py +358 -0
  60. shotgun/codebase/core/code_retrieval.py +243 -0
  61. shotgun/codebase/core/ingestor.py +1497 -0
  62. shotgun/codebase/core/language_config.py +297 -0
  63. shotgun/codebase/core/manager.py +1662 -0
  64. shotgun/codebase/core/nl_query.py +331 -0
  65. shotgun/codebase/core/parser_loader.py +128 -0
  66. shotgun/codebase/models.py +111 -0
  67. shotgun/codebase/service.py +206 -0
  68. shotgun/logging_config.py +227 -0
  69. shotgun/main.py +167 -0
  70. shotgun/posthog_telemetry.py +158 -0
  71. shotgun/prompts/__init__.py +5 -0
  72. shotgun/prompts/agents/__init__.py +1 -0
  73. shotgun/prompts/agents/export.j2 +350 -0
  74. shotgun/prompts/agents/partials/codebase_understanding.j2 +87 -0
  75. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +37 -0
  76. shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
  77. shotgun/prompts/agents/partials/interactive_mode.j2 +26 -0
  78. shotgun/prompts/agents/plan.j2 +144 -0
  79. shotgun/prompts/agents/research.j2 +69 -0
  80. shotgun/prompts/agents/specify.j2 +51 -0
  81. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +19 -0
  82. shotgun/prompts/agents/state/system_state.j2 +31 -0
  83. shotgun/prompts/agents/tasks.j2 +143 -0
  84. shotgun/prompts/codebase/__init__.py +1 -0
  85. shotgun/prompts/codebase/cypher_query_patterns.j2 +223 -0
  86. shotgun/prompts/codebase/cypher_system.j2 +28 -0
  87. shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
  88. shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
  89. shotgun/prompts/codebase/partials/graph_schema.j2 +30 -0
  90. shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
  91. shotgun/prompts/history/__init__.py +1 -0
  92. shotgun/prompts/history/incremental_summarization.j2 +53 -0
  93. shotgun/prompts/history/summarization.j2 +46 -0
  94. shotgun/prompts/loader.py +140 -0
  95. shotgun/py.typed +0 -0
  96. shotgun/sdk/__init__.py +13 -0
  97. shotgun/sdk/codebase.py +219 -0
  98. shotgun/sdk/exceptions.py +17 -0
  99. shotgun/sdk/models.py +189 -0
  100. shotgun/sdk/services.py +23 -0
  101. shotgun/sentry_telemetry.py +87 -0
  102. shotgun/telemetry.py +93 -0
  103. shotgun/tui/__init__.py +0 -0
  104. shotgun/tui/app.py +116 -0
  105. shotgun/tui/commands/__init__.py +76 -0
  106. shotgun/tui/components/prompt_input.py +69 -0
  107. shotgun/tui/components/spinner.py +86 -0
  108. shotgun/tui/components/splash.py +25 -0
  109. shotgun/tui/components/vertical_tail.py +13 -0
  110. shotgun/tui/screens/chat.py +782 -0
  111. shotgun/tui/screens/chat.tcss +43 -0
  112. shotgun/tui/screens/chat_screen/__init__.py +0 -0
  113. shotgun/tui/screens/chat_screen/command_providers.py +219 -0
  114. shotgun/tui/screens/chat_screen/hint_message.py +40 -0
  115. shotgun/tui/screens/chat_screen/history.py +221 -0
  116. shotgun/tui/screens/directory_setup.py +113 -0
  117. shotgun/tui/screens/provider_config.py +221 -0
  118. shotgun/tui/screens/splash.py +31 -0
  119. shotgun/tui/styles.tcss +10 -0
  120. shotgun/tui/utils/__init__.py +5 -0
  121. shotgun/tui/utils/mode_progress.py +257 -0
  122. shotgun/utils/__init__.py +5 -0
  123. shotgun/utils/env_utils.py +35 -0
  124. shotgun/utils/file_system_utils.py +36 -0
  125. shotgun/utils/update_checker.py +375 -0
  126. shotgun_sh-0.1.0.dist-info/METADATA +466 -0
  127. shotgun_sh-0.1.0.dist-info/RECORD +130 -0
  128. shotgun_sh-0.1.0.dist-info/WHEEL +4 -0
  129. shotgun_sh-0.1.0.dist-info/entry_points.txt +2 -0
  130. shotgun_sh-0.1.0.dist-info/licenses/LICENSE +21 -0
shotgun/cli/update.py ADDED
@@ -0,0 +1,152 @@
1
+ """Update command for shotgun CLI."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+
9
+ from shotgun.logging_config import get_logger
10
+ from shotgun.utils.update_checker import (
11
+ detect_installation_method,
12
+ get_latest_version,
13
+ is_dev_version,
14
+ perform_update,
15
+ )
16
+
17
+ logger = get_logger(__name__)
18
+ console = Console()
19
+ app = typer.Typer()
20
+
21
+
22
+ @app.callback(invoke_without_command=True)
23
+ def update(
24
+ ctx: typer.Context,
25
+ force: Annotated[
26
+ bool,
27
+ typer.Option(
28
+ "--force",
29
+ "-f",
30
+ help="Force update even for development versions",
31
+ ),
32
+ ] = False,
33
+ check_only: Annotated[
34
+ bool,
35
+ typer.Option(
36
+ "--check",
37
+ "-c",
38
+ help="Check for updates without installing",
39
+ ),
40
+ ] = False,
41
+ ) -> None:
42
+ """Check for and install updates to shotgun-sh.
43
+
44
+ This command will:
45
+ - Check PyPI for the latest version
46
+ - Detect your installation method (pipx, pip, or venv)
47
+ - Perform the appropriate upgrade command
48
+
49
+ Examples:
50
+ shotgun update # Check and install updates
51
+ shotgun update --check # Only check for updates
52
+ shotgun update --force # Force update (even for dev versions)
53
+ """
54
+ if ctx.resilient_parsing:
55
+ return
56
+
57
+ # Handle check-only mode
58
+ if check_only:
59
+ with Progress(
60
+ SpinnerColumn(),
61
+ TextColumn("[progress.description]{task.description}"),
62
+ console=console,
63
+ ) as progress:
64
+ progress.add_task("Checking for updates...", total=None)
65
+
66
+ latest = get_latest_version()
67
+ if not latest:
68
+ console.print(
69
+ "[red]✗[/red] Failed to check for updates", style="bold red"
70
+ )
71
+ raise typer.Exit(1)
72
+
73
+ from shotgun import __version__
74
+ from shotgun.utils.update_checker import compare_versions
75
+
76
+ if compare_versions(__version__, latest):
77
+ console.print(
78
+ f"[green]✓[/green] Update available: [cyan]{__version__}[/cyan] → [green]{latest}[/green]",
79
+ style="bold",
80
+ )
81
+ console.print("Run 'shotgun update' to install the update")
82
+ else:
83
+ console.print(
84
+ f"[green]✓[/green] You're on the latest version ([cyan]{__version__}[/cyan])",
85
+ style="bold",
86
+ )
87
+ return
88
+
89
+ # Check for dev version
90
+ if is_dev_version() and not force:
91
+ console.print(
92
+ "[yellow]⚠[/yellow] You're running a development version",
93
+ style="bold yellow",
94
+ )
95
+ console.print(
96
+ "Use --force to update anyway, or install the stable version with:\n"
97
+ " pipx install shotgun-sh\n"
98
+ " or\n"
99
+ " pip install shotgun-sh",
100
+ )
101
+ raise typer.Exit(1)
102
+
103
+ # Confirm if forcing dev version update
104
+ if is_dev_version() and force:
105
+ confirm = typer.confirm(
106
+ "⚠️ You're about to replace a development version. Continue?",
107
+ default=False,
108
+ )
109
+ if not confirm:
110
+ console.print("Update cancelled", style="dim")
111
+ raise typer.Exit(0)
112
+
113
+ # Detect installation method
114
+ method = detect_installation_method()
115
+ console.print(f"Installation method: [cyan]{method}[/cyan]", style="dim")
116
+
117
+ # Perform update
118
+ with Progress(
119
+ SpinnerColumn(),
120
+ TextColumn("[progress.description]{task.description}"),
121
+ console=console,
122
+ ) as progress:
123
+ task = progress.add_task("Updating shotgun-sh...", total=None)
124
+
125
+ success, message = perform_update(force=force)
126
+
127
+ if success:
128
+ progress.update(task, description="[green]✓[/green] Update complete!")
129
+ console.print(f"\n[green]✓[/green] {message}", style="bold green")
130
+ console.print(
131
+ "\n[dim]Restart your terminal or run 'shotgun --version' to verify the update[/dim]"
132
+ )
133
+ else:
134
+ progress.update(task, description="[red]✗[/red] Update failed")
135
+ console.print(f"\n[red]✗[/red] {message}", style="bold red")
136
+
137
+ # Provide manual update instructions
138
+ if method == "pipx":
139
+ console.print(
140
+ "\n[yellow]Try updating manually:[/yellow]\n"
141
+ " pipx upgrade shotgun-sh"
142
+ )
143
+ else:
144
+ console.print(
145
+ "\n[yellow]Try updating manually:[/yellow]\n"
146
+ " pip install --upgrade shotgun-sh"
147
+ )
148
+ raise typer.Exit(1)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ app()
shotgun/cli/utils.py ADDED
@@ -0,0 +1,25 @@
1
+ """Common utilities for CLI commands."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from .models import OutputFormat
9
+
10
+
11
+ def format_result_json(result: Any) -> str:
12
+ """Format result object as JSON using Pydantic serialization."""
13
+ if isinstance(result, BaseModel):
14
+ return result.model_dump_json(indent=2)
15
+ else:
16
+ # Fallback for non-Pydantic objects
17
+ return json.dumps({"result": str(result)}, indent=2)
18
+
19
+
20
+ def output_result(result: Any, format_type: OutputFormat = OutputFormat.TEXT) -> None:
21
+ """Output result in specified format."""
22
+ if format_type == OutputFormat.JSON:
23
+ print(format_result_json(result))
24
+ else: # Default to text
25
+ print(str(result))
@@ -0,0 +1,12 @@
1
+ """Shotgun codebase analysis and graph management."""
2
+
3
+ from shotgun.codebase.models import CodebaseGraph, GraphStatus, QueryResult, QueryType
4
+ from shotgun.codebase.service import CodebaseService
5
+
6
+ __all__ = [
7
+ "CodebaseService",
8
+ "CodebaseGraph",
9
+ "GraphStatus",
10
+ "QueryResult",
11
+ "QueryType",
12
+ ]
@@ -0,0 +1,46 @@
1
+ """Core components for codebase understanding."""
2
+
3
+ from shotgun.codebase.core.code_retrieval import (
4
+ CodeSnippet,
5
+ retrieve_code_by_cypher,
6
+ retrieve_code_by_qualified_name,
7
+ )
8
+ from shotgun.codebase.core.ingestor import (
9
+ CodebaseIngestor,
10
+ Ingestor,
11
+ SimpleGraphBuilder,
12
+ )
13
+ from shotgun.codebase.core.language_config import (
14
+ LANGUAGE_CONFIGS,
15
+ LanguageConfig,
16
+ get_language_config,
17
+ )
18
+ from shotgun.codebase.core.manager import CodebaseGraphManager
19
+ from shotgun.codebase.core.nl_query import (
20
+ clean_cypher_response,
21
+ generate_cypher,
22
+ generate_cypher_openai_async,
23
+ )
24
+ from shotgun.codebase.core.parser_loader import load_parsers
25
+
26
+ __all__ = [
27
+ # Ingestor classes
28
+ "CodebaseIngestor",
29
+ "Ingestor",
30
+ "SimpleGraphBuilder",
31
+ "CodebaseGraphManager",
32
+ # Language configuration
33
+ "LanguageConfig",
34
+ "LANGUAGE_CONFIGS",
35
+ "get_language_config",
36
+ # Parser loading
37
+ "load_parsers",
38
+ # Natural language query
39
+ "generate_cypher",
40
+ "generate_cypher_openai_async",
41
+ "clean_cypher_response",
42
+ # Code retrieval
43
+ "CodeSnippet",
44
+ "retrieve_code_by_qualified_name",
45
+ "retrieve_code_by_cypher",
46
+ ]
@@ -0,0 +1,358 @@
1
+ """Change detection for incremental graph updates."""
2
+
3
+ import hashlib
4
+ import os
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any, cast
8
+
9
+ import kuzu
10
+
11
+ from shotgun.logging_config import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class ChangeType(Enum):
17
+ """Types of file changes."""
18
+
19
+ ADDED = "added"
20
+ MODIFIED = "modified"
21
+ DELETED = "deleted"
22
+
23
+
24
+ class ChangeDetector:
25
+ """Detects changes in the codebase by comparing with FileMetadata nodes."""
26
+
27
+ def __init__(self, connection: kuzu.Connection, repo_path: Path):
28
+ """Initialize change detector.
29
+
30
+ Args:
31
+ connection: Kuzu database connection
32
+ repo_path: Root path of the repository
33
+ """
34
+ self.conn = connection
35
+ self.repo_path = Path(repo_path).resolve()
36
+
37
+ # Validate that repo path exists
38
+ if not self.repo_path.exists():
39
+ raise ValueError(f"Repository path does not exist: {self.repo_path}")
40
+ if not self.repo_path.is_dir():
41
+ raise ValueError(f"Repository path is not a directory: {self.repo_path}")
42
+
43
+ logger.info(f"ChangeDetector initialized with repo_path: {self.repo_path}")
44
+
45
+ def detect_changes(
46
+ self,
47
+ languages: list[str] | None = None,
48
+ exclude_patterns: list[str] | None = None,
49
+ ) -> dict[str, ChangeType]:
50
+ """Detect all changes since last update.
51
+
52
+ Args:
53
+ languages: Optional list of languages to include
54
+ exclude_patterns: Optional patterns to exclude
55
+
56
+ Returns:
57
+ Dictionary mapping file paths to change types
58
+ """
59
+ changes = {}
60
+ current_files = set()
61
+
62
+ # Get supported file extensions
63
+ from shotgun.codebase.core.language_config import get_language_config
64
+
65
+ supported_extensions = set()
66
+ if languages:
67
+ # Map language names to their primary extensions
68
+ lang_to_ext = {
69
+ "python": ".py",
70
+ "javascript": ".js",
71
+ "typescript": ".ts",
72
+ "java": ".java",
73
+ "cpp": ".cpp",
74
+ "c": ".c",
75
+ "csharp": ".cs",
76
+ "go": ".go",
77
+ "rust": ".rs",
78
+ "ruby": ".rb",
79
+ "php": ".php",
80
+ }
81
+
82
+ for lang in languages:
83
+ # Try to get config using the language's primary extension
84
+ primary_ext = lang_to_ext.get(lang.lower())
85
+ if primary_ext:
86
+ config = get_language_config(primary_ext)
87
+ if config:
88
+ supported_extensions.update(config.file_extensions)
89
+ else:
90
+ # Get all supported extensions
91
+ for ext in [
92
+ ".py",
93
+ ".js",
94
+ ".ts",
95
+ ".tsx",
96
+ ".jsx",
97
+ ".java",
98
+ ".cpp",
99
+ ".c",
100
+ ".hpp",
101
+ ".h",
102
+ ".cs",
103
+ ".go",
104
+ ".rs",
105
+ ".rb",
106
+ ".php",
107
+ ]:
108
+ if get_language_config(ext):
109
+ supported_extensions.add(ext)
110
+
111
+ # Walk through all source files
112
+ logger.debug(
113
+ f"Walking source files in {self.repo_path} with extensions {supported_extensions}"
114
+ )
115
+ for filepath in self._walk_source_files(supported_extensions, exclude_patterns):
116
+ # Normalize the relative path to use forward slashes consistently
117
+ relative_path = str(filepath.relative_to(self.repo_path)).replace(
118
+ os.sep, "/"
119
+ )
120
+ current_files.add(relative_path)
121
+
122
+ file_info = self._get_file_info(relative_path)
123
+ if not file_info:
124
+ # New file
125
+ changes[relative_path] = ChangeType.ADDED
126
+ logger.debug(f"Detected new file: {relative_path}")
127
+ else:
128
+ # Check if modified
129
+ mtime = int(filepath.stat().st_mtime)
130
+ if mtime > file_info["mtime"]:
131
+ # Check hash to confirm actual change
132
+ current_hash = self._calculate_file_hash(filepath)
133
+ if current_hash != file_info.get("hash", ""):
134
+ changes[relative_path] = ChangeType.MODIFIED
135
+ logger.debug(f"Detected modified file: {relative_path}")
136
+
137
+ # Check for deleted files
138
+ tracked_files = self._get_tracked_files()
139
+ logger.debug(
140
+ f"Found {len(tracked_files)} tracked files, {len(current_files)} current files"
141
+ )
142
+
143
+ # Log sample of files for debugging
144
+ if tracked_files and current_files:
145
+ logger.debug(f"Sample tracked files: {list(tracked_files)[:5]}")
146
+ logger.debug(f"Sample current files: {list(current_files)[:5]}")
147
+
148
+ for tracked_file in tracked_files:
149
+ if tracked_file not in current_files:
150
+ changes[tracked_file] = ChangeType.DELETED
151
+ logger.debug(f"Detected deleted file: {tracked_file}")
152
+
153
+ # Warn if detecting large number of deletions
154
+ deleted_count = sum(1 for c in changes.values() if c == ChangeType.DELETED)
155
+ if deleted_count > 100:
156
+ logger.warning(
157
+ f"Detected {deleted_count} file deletions - this may indicate a path resolution issue"
158
+ )
159
+
160
+ logger.info(
161
+ f"Detected {len(changes)} file changes: "
162
+ f"{sum(1 for c in changes.values() if c == ChangeType.ADDED)} added, "
163
+ f"{sum(1 for c in changes.values() if c == ChangeType.MODIFIED)} modified, "
164
+ f"{sum(1 for c in changes.values() if c == ChangeType.DELETED)} deleted"
165
+ )
166
+
167
+ return changes
168
+
169
+ def _get_file_info(self, filepath: str) -> dict[str, Any] | None:
170
+ """Get FileMetadata info for a file.
171
+
172
+ Args:
173
+ filepath: Relative file path
174
+
175
+ Returns:
176
+ Dictionary with file metadata or None if not found
177
+ """
178
+ try:
179
+ result = self.conn.execute(
180
+ "MATCH (f:FileMetadata {filepath: $path}) RETURN f.mtime, f.hash",
181
+ {"path": filepath},
182
+ )
183
+
184
+ # Handle the QueryResult properly - cast to proper type
185
+ if hasattr(result, "has_next"):
186
+ query_result = cast(Any, result)
187
+ if query_result.has_next():
188
+ row = query_result.get_next()
189
+ if isinstance(row, list | tuple) and len(row) >= 2:
190
+ return {"mtime": row[0], "hash": row[1]}
191
+ except Exception as e:
192
+ logger.error(f"Failed to get file info for {filepath}: {e}")
193
+
194
+ return None
195
+
196
+ def _get_tracked_files(self) -> list[str]:
197
+ """Get all tracked file paths from database.
198
+
199
+ Returns:
200
+ List of relative file paths
201
+ """
202
+ files = []
203
+ try:
204
+ result = self.conn.execute("MATCH (f:FileMetadata) RETURN f.filepath")
205
+ # Handle the QueryResult properly - cast to proper type
206
+ if hasattr(result, "has_next"):
207
+ query_result = cast(Any, result)
208
+ while query_result.has_next():
209
+ # Normalize path separators to forward slashes
210
+ row = query_result.get_next()
211
+ if isinstance(row, list | tuple) and len(row) > 0:
212
+ filepath = row[0]
213
+ if filepath:
214
+ normalized_path = filepath.replace(os.sep, "/")
215
+ files.append(normalized_path)
216
+ except Exception as e:
217
+ logger.error(f"Failed to get tracked files: {e}")
218
+ return files
219
+
220
+ def _walk_source_files(
221
+ self, supported_extensions: set[str], exclude_patterns: list[str] | None = None
222
+ ) -> list[Path]:
223
+ """Walk repository and find all source files.
224
+
225
+ Args:
226
+ supported_extensions: Set of file extensions to include
227
+ exclude_patterns: Optional patterns to exclude
228
+
229
+ Returns:
230
+ List of absolute file paths
231
+ """
232
+ source_files = []
233
+ ignore_dirs = {
234
+ ".git",
235
+ "__pycache__",
236
+ "node_modules",
237
+ ".venv",
238
+ "venv",
239
+ ".env",
240
+ "dist",
241
+ "build",
242
+ ".pytest_cache",
243
+ ".mypy_cache",
244
+ ".tox",
245
+ }
246
+
247
+ logger.debug(f"Walking files from: {self.repo_path}")
248
+ logger.debug(f"Current working directory: {Path.cwd()}")
249
+
250
+ # Add custom exclude patterns
251
+ if exclude_patterns:
252
+ for pattern in exclude_patterns:
253
+ if pattern.startswith("*/"):
254
+ ignore_dirs.add(pattern[2:])
255
+
256
+ for root, dirs, files in os.walk(self.repo_path):
257
+ root_path = Path(root)
258
+
259
+ # Filter out ignored directories
260
+ dirs[:] = [d for d in dirs if d not in ignore_dirs]
261
+
262
+ # Skip if any parent directory should be ignored
263
+ if any(part in ignore_dirs for part in root_path.parts):
264
+ continue
265
+
266
+ for file in files:
267
+ filepath = root_path / file
268
+
269
+ # Check if it's a supported source file
270
+ if filepath.suffix in supported_extensions:
271
+ # Check exclude patterns
272
+ if exclude_patterns:
273
+ relative_path = str(filepath.relative_to(self.repo_path))
274
+ if any(
275
+ self._matches_pattern(relative_path, pattern)
276
+ for pattern in exclude_patterns
277
+ ):
278
+ continue
279
+
280
+ source_files.append(filepath)
281
+
282
+ return source_files
283
+
284
+ def _matches_pattern(self, filepath: str, pattern: str) -> bool:
285
+ """Check if filepath matches an exclude pattern.
286
+
287
+ Args:
288
+ filepath: File path to check
289
+ pattern: Pattern to match against
290
+
291
+ Returns:
292
+ True if matches
293
+ """
294
+ # Simple pattern matching
295
+ if "*" in pattern:
296
+ # Convert simple glob to regex-like matching
297
+ import fnmatch
298
+
299
+ return fnmatch.fnmatch(filepath, pattern)
300
+ else:
301
+ # Direct substring match
302
+ return pattern in filepath
303
+
304
+ def _calculate_file_hash(self, filepath: Path) -> str:
305
+ """Calculate hash of file contents.
306
+
307
+ Args:
308
+ filepath: Path to file
309
+
310
+ Returns:
311
+ SHA256 hash of file contents
312
+ """
313
+ try:
314
+ with open(filepath, "rb") as f:
315
+ return hashlib.sha256(f.read()).hexdigest()
316
+ except Exception as e:
317
+ logger.error(f"Failed to calculate hash for {filepath}: {e}")
318
+ return ""
319
+
320
+ def get_file_nodes(self, filepath: str) -> set[str]:
321
+ """Get all nodes tracked by a FileMetadata.
322
+
323
+ Args:
324
+ filepath: Relative file path
325
+
326
+ Returns:
327
+ Set of qualified names of nodes in the file
328
+ """
329
+ nodes = set()
330
+
331
+ # Query each TRACKS relationship type
332
+ for node_type, rel_type in [
333
+ ("Module", "TRACKS_Module"),
334
+ ("Class", "TRACKS_Class"),
335
+ ("Function", "TRACKS_Function"),
336
+ ("Method", "TRACKS_Method"),
337
+ ]:
338
+ try:
339
+ result = self.conn.execute(
340
+ f"""
341
+ MATCH (f:FileMetadata {{filepath: $path}})-[:{rel_type}]->(n:{node_type})
342
+ RETURN n.qualified_name
343
+ """,
344
+ {"path": filepath},
345
+ )
346
+
347
+ # Handle the QueryResult properly - cast to proper type
348
+ if hasattr(result, "has_next"):
349
+ query_result = cast(Any, result)
350
+ while query_result.has_next():
351
+ row = query_result.get_next()
352
+ if isinstance(row, list | tuple) and len(row) > 0:
353
+ nodes.add(row[0])
354
+ except Exception as e:
355
+ # Ignore if relationship doesn't exist - this is expected when tables aren't created yet
356
+ logger.debug(f"No {rel_type} relationship found for {filepath}: {e}")
357
+
358
+ return nodes