shotgun-sh 0.1.0.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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (94) hide show
  1. shotgun/__init__.py +3 -0
  2. shotgun/agents/__init__.py +1 -0
  3. shotgun/agents/agent_manager.py +196 -0
  4. shotgun/agents/common.py +295 -0
  5. shotgun/agents/config/__init__.py +13 -0
  6. shotgun/agents/config/manager.py +215 -0
  7. shotgun/agents/config/models.py +120 -0
  8. shotgun/agents/config/provider.py +91 -0
  9. shotgun/agents/history/__init__.py +5 -0
  10. shotgun/agents/history/history_processors.py +213 -0
  11. shotgun/agents/models.py +94 -0
  12. shotgun/agents/plan.py +119 -0
  13. shotgun/agents/research.py +131 -0
  14. shotgun/agents/tasks.py +122 -0
  15. shotgun/agents/tools/__init__.py +26 -0
  16. shotgun/agents/tools/codebase/__init__.py +28 -0
  17. shotgun/agents/tools/codebase/codebase_shell.py +256 -0
  18. shotgun/agents/tools/codebase/directory_lister.py +141 -0
  19. shotgun/agents/tools/codebase/file_read.py +144 -0
  20. shotgun/agents/tools/codebase/models.py +252 -0
  21. shotgun/agents/tools/codebase/query_graph.py +67 -0
  22. shotgun/agents/tools/codebase/retrieve_code.py +81 -0
  23. shotgun/agents/tools/file_management.py +130 -0
  24. shotgun/agents/tools/user_interaction.py +36 -0
  25. shotgun/agents/tools/web_search.py +69 -0
  26. shotgun/cli/__init__.py +1 -0
  27. shotgun/cli/codebase/__init__.py +5 -0
  28. shotgun/cli/codebase/commands.py +202 -0
  29. shotgun/cli/codebase/models.py +21 -0
  30. shotgun/cli/config.py +261 -0
  31. shotgun/cli/models.py +10 -0
  32. shotgun/cli/plan.py +65 -0
  33. shotgun/cli/research.py +78 -0
  34. shotgun/cli/tasks.py +71 -0
  35. shotgun/cli/utils.py +25 -0
  36. shotgun/codebase/__init__.py +12 -0
  37. shotgun/codebase/core/__init__.py +46 -0
  38. shotgun/codebase/core/change_detector.py +358 -0
  39. shotgun/codebase/core/code_retrieval.py +243 -0
  40. shotgun/codebase/core/ingestor.py +1497 -0
  41. shotgun/codebase/core/language_config.py +297 -0
  42. shotgun/codebase/core/manager.py +1554 -0
  43. shotgun/codebase/core/nl_query.py +327 -0
  44. shotgun/codebase/core/parser_loader.py +152 -0
  45. shotgun/codebase/models.py +107 -0
  46. shotgun/codebase/service.py +148 -0
  47. shotgun/logging_config.py +172 -0
  48. shotgun/main.py +73 -0
  49. shotgun/prompts/__init__.py +5 -0
  50. shotgun/prompts/agents/__init__.py +1 -0
  51. shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
  52. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
  53. shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
  54. shotgun/prompts/agents/plan.j2 +57 -0
  55. shotgun/prompts/agents/research.j2 +38 -0
  56. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
  57. shotgun/prompts/agents/state/system_state.j2 +1 -0
  58. shotgun/prompts/agents/tasks.j2 +67 -0
  59. shotgun/prompts/codebase/__init__.py +1 -0
  60. shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
  61. shotgun/prompts/codebase/cypher_system.j2 +28 -0
  62. shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
  63. shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
  64. shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
  65. shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
  66. shotgun/prompts/history/__init__.py +1 -0
  67. shotgun/prompts/history/summarization.j2 +46 -0
  68. shotgun/prompts/loader.py +140 -0
  69. shotgun/prompts/user/research.j2 +5 -0
  70. shotgun/py.typed +0 -0
  71. shotgun/sdk/__init__.py +13 -0
  72. shotgun/sdk/codebase.py +195 -0
  73. shotgun/sdk/exceptions.py +17 -0
  74. shotgun/sdk/models.py +189 -0
  75. shotgun/sdk/services.py +23 -0
  76. shotgun/telemetry.py +68 -0
  77. shotgun/tui/__init__.py +0 -0
  78. shotgun/tui/app.py +49 -0
  79. shotgun/tui/components/prompt_input.py +69 -0
  80. shotgun/tui/components/spinner.py +86 -0
  81. shotgun/tui/components/splash.py +25 -0
  82. shotgun/tui/components/vertical_tail.py +28 -0
  83. shotgun/tui/screens/chat.py +415 -0
  84. shotgun/tui/screens/chat.tcss +28 -0
  85. shotgun/tui/screens/provider_config.py +221 -0
  86. shotgun/tui/screens/splash.py +31 -0
  87. shotgun/tui/styles.tcss +10 -0
  88. shotgun/utils/__init__.py +5 -0
  89. shotgun/utils/file_system_utils.py +31 -0
  90. shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
  91. shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
  92. shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
  93. shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
  94. shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
shotgun/cli/tasks.py ADDED
@@ -0,0 +1,71 @@
1
+ """Tasks command for shotgun CLI."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from shotgun.agents.config import ProviderType
9
+ from shotgun.agents.models import AgentRuntimeOptions
10
+ from shotgun.agents.tasks import (
11
+ create_tasks_agent,
12
+ get_tasks_history,
13
+ run_tasks_agent,
14
+ )
15
+ from shotgun.logging_config import get_logger
16
+
17
+ app = typer.Typer(name="tasks", help="Generate task lists with agentic approach")
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ @app.callback(invoke_without_command=True)
22
+ def tasks(
23
+ instruction: Annotated[
24
+ str, typer.Argument(help="Task creation instruction or project description")
25
+ ],
26
+ non_interactive: Annotated[
27
+ bool,
28
+ typer.Option(
29
+ "--non-interactive", "-n", help="Disable user interaction (for CI/CD)"
30
+ ),
31
+ ] = False,
32
+ provider: Annotated[
33
+ ProviderType | None,
34
+ typer.Option("--provider", "-p", help="AI provider to use (overrides default)"),
35
+ ] = None,
36
+ ) -> None:
37
+ """Generate actionable task lists based on existing research and plans.
38
+
39
+ This command creates detailed task breakdowns using AI agents that analyze
40
+ your research and plans to generate prioritized, actionable tasks with
41
+ acceptance criteria and effort estimates.
42
+ """
43
+
44
+ logger.info("📋 Task Creation Instruction: %s", instruction)
45
+
46
+ try:
47
+ # Create agent dependencies
48
+ agent_runtime_options = AgentRuntimeOptions(
49
+ interactive_mode=not non_interactive
50
+ )
51
+
52
+ # Create the tasks agent with deps and provider
53
+ agent, deps = create_tasks_agent(agent_runtime_options, provider)
54
+
55
+ # Start task creation process
56
+ logger.info("🎯 Starting task creation...")
57
+ result = asyncio.run(run_tasks_agent(agent, instruction, deps))
58
+
59
+ # Display results
60
+ logger.info("✅ Task Creation Complete!")
61
+ logger.info("📋 Results:")
62
+ logger.info("%s", result.output)
63
+ logger.info("📄 Tasks saved to: .shotgun/tasks.md")
64
+ logger.debug("📚 Current tasks:")
65
+ logger.debug("%s", get_tasks_history())
66
+
67
+ except Exception as e:
68
+ logger.error("❌ Error during task creation: %s", str(e))
69
+ import traceback
70
+
71
+ logger.debug("Full traceback:\n%s", traceback.format_exc())
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