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
@@ -0,0 +1,60 @@
1
+ """Web search tools for Pydantic AI agents.
2
+
3
+ Provides web search capabilities for multiple LLM providers:
4
+ - OpenAI: Uses Responses API with web_search tool
5
+ - Anthropic: Uses Messages API with web_search_20250305 tool
6
+ - Gemini: Uses grounding with Google Search
7
+ """
8
+
9
+ from collections.abc import Callable
10
+
11
+ from shotgun.agents.config.models import ProviderType
12
+ from shotgun.logging_config import get_logger
13
+
14
+ from .anthropic import anthropic_web_search_tool
15
+ from .gemini import gemini_web_search_tool
16
+ from .openai import openai_web_search_tool
17
+ from .utils import is_provider_available
18
+
19
+ logger = get_logger(__name__)
20
+
21
+ # Type alias for web search tools
22
+ WebSearchTool = Callable[[str], str]
23
+
24
+
25
+ def get_available_web_search_tools() -> list[WebSearchTool]:
26
+ """Get list of available web search tools based on configured API keys.
27
+
28
+ Returns:
29
+ List of web search tool functions that have API keys configured
30
+ """
31
+ tools: list[WebSearchTool] = []
32
+
33
+ if is_provider_available(ProviderType.OPENAI):
34
+ logger.debug("✅ OpenAI web search tool available")
35
+ tools.append(openai_web_search_tool)
36
+
37
+ if is_provider_available(ProviderType.ANTHROPIC):
38
+ logger.debug("✅ Anthropic web search tool available")
39
+ tools.append(anthropic_web_search_tool)
40
+
41
+ if is_provider_available(ProviderType.GOOGLE):
42
+ logger.debug("✅ Gemini web search tool available")
43
+ tools.append(gemini_web_search_tool)
44
+
45
+ if not tools:
46
+ logger.warning("⚠️ No web search tools available - no API keys configured")
47
+ else:
48
+ logger.info("🔍 %d web search tool(s) available", len(tools))
49
+
50
+ return tools
51
+
52
+
53
+ __all__ = [
54
+ "openai_web_search_tool",
55
+ "anthropic_web_search_tool",
56
+ "gemini_web_search_tool",
57
+ "get_available_web_search_tools",
58
+ "is_provider_available",
59
+ "WebSearchTool",
60
+ ]
@@ -0,0 +1,144 @@
1
+ """Anthropic web search tool implementation."""
2
+
3
+ import anthropic
4
+ from opentelemetry import trace
5
+
6
+ from shotgun.agents.config import get_provider_model
7
+ from shotgun.agents.config.models import ProviderType
8
+ from shotgun.logging_config import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def anthropic_web_search_tool(query: str) -> str:
14
+ """Perform a web search using Anthropic's Claude API with streaming.
15
+
16
+ This tool uses Anthropic's web search capabilities to find current information
17
+ about the given query. Results are streamed for faster response times.
18
+
19
+ Args:
20
+ query: The search query
21
+
22
+ Returns:
23
+ Search results as a formatted string
24
+ """
25
+ logger.debug("🔧 Invoking Anthropic web_search_tool with query: %s", query)
26
+
27
+ span = trace.get_current_span()
28
+ span.set_attribute("input.value", f"**Query:** {query}\n")
29
+
30
+ logger.debug("📡 Executing Anthropic web search with streaming prompt: %s", query)
31
+
32
+ # Get API key from centralized configuration
33
+ try:
34
+ model_config = get_provider_model(ProviderType.ANTHROPIC)
35
+ api_key = model_config.api_key
36
+ except ValueError as e:
37
+ error_msg = f"Anthropic API key not configured: {str(e)}"
38
+ logger.error("❌ %s", error_msg)
39
+ span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
40
+ return error_msg
41
+
42
+ client = anthropic.Anthropic(api_key=api_key)
43
+
44
+ # Use the Messages API with web search tool and streaming
45
+ try:
46
+ result_text = ""
47
+
48
+ with client.messages.stream(
49
+ model="claude-3-5-sonnet-latest",
50
+ max_tokens=8192, # Maximum for Claude 3.5 Sonnet
51
+ messages=[{"role": "user", "content": f"Search for: {query}"}],
52
+ tools=[
53
+ {
54
+ "type": "web_search_20250305",
55
+ "name": "web_search",
56
+ }
57
+ ],
58
+ tool_choice={"type": "tool", "name": "web_search"},
59
+ ) as stream:
60
+ logger.debug("🌊 Started streaming Anthropic web search response")
61
+
62
+ for event in stream:
63
+ if event.type == "content_block_delta":
64
+ if hasattr(event.delta, "text"):
65
+ result_text += event.delta.text
66
+ elif event.type == "message_start":
67
+ logger.debug("🚀 Streaming started")
68
+ elif event.type == "message_stop":
69
+ logger.debug("✅ Streaming completed")
70
+
71
+ if not result_text:
72
+ result_text = "No content returned from search"
73
+
74
+ logger.debug("📄 Anthropic web search result: %d characters", len(result_text))
75
+ logger.debug(
76
+ "🔍 Result preview: %s...",
77
+ result_text[:100] if result_text else "No result",
78
+ )
79
+
80
+ span.set_attribute("output.value", f"**Results:**\n {result_text}\n")
81
+
82
+ return result_text
83
+ except Exception as e:
84
+ error_msg = f"Error performing Anthropic web search: {str(e)}"
85
+ logger.error("❌ Anthropic web search failed: %s", str(e))
86
+ logger.debug("💥 Full error details: %s", error_msg)
87
+ span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
88
+ return error_msg
89
+
90
+
91
+ def main() -> None:
92
+ """Main function for testing the Anthropic web search tool."""
93
+ import os
94
+ import sys
95
+
96
+ from shotgun.logging_config import setup_logger
97
+
98
+ # Use project's logging configuration instead of basicConfig
99
+ setup_logger(__name__)
100
+
101
+ if len(sys.argv) < 2:
102
+ print(
103
+ "Usage: python -m shotgun.agents.tools.web_search.anthropic <search_query>"
104
+ )
105
+ print(
106
+ "Example: python -m shotgun.agents.tools.web_search.anthropic 'latest Python updates'"
107
+ )
108
+ sys.exit(1)
109
+
110
+ # Join all arguments as the search query
111
+ query = " ".join(sys.argv[1:])
112
+
113
+ print("🔍 Testing Anthropic Web Search with streaming")
114
+ print(f"📝 Query: {query}")
115
+ print("=" * 60)
116
+
117
+ # Check if API key is available
118
+ if not (
119
+ os.getenv("ANTHROPIC_API_KEY")
120
+ or (
121
+ callable(get_provider_model)
122
+ and get_provider_model(ProviderType.ANTHROPIC).api_key
123
+ )
124
+ ):
125
+ print("❌ Error: ANTHROPIC_API_KEY environment variable not set")
126
+ print(" Please set it with: export ANTHROPIC_API_KEY=your_key_here")
127
+ sys.exit(1)
128
+
129
+ try:
130
+ result = anthropic_web_search_tool(query)
131
+ print(f"✅ Search completed! Result length: {len(result)} characters")
132
+ print("=" * 60)
133
+ print("📄 RESULTS:")
134
+ print("=" * 60)
135
+ print(result)
136
+ except KeyboardInterrupt:
137
+ print("\n⏹️ Search interrupted by user")
138
+ except Exception as e:
139
+ print(f"❌ Error during search: {e}")
140
+ sys.exit(1)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
@@ -0,0 +1,85 @@
1
+ """Gemini web search tool implementation."""
2
+
3
+ import google.generativeai as genai
4
+ from opentelemetry import trace
5
+
6
+ from shotgun.agents.config import get_provider_model
7
+ from shotgun.agents.config.models import ProviderType
8
+ from shotgun.logging_config import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def gemini_web_search_tool(query: str) -> str:
14
+ """Perform a web search using Google's Gemini API with grounding.
15
+
16
+ This tool uses Gemini's Google Search grounding to find current information
17
+ about the given query.
18
+
19
+ Args:
20
+ query: The search query
21
+
22
+ Returns:
23
+ Search results as a formatted string
24
+ """
25
+ logger.debug("🔧 Invoking Gemini web_search_tool with query: %s", query)
26
+
27
+ span = trace.get_current_span()
28
+ span.set_attribute("input.value", f"**Query:** {query}\n")
29
+
30
+ logger.debug("📡 Executing Gemini web search with prompt: %s", query)
31
+
32
+ # Get API key from centralized configuration
33
+ try:
34
+ model_config = get_provider_model(ProviderType.GOOGLE)
35
+ api_key = model_config.api_key
36
+ except ValueError as e:
37
+ error_msg = f"Gemini API key not configured: {str(e)}"
38
+ logger.error("❌ %s", error_msg)
39
+ span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
40
+ return error_msg
41
+
42
+ genai.configure(api_key=api_key) # type: ignore[attr-defined]
43
+
44
+ # Create model without built-in tools to avoid conflict with Pydantic AI
45
+ # Using prompt-based search approach instead
46
+ model = genai.GenerativeModel("gemini-2.5-pro") # type: ignore[attr-defined]
47
+
48
+ # Create a search-optimized prompt that leverages Gemini's knowledge
49
+ search_prompt = f"""Please provide current and accurate information about the following query:
50
+
51
+ Query: {query}
52
+
53
+ Instructions:
54
+ - Provide comprehensive, factual information
55
+ - Include relevant details and context
56
+ - Focus on current and recent information
57
+ - Be specific and accurate in your response"""
58
+
59
+ # Generate response using the model's knowledge
60
+ try:
61
+ response = model.generate_content(
62
+ search_prompt,
63
+ generation_config=genai.GenerationConfig( # type: ignore[attr-defined]
64
+ temperature=0.3,
65
+ max_output_tokens=8192,
66
+ ),
67
+ )
68
+
69
+ result_text = response.text or "No content returned from search"
70
+
71
+ logger.debug("📄 Gemini web search result: %d characters", len(result_text))
72
+ logger.debug(
73
+ "🔍 Result preview: %s...",
74
+ result_text[:100] if result_text else "No result",
75
+ )
76
+
77
+ span.set_attribute("output.value", f"**Results:**\n {result_text}\n")
78
+
79
+ return result_text
80
+ except Exception as e:
81
+ error_msg = f"Error performing Gemini web search: {str(e)}"
82
+ logger.error("❌ Gemini web search failed: %s", str(e))
83
+ logger.debug("💥 Full error details: %s", error_msg)
84
+ span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
85
+ return error_msg
@@ -0,0 +1,98 @@
1
+ """OpenAI web search tool implementation."""
2
+
3
+ from openai import OpenAI
4
+ from opentelemetry import trace
5
+
6
+ from shotgun.agents.config import get_provider_model
7
+ from shotgun.agents.config.models import ProviderType
8
+ from shotgun.logging_config import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def openai_web_search_tool(query: str) -> str:
14
+ """Perform a web search and return results.
15
+
16
+ This tool uses OpenAI's web search capabilities to find current information
17
+ about the given query.
18
+
19
+ Args:
20
+ query: The search query
21
+
22
+ Returns:
23
+ Search results as a formatted string
24
+ """
25
+ logger.debug("🔧 Invoking OpenAI web_search_tool with query: %s", query)
26
+
27
+ span = trace.get_current_span()
28
+ span.set_attribute("input.value", f"**Query:** {query}\n")
29
+
30
+ try:
31
+ logger.debug("📡 Executing OpenAI web search with prompt: %s", query)
32
+
33
+ # Get API key from centralized configuration
34
+ try:
35
+ model_config = get_provider_model(ProviderType.OPENAI)
36
+ api_key = model_config.api_key
37
+ except ValueError as e:
38
+ error_msg = f"OpenAI API key not configured: {str(e)}"
39
+ logger.error("❌ %s", error_msg)
40
+ span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
41
+ return error_msg
42
+
43
+ prompt = f"""Please provide current and accurate information about the following query:
44
+
45
+ Query: {query}
46
+
47
+ Instructions:
48
+ - Provide comprehensive, factual information
49
+ - Include relevant details and context
50
+ - Focus on current and recent information
51
+ - Be specific and accurate in your response
52
+ - You can't ask the user for details, so assume the most relevant details for the query
53
+
54
+ ALWAYS PROVIDE THE SOURCES (urls) TO BACK UP THE INFORMATION YOU PROVIDE.
55
+ """
56
+
57
+ client = OpenAI(api_key=api_key)
58
+ response = client.responses.create( # type: ignore[call-overload]
59
+ model="gpt-5-mini",
60
+ input=[
61
+ {"role": "user", "content": [{"type": "input_text", "text": prompt}]}
62
+ ],
63
+ text={
64
+ "format": {"type": "text"},
65
+ "verbosity": "high",
66
+ }, # Increased from medium
67
+ reasoning={"effort": "medium", "summary": "auto"},
68
+ tools=[
69
+ {
70
+ "type": "web_search",
71
+ "user_location": {"type": "approximate"},
72
+ "search_context_size": "high", # Increased from low for more context
73
+ }
74
+ ],
75
+ store=False,
76
+ include=[
77
+ "reasoning.encrypted_content",
78
+ "web_search_call.action.sources", # pyright: ignore[reportArgumentType]
79
+ ],
80
+ )
81
+
82
+ result_text = response.output_text or "No content returned"
83
+
84
+ logger.debug("📄 Web search result: %d characters", len(result_text))
85
+ logger.debug(
86
+ "🔍 Result preview: %s...",
87
+ result_text[:100] if result_text else "No result",
88
+ )
89
+
90
+ span.set_attribute("output.value", f"**Results:**\n {result_text}\n")
91
+
92
+ return result_text
93
+ except Exception as e:
94
+ error_msg = f"Error performing web search: {str(e)}"
95
+ logger.error("❌ Web search failed: %s", str(e))
96
+ logger.debug("💥 Full error details: %s", error_msg)
97
+ span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
98
+ return error_msg
@@ -0,0 +1,20 @@
1
+ """Utility functions for web search tools."""
2
+
3
+ from shotgun.agents.config import get_provider_model
4
+ from shotgun.agents.config.models import ProviderType
5
+
6
+
7
+ def is_provider_available(provider: ProviderType) -> bool:
8
+ """Check if a provider has API key configured.
9
+
10
+ Args:
11
+ provider: The provider to check
12
+
13
+ Returns:
14
+ True if the provider has valid credentials configured (from config or env)
15
+ """
16
+ try:
17
+ get_provider_model(provider)
18
+ return True
19
+ except ValueError:
20
+ return False
@@ -0,0 +1,20 @@
1
+ """Build-time constants generated during packaging.
2
+
3
+ This file is auto-generated during the build process.
4
+ DO NOT EDIT MANUALLY.
5
+ """
6
+
7
+ # Sentry DSN embedded at build time (empty string if not provided)
8
+ SENTRY_DSN = 'https://2818a6d165c64eccc94cfd51ce05d6aa@o4506813296738304.ingest.us.sentry.io/4510045952409600'
9
+
10
+ # PostHog configuration embedded at build time (empty strings if not provided)
11
+ POSTHOG_API_KEY = ''
12
+ POSTHOG_PROJECT_ID = '191396'
13
+
14
+ # Logfire configuration embedded at build time (only for dev builds)
15
+ LOGFIRE_ENABLED = ''
16
+ LOGFIRE_TOKEN = ''
17
+
18
+ # Build metadata
19
+ BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
20
+ IS_DEV_BUILD = True
@@ -0,0 +1 @@
1
+ """Commands package for shotgun CLI."""
@@ -0,0 +1,5 @@
1
+ """Codebase CLI module."""
2
+
3
+ from .commands import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1,202 @@
1
+ """Codebase management CLI commands."""
2
+
3
+ import asyncio
4
+ import traceback
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from shotgun.codebase.models import CodebaseGraph, QueryType
11
+ from shotgun.logging_config import get_logger
12
+ from shotgun.sdk.codebase import CodebaseSDK
13
+ from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
14
+
15
+ from ..models import OutputFormat
16
+ from ..utils import output_result
17
+ from .models import ErrorResult
18
+
19
+ app = typer.Typer(
20
+ name="codebase",
21
+ help="Manage and query code knowledge graphs",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+ # Set up logger but it will be suppressed by default
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ @app.command(name="list")
30
+ def list_codebases(
31
+ format_type: Annotated[
32
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
33
+ ] = OutputFormat.TEXT,
34
+ ) -> None:
35
+ """List all indexed codebases."""
36
+ sdk = CodebaseSDK()
37
+
38
+ try:
39
+ result = asyncio.run(sdk.list_codebases())
40
+ output_result(result, format_type)
41
+ except Exception as e:
42
+ error_result = ErrorResult(
43
+ error_message=f"Error listing codebases: {e}",
44
+ details=f"Full traceback:\n{traceback.format_exc()}",
45
+ )
46
+ output_result(error_result, format_type)
47
+ raise typer.Exit(1) from e
48
+
49
+
50
+ @app.command()
51
+ def index(
52
+ path: Annotated[str, typer.Argument(help="Path to repository to index")],
53
+ name: Annotated[
54
+ str, typer.Option("--name", "-n", help="Human-readable name for the codebase")
55
+ ],
56
+ format_type: Annotated[
57
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
58
+ ] = OutputFormat.TEXT,
59
+ ) -> None:
60
+ """Index a new codebase."""
61
+ sdk = CodebaseSDK()
62
+
63
+ try:
64
+ repo_path = Path(path)
65
+ result = asyncio.run(sdk.index_codebase(repo_path, name))
66
+ output_result(result, format_type)
67
+ except InvalidPathError as e:
68
+ error_result = ErrorResult(error_message=str(e))
69
+ output_result(error_result, format_type)
70
+ raise typer.Exit(1) from e
71
+ except Exception as e:
72
+ error_result = ErrorResult(
73
+ error_message=f"Error indexing codebase: {e}",
74
+ details=f"Full traceback:\n{traceback.format_exc()}",
75
+ )
76
+ output_result(error_result, format_type)
77
+ raise typer.Exit(1) from e
78
+
79
+
80
+ @app.command()
81
+ def delete(
82
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to delete")],
83
+ format_type: Annotated[
84
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
85
+ ] = OutputFormat.TEXT,
86
+ ) -> None:
87
+ """Delete an indexed codebase."""
88
+ sdk = CodebaseSDK()
89
+
90
+ # CLI-specific confirmation callback
91
+ def cli_confirm(graph: CodebaseGraph) -> bool:
92
+ return typer.confirm(
93
+ f"Are you sure you want to delete codebase '{graph.name}' ({graph_id})?"
94
+ )
95
+
96
+ try:
97
+ result = asyncio.run(sdk.delete_codebase(graph_id, cli_confirm))
98
+ output_result(result, format_type)
99
+ if not result.deleted and not result.cancelled:
100
+ raise typer.Exit(1)
101
+ except CodebaseNotFoundError as e:
102
+ error_result = ErrorResult(error_message=str(e))
103
+ output_result(error_result, format_type)
104
+ raise typer.Exit(1) from e
105
+ except Exception as e:
106
+ error_result = ErrorResult(
107
+ error_message=f"Error deleting codebase: {e}",
108
+ details=f"Full traceback:\n{traceback.format_exc()}",
109
+ )
110
+ output_result(error_result, format_type)
111
+ raise typer.Exit(1) from e
112
+
113
+
114
+ @app.command()
115
+ def info(
116
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to show info for")],
117
+ format_type: Annotated[
118
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
119
+ ] = OutputFormat.TEXT,
120
+ ) -> None:
121
+ """Show detailed information about a codebase."""
122
+ sdk = CodebaseSDK()
123
+
124
+ try:
125
+ result = asyncio.run(sdk.get_info(graph_id))
126
+ output_result(result, format_type)
127
+ except CodebaseNotFoundError as e:
128
+ error_result = ErrorResult(error_message=str(e))
129
+ output_result(error_result, format_type)
130
+ raise typer.Exit(1) from e
131
+ except Exception as e:
132
+ error_result = ErrorResult(
133
+ error_message=f"Error getting codebase info: {e}",
134
+ details=f"Full traceback:\n{traceback.format_exc()}",
135
+ )
136
+ output_result(error_result, format_type)
137
+ raise typer.Exit(1) from e
138
+
139
+
140
+ @app.command()
141
+ def query(
142
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to query")],
143
+ query_text: Annotated[
144
+ str, typer.Argument(help="Query text (natural language or Cypher)")
145
+ ],
146
+ cypher: Annotated[
147
+ bool,
148
+ typer.Option(
149
+ "--cypher", help="Treat query as Cypher instead of natural language"
150
+ ),
151
+ ] = False,
152
+ format_type: Annotated[
153
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
154
+ ] = OutputFormat.TEXT,
155
+ ) -> None:
156
+ """Query a codebase using natural language or Cypher."""
157
+
158
+ try:
159
+ sdk = CodebaseSDK()
160
+ query_type = QueryType.CYPHER if cypher else QueryType.NATURAL_LANGUAGE
161
+ result = asyncio.run(sdk.query_codebase(graph_id, query_text, query_type))
162
+ output_result(result, format_type)
163
+ except CodebaseNotFoundError as e:
164
+ error_result = ErrorResult(error_message=str(e))
165
+ output_result(error_result, format_type)
166
+ raise typer.Exit(1) from e
167
+
168
+ except Exception as e:
169
+ error_result = ErrorResult(
170
+ error_message=f"Error executing query: {e}",
171
+ details=f"Full traceback:\n{traceback.format_exc()}",
172
+ )
173
+ output_result(error_result, format_type)
174
+ raise typer.Exit(1) from e
175
+
176
+
177
+ @app.command()
178
+ def reindex(
179
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to reindex")],
180
+ format_type: Annotated[
181
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
182
+ ] = OutputFormat.TEXT,
183
+ ) -> None:
184
+ """Reindex an existing codebase."""
185
+
186
+ try:
187
+ sdk = CodebaseSDK()
188
+ result = asyncio.run(sdk.reindex_codebase(graph_id))
189
+ # Stats are always shown now that verbose is controlled by env var
190
+ output_result(result, format_type)
191
+ except CodebaseNotFoundError as e:
192
+ error_result = ErrorResult(error_message=str(e))
193
+ output_result(error_result, format_type)
194
+ raise typer.Exit(1) from e
195
+
196
+ except Exception as e:
197
+ error_result = ErrorResult(
198
+ error_message=f"Error reindexing codebase: {e}",
199
+ details=f"Full traceback:\n{traceback.format_exc()}",
200
+ )
201
+ output_result(error_result, format_type)
202
+ raise typer.Exit(1) from e
@@ -0,0 +1,21 @@
1
+ """Re-export SDK models for backward compatibility."""
2
+
3
+ from shotgun.sdk.models import (
4
+ DeleteResult,
5
+ ErrorResult,
6
+ IndexResult,
7
+ InfoResult,
8
+ ListResult,
9
+ QueryCommandResult,
10
+ ReindexResult,
11
+ )
12
+
13
+ __all__ = [
14
+ "ListResult",
15
+ "IndexResult",
16
+ "DeleteResult",
17
+ "InfoResult",
18
+ "QueryCommandResult",
19
+ "ReindexResult",
20
+ "ErrorResult",
21
+ ]