shotgun-sh 0.1.1__tar.gz → 0.1.2__tar.gz

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 (132) hide show
  1. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/PKG-INFO +1 -1
  2. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/pyproject.toml +1 -1
  3. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/agent_manager.py +11 -0
  4. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/nl_query.py +39 -180
  5. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/service.py +0 -17
  6. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/main.py +14 -4
  7. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/cypher_system.j2 +1 -15
  8. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/app.py +13 -3
  9. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat.py +11 -5
  10. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/history.py +4 -5
  11. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/utils/update_checker.py +202 -0
  12. shotgun_sh-0.1.1/src/shotgun/codebase/core/cypher_models.py +0 -46
  13. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/.gitignore +0 -0
  14. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/LICENSE +0 -0
  15. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/README.md +0 -0
  16. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/hatch_build.py +0 -0
  17. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/__init__.py +0 -0
  18. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/__init__.py +0 -0
  19. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/common.py +0 -0
  20. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/__init__.py +0 -0
  21. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/constants.py +0 -0
  22. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/manager.py +0 -0
  23. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/models.py +0 -0
  24. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/provider.py +0 -0
  25. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/conversation_history.py +0 -0
  26. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/conversation_manager.py +0 -0
  27. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/export.py +0 -0
  28. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/__init__.py +0 -0
  29. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/compaction.py +0 -0
  30. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/constants.py +0 -0
  31. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/context_extraction.py +0 -0
  32. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/history_building.py +0 -0
  33. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/history_processors.py +0 -0
  34. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/message_utils.py +0 -0
  35. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/token_counting.py +0 -0
  36. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/token_estimation.py +0 -0
  37. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/messages.py +0 -0
  38. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/models.py +0 -0
  39. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/plan.py +0 -0
  40. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/research.py +0 -0
  41. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/specify.py +0 -0
  42. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tasks.py +0 -0
  43. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/__init__.py +0 -0
  44. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  45. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  46. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  47. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  48. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/models.py +0 -0
  49. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  50. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  51. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/file_management.py +0 -0
  52. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/user_interaction.py +0 -0
  53. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
  54. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
  55. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
  56. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/openai.py +0 -0
  57. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  58. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/build_constants.py +0 -0
  59. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/__init__.py +0 -0
  60. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/codebase/__init__.py +0 -0
  61. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/codebase/commands.py +0 -0
  62. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/codebase/models.py +0 -0
  63. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/config.py +0 -0
  64. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/export.py +0 -0
  65. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/models.py +0 -0
  66. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/plan.py +0 -0
  67. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/research.py +0 -0
  68. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/specify.py +0 -0
  69. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/tasks.py +0 -0
  70. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/update.py +0 -0
  71. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/cli/utils.py +0 -0
  72. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/__init__.py +0 -0
  73. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/__init__.py +0 -0
  74. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/change_detector.py +0 -0
  75. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  76. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/ingestor.py +0 -0
  77. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/language_config.py +0 -0
  78. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/manager.py +0 -0
  79. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/parser_loader.py +0 -0
  80. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/codebase/models.py +0 -0
  81. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/logging_config.py +0 -0
  82. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/posthog_telemetry.py +0 -0
  83. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/__init__.py +0 -0
  84. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/__init__.py +0 -0
  85. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/export.j2 +0 -0
  86. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  87. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  88. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  89. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  90. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/plan.j2 +0 -0
  91. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/research.j2 +0 -0
  92. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/specify.j2 +0 -0
  93. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  94. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  95. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  96. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/__init__.py +0 -0
  97. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  98. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  99. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  100. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  101. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  102. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/history/__init__.py +0 -0
  103. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  104. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/history/summarization.j2 +0 -0
  105. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/prompts/loader.py +0 -0
  106. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/py.typed +0 -0
  107. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/sdk/__init__.py +0 -0
  108. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/sdk/codebase.py +0 -0
  109. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/sdk/exceptions.py +0 -0
  110. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/sdk/models.py +0 -0
  111. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/sdk/services.py +0 -0
  112. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/sentry_telemetry.py +0 -0
  113. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/telemetry.py +0 -0
  114. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/__init__.py +0 -0
  115. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/commands/__init__.py +0 -0
  116. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/prompt_input.py +0 -0
  117. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/spinner.py +0 -0
  118. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/splash.py +0 -0
  119. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/vertical_tail.py +0 -0
  120. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat.tcss +0 -0
  121. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  122. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
  123. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  124. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/directory_setup.py +0 -0
  125. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/provider_config.py +0 -0
  126. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/splash.py +0 -0
  127. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/styles.tcss +0 -0
  128. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/utils/__init__.py +0 -0
  129. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/tui/utils/mode_progress.py +0 -0
  130. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/utils/__init__.py +0 -0
  131. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/utils/env_utils.py +0 -0
  132. {shotgun_sh-0.1.1 → shotgun_sh-0.1.2}/src/shotgun/utils/file_system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -362,6 +362,17 @@ class AgentManager(Widget):
362
362
  **kwargs,
363
363
  )
364
364
  finally:
365
+ # If the stream ended unexpectedly without a final result, clear accumulated state.
366
+ # state = self._stream_state
367
+ # if state is not None:
368
+ # pending_response = state.current_response
369
+ # if pending_response is not None:
370
+ # already_recorded = (
371
+ # bool(state.messages) and state.messages[-1] is pending_response
372
+ # )
373
+ # if not already_recorded:
374
+ # self._post_partial_message(pending_response, True)
375
+ # state.messages.append(pending_response)
365
376
  self._stream_state = None
366
377
 
367
378
  self.ui_message_history = original_messages + cast(
@@ -4,13 +4,15 @@ import time
4
4
  from datetime import datetime
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from pydantic_ai import Agent
7
+ from pydantic_ai.messages import (
8
+ ModelRequest,
9
+ SystemPromptPart,
10
+ TextPart,
11
+ UserPromptPart,
12
+ )
8
13
 
9
14
  from shotgun.agents.config import get_provider_model
10
- from shotgun.codebase.core.cypher_models import (
11
- CypherGenerationNotPossibleError,
12
- CypherGenerationResponse,
13
- )
15
+ from shotgun.agents.config.models import shotgun_model_request
14
16
  from shotgun.logging_config import get_logger
15
17
  from shotgun.prompts import PromptLoader
16
18
 
@@ -23,52 +25,42 @@ logger = get_logger(__name__)
23
25
  prompt_loader = PromptLoader()
24
26
 
25
27
 
26
- async def llm_cypher_prompt(
27
- system_prompt: str, user_prompt: str
28
- ) -> CypherGenerationResponse:
29
- """Generate a Cypher query from a natural language prompt using structured output.
28
+ async def llm_cypher_prompt(system_prompt: str, user_prompt: str) -> str:
29
+ """Generate a Cypher query from a natural language prompt using the configured LLM provider.
30
30
 
31
31
  Args:
32
32
  system_prompt: The system prompt defining the behavior and context for the LLM
33
33
  user_prompt: The user's natural language query
34
34
  Returns:
35
- CypherGenerationResponse with cypher_query, can_generate flag, and reason if not
35
+ The generated Cypher query as a string
36
36
  """
37
37
  model_config = get_provider_model()
38
-
39
- # Create an agent with structured output for Cypher generation
40
- cypher_agent = Agent(
41
- model=model_config.model_instance,
42
- output_type=CypherGenerationResponse,
43
- retries=2,
38
+ # Use shotgun wrapper to maximize response quality for codebase queries
39
+ # Limit max_tokens to 2000 for Cypher queries (they're typically 50-200 tokens)
40
+ # This prevents Anthropic SDK from requiring streaming for longer token limits
41
+ query_cypher_response = await shotgun_model_request(
42
+ model_config=model_config,
43
+ messages=[
44
+ ModelRequest(
45
+ parts=[
46
+ SystemPromptPart(content=system_prompt),
47
+ UserPromptPart(content=user_prompt),
48
+ ]
49
+ ),
50
+ ],
51
+ max_tokens=2000, # Cypher queries are short, 2000 tokens is plenty
44
52
  )
45
53
 
46
- # Combine system and user prompts
47
- combined_prompt = f"{system_prompt}\n\nUser Query: {user_prompt}"
48
-
49
- try:
50
- # Run the agent to get structured response
51
- result = await cypher_agent.run(combined_prompt)
52
- response = result.output
53
-
54
- # Log the structured response for debugging
55
- logger.debug(
56
- "Cypher generation response - can_generate: %s, query: %s, reason: %s",
57
- response.can_generate_valid_cypher,
58
- response.cypher_query[:50] if response.cypher_query else None,
59
- response.reason_cannot_generate,
60
- )
61
-
62
- return response
54
+ if not query_cypher_response.parts or not query_cypher_response.parts[0]:
55
+ raise ValueError("Empty response from LLM")
63
56
 
64
- except Exception as e:
65
- logger.error("Failed to generate Cypher query with structured output: %s", e)
66
- # Return a failure response
67
- return CypherGenerationResponse(
68
- cypher_query=None,
69
- can_generate_valid_cypher=False,
70
- reason_cannot_generate=f"LLM error: {str(e)}",
71
- )
57
+ message_part = query_cypher_response.parts[0]
58
+ if not isinstance(message_part, TextPart):
59
+ raise ValueError("Unexpected response part type from LLM")
60
+ cypher_query = str(message_part.content)
61
+ if not cypher_query:
62
+ raise ValueError("Empty content in LLM response")
63
+ return cypher_query
72
64
 
73
65
 
74
66
  async def generate_cypher(natural_language_query: str) -> str:
@@ -79,10 +71,6 @@ async def generate_cypher(natural_language_query: str) -> str:
79
71
 
80
72
  Returns:
81
73
  Generated Cypher query
82
-
83
- Raises:
84
- CypherGenerationNotPossibleError: If the query cannot be converted to Cypher
85
- RuntimeError: If there's an error during generation
86
74
  """
87
75
  # Get current time for context
88
76
  current_timestamp = int(time.time())
@@ -100,30 +88,8 @@ async def generate_cypher(natural_language_query: str) -> str:
100
88
  )
101
89
 
102
90
  try:
103
- response = await llm_cypher_prompt(system_prompt, enhanced_query)
104
-
105
- # Check if the LLM could generate a valid Cypher query
106
- if not response.can_generate_valid_cypher:
107
- logger.info(
108
- "Cannot generate Cypher for query '%s': %s",
109
- natural_language_query,
110
- response.reason_cannot_generate,
111
- )
112
- raise CypherGenerationNotPossibleError(
113
- response.reason_cannot_generate or "Query cannot be converted to Cypher"
114
- )
115
-
116
- if not response.cypher_query:
117
- raise ValueError("LLM indicated success but provided no query")
118
-
119
- cleaned_query = clean_cypher_response(response.cypher_query)
120
-
121
- # Validate Cypher keywords
122
- is_valid, validation_error = validate_cypher_keywords(cleaned_query)
123
- if not is_valid:
124
- logger.warning(f"Generated query has invalid syntax: {validation_error}")
125
- logger.warning(f"Problematic query: {cleaned_query}")
126
- raise ValueError(f"Generated query validation failed: {validation_error}")
91
+ cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
92
+ cleaned_query = clean_cypher_response(cypher_query)
127
93
 
128
94
  # Validate UNION ALL queries
129
95
  is_valid, validation_error = validate_union_query(cleaned_query)
@@ -134,8 +100,6 @@ async def generate_cypher(natural_language_query: str) -> str:
134
100
 
135
101
  return cleaned_query
136
102
 
137
- except CypherGenerationNotPossibleError:
138
- raise # Re-raise as-is
139
103
  except Exception as e:
140
104
  raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
141
105
 
@@ -206,31 +170,8 @@ MATCH (f:Function) RETURN f.name, f.qualified_name // WRONG: missing third colu
206
170
  base_system_prompt=prompt_loader.render("codebase/cypher_system.j2"),
207
171
  )
208
172
 
209
- response = await llm_cypher_prompt(enhanced_system_prompt, enhanced_query)
210
-
211
- # Check if the LLM could generate a valid Cypher query
212
- if not response.can_generate_valid_cypher:
213
- logger.info(
214
- "Cannot generate Cypher for retry query '%s': %s",
215
- natural_language_query,
216
- response.reason_cannot_generate,
217
- )
218
- raise CypherGenerationNotPossibleError(
219
- response.reason_cannot_generate
220
- or "Query cannot be converted to Cypher even with error context"
221
- )
222
-
223
- if not response.cypher_query:
224
- raise ValueError("LLM indicated success but provided no query on retry")
225
-
226
- cleaned_query = clean_cypher_response(response.cypher_query)
227
-
228
- # Validate Cypher keywords
229
- is_valid, validation_error = validate_cypher_keywords(cleaned_query)
230
- if not is_valid:
231
- logger.warning(f"Generated query has invalid syntax: {validation_error}")
232
- logger.warning(f"Problematic query: {cleaned_query}")
233
- raise ValueError(f"Generated query validation failed: {validation_error}")
173
+ cypher_query = await llm_cypher_prompt(enhanced_system_prompt, enhanced_query)
174
+ cleaned_query = clean_cypher_response(cypher_query)
234
175
 
235
176
  # Validate UNION ALL queries
236
177
  is_valid, validation_error = validate_union_query(cleaned_query)
@@ -241,8 +182,6 @@ MATCH (f:Function) RETURN f.name, f.qualified_name // WRONG: missing third colu
241
182
 
242
183
  return cleaned_query
243
184
 
244
- except CypherGenerationNotPossibleError:
245
- raise # Re-raise as-is
246
185
  except Exception as e:
247
186
  raise RuntimeError(
248
187
  f"Failed to generate Cypher query with error context: {e}"
@@ -263,10 +202,6 @@ async def generate_cypher_openai_async(
263
202
 
264
203
  Returns:
265
204
  Generated Cypher query
266
-
267
- Raises:
268
- CypherGenerationNotPossibleError: If the query cannot be converted to Cypher
269
- RuntimeError: If there's an error during generation
270
205
  """
271
206
  # Get current time for context
272
207
  current_timestamp = int(time.time())
@@ -284,26 +219,9 @@ async def generate_cypher_openai_async(
284
219
  )
285
220
 
286
221
  try:
287
- response = await llm_cypher_prompt(system_prompt, enhanced_query)
288
-
289
- # Check if the LLM could generate a valid Cypher query
290
- if not response.can_generate_valid_cypher:
291
- logger.info(
292
- "Cannot generate Cypher for query '%s': %s",
293
- natural_language_query,
294
- response.reason_cannot_generate,
295
- )
296
- raise CypherGenerationNotPossibleError(
297
- response.reason_cannot_generate or "Query cannot be converted to Cypher"
298
- )
222
+ cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
223
+ return clean_cypher_response(cypher_query)
299
224
 
300
- if not response.cypher_query:
301
- raise ValueError("LLM indicated success but provided no query")
302
-
303
- return clean_cypher_response(response.cypher_query)
304
-
305
- except CypherGenerationNotPossibleError:
306
- raise # Re-raise as-is
307
225
  except Exception as e:
308
226
  logger.error(f"OpenAI API error: {e}")
309
227
  raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
@@ -370,65 +288,6 @@ def validate_union_query(cypher_query: str) -> tuple[bool, str]:
370
288
  return True, ""
371
289
 
372
290
 
373
- def validate_cypher_keywords(query: str) -> tuple[bool, str]:
374
- """Validate that a query starts with valid Kuzu Cypher keywords.
375
-
376
- Args:
377
- query: The Cypher query to validate
378
-
379
- Returns:
380
- Tuple of (is_valid, error_message)
381
- """
382
- # Valid Kuzu Cypher starting keywords based on parser expectations
383
- valid_cypher_keywords = {
384
- "ALTER",
385
- "ATTACH",
386
- "BEGIN",
387
- "CALL",
388
- "CHECKPOINT",
389
- "COMMENT",
390
- "COMMIT",
391
- "COPY",
392
- "CREATE",
393
- "DELETE",
394
- "DETACH",
395
- "DROP",
396
- "EXPLAIN",
397
- "EXPORT",
398
- "FORCE",
399
- "IMPORT",
400
- "INSTALL",
401
- "LOAD",
402
- "MATCH",
403
- "MERGE",
404
- "OPTIONAL",
405
- "PROFILE",
406
- "RETURN",
407
- "ROLLBACK",
408
- "SET",
409
- "UNWIND",
410
- "UNINSTALL",
411
- "UPDATE",
412
- "USE",
413
- "WITH",
414
- }
415
-
416
- query = query.strip()
417
- if not query:
418
- return False, "Empty query"
419
-
420
- # Get the first word
421
- first_word = query.upper().split()[0] if query else ""
422
-
423
- if first_word not in valid_cypher_keywords:
424
- return (
425
- False,
426
- f"Query doesn't start with valid Cypher keyword. Found: '{first_word}'",
427
- )
428
-
429
- return True, ""
430
-
431
-
432
291
  def clean_cypher_response(response_text: str) -> str:
433
292
  """Clean up common LLM formatting artifacts from a Cypher query.
434
293
 
@@ -4,7 +4,6 @@ import time
4
4
  from pathlib import Path
5
5
  from typing import Any
6
6
 
7
- from shotgun.codebase.core.cypher_models import CypherGenerationNotPossibleError
8
7
  from shotgun.codebase.core.manager import CodebaseGraphManager
9
8
  from shotgun.codebase.core.nl_query import generate_cypher
10
9
  from shotgun.codebase.models import CodebaseGraph, QueryResult, QueryType
@@ -191,22 +190,6 @@ class CodebaseService:
191
190
  error=None,
192
191
  )
193
192
 
194
- except CypherGenerationNotPossibleError as e:
195
- # Handle queries that cannot be converted to Cypher
196
- execution_time = (time.time() - start_time) * 1000
197
- logger.info(f"Query cannot be converted to Cypher: {e.reason}")
198
-
199
- return QueryResult(
200
- query=query,
201
- cypher_query=None,
202
- results=[],
203
- column_names=[],
204
- row_count=0,
205
- execution_time_ms=execution_time,
206
- success=False,
207
- error=f"This query cannot be converted to Cypher: {e.reason}",
208
- )
209
-
210
193
  except Exception as e:
211
194
  execution_time = (time.time() - start_time) * 1000
212
195
  logger.error(f"Query execution failed: {e}")
@@ -22,7 +22,7 @@ from shotgun.posthog_telemetry import setup_posthog_observability
22
22
  from shotgun.sentry_telemetry import setup_sentry_observability
23
23
  from shotgun.telemetry import setup_logfire_observability
24
24
  from shotgun.tui import app as tui_app
25
- from shotgun.utils.update_checker import check_for_updates_async
25
+ from shotgun.utils.update_checker import check_and_install_updates_async
26
26
 
27
27
  # Load environment variables from .env file
28
28
  load_dotenv()
@@ -52,6 +52,7 @@ logger.debug("PostHog analytics enabled: %s", _posthog_enabled)
52
52
 
53
53
  # Global variable to store update notification
54
54
  _update_notification: str | None = None
55
+ _update_progress: str | None = None
55
56
 
56
57
 
57
58
  def _update_callback(notification: str) -> None:
@@ -60,6 +61,13 @@ def _update_callback(notification: str) -> None:
60
61
  _update_notification = notification
61
62
 
62
63
 
64
+ def _update_progress_callback(progress: str) -> None:
65
+ """Callback to store update progress."""
66
+ global _update_progress
67
+ _update_progress = progress
68
+ logger.debug(f"Update progress: {progress}")
69
+
70
+
63
71
  app = typer.Typer(
64
72
  name="shotgun",
65
73
  help="Shotgun - AI-powered CLI tool for research, planning, and task management",
@@ -121,10 +129,12 @@ def main(
121
129
  """Shotgun - AI-powered CLI tool."""
122
130
  logger.debug("Starting shotgun CLI application")
123
131
 
124
- # Start async update check (non-blocking)
132
+ # Start async update check and install (non-blocking)
125
133
  if not ctx.resilient_parsing:
126
- check_for_updates_async(
127
- callback=_update_callback, no_update_check=no_update_check
134
+ check_and_install_updates_async(
135
+ callback=_update_callback,
136
+ no_update_check=no_update_check,
137
+ progress_callback=_update_progress_callback,
128
138
  )
129
139
 
130
140
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
@@ -25,18 +25,4 @@ Your goal is to return appropriate properties for each node type. Common propert
25
25
  {% include 'codebase/partials/temporal_context.j2' %}
26
26
 
27
27
  **6. Output Format**
28
- You must return a structured JSON response with the following fields:
29
- - `cypher_query`: The generated Cypher query string (or null if not possible)
30
- - `can_generate_valid_cypher`: Boolean indicating if a valid Cypher query can be generated
31
- - `reason_cannot_generate`: String explaining why generation isn't possible (or null if successful)
32
-
33
- **IMPORTANT:** Some queries cannot be expressed in Cypher:
34
- - Conceptual questions requiring interpretation (e.g., "What is the main purpose of this codebase?")
35
- - Questions about code quality or best practices
36
- - Questions requiring semantic understanding beyond structure
37
-
38
- For these, set `can_generate_valid_cypher` to false and provide a clear explanation in `reason_cannot_generate`.
39
-
40
- Examples:
41
- - Query: "Show all classes" → can_generate_valid_cypher: true, cypher_query: "MATCH (c:Class) RETURN c.name, c.qualified_name;"
42
- - Query: "What is the main purpose of this codebase?" → can_generate_valid_cypher: false, reason_cannot_generate: "This is a conceptual question requiring interpretation and analysis of the code's overall design and intent, rather than a structural query about specific code elements."
28
+ Provide only the Cypher query.
@@ -9,7 +9,7 @@ from shotgun.agents.config import ConfigManager, get_config_manager
9
9
  from shotgun.logging_config import get_logger
10
10
  from shotgun.tui.screens.splash import SplashScreen
11
11
  from shotgun.utils.file_system_utils import get_shotgun_base_path
12
- from shotgun.utils.update_checker import check_for_updates_async
12
+ from shotgun.utils.update_checker import check_and_install_updates_async
13
13
 
14
14
  from .screens.chat import ChatScreen
15
15
  from .screens.directory_setup import DirectorySetupScreen
@@ -37,16 +37,26 @@ class ShotgunApp(App[None]):
37
37
  self.no_update_check = no_update_check
38
38
  self.continue_session = continue_session
39
39
  self.update_notification: str | None = None
40
+ self.update_progress: str | None = None
40
41
 
41
- # Start async update check
42
+ # Start async update check and install
42
43
  if not no_update_check:
43
- check_for_updates_async(callback=self._update_callback)
44
+ check_and_install_updates_async(
45
+ callback=self._update_callback,
46
+ no_update_check=no_update_check,
47
+ progress_callback=self._update_progress_callback,
48
+ )
44
49
 
45
50
  def _update_callback(self, notification: str) -> None:
46
51
  """Store update notification to show later."""
47
52
  self.update_notification = notification
48
53
  logger.debug(f"Update notification received: {notification}")
49
54
 
55
+ def _update_progress_callback(self, progress: str) -> None:
56
+ """Store update progress."""
57
+ self.update_progress = progress
58
+ logger.debug(f"Update progress: {progress}")
59
+
50
60
  def on_mount(self) -> None:
51
61
  self.theme = "gruvbox"
52
62
  # Track TUI startup
@@ -411,7 +411,7 @@ class ChatScreen(Screen[None]):
411
411
  await self.codebase_sdk.list_codebases_for_directory()
412
412
  ).graphs
413
413
  if accessible_graphs:
414
- self.mount_hint(help_text_with_codebase(already_indexed=True))
414
+ self.mount_hint(help_text_with_codebase())
415
415
  return
416
416
 
417
417
  should_index = await self.app.push_screen_wait(CodebaseIndexPromptScreen())
@@ -419,8 +419,6 @@ class ChatScreen(Screen[None]):
419
419
  self.mount_hint(help_text_empty_dir())
420
420
  return
421
421
 
422
- self.mount_hint(help_text_with_codebase(already_indexed=False))
423
-
424
422
  self.index_codebase_command()
425
423
 
426
424
  def watch_mode(self, new_mode: AgentType) -> None:
@@ -694,6 +692,7 @@ class ChatScreen(Screen[None]):
694
692
  timeout=8,
695
693
  )
696
694
 
695
+ self.mount_hint(codebase_indexed_hint(selection.name))
697
696
  except CodebaseAlreadyIndexedError as exc:
698
697
  logger.warning(f"Codebase already indexed: {exc}")
699
698
  self.notify(str(exc), severity="warning")
@@ -797,10 +796,17 @@ class ChatScreen(Screen[None]):
797
796
  self.mode = AgentType(conversation.last_agent_model)
798
797
 
799
798
 
800
- def help_text_with_codebase(already_indexed: bool = False) -> str:
799
+ def codebase_indexed_hint(codebase_name: str) -> str:
800
+ return (
801
+ f"Codebase **{codebase_name}** indexed successfully. You can now use it in your chat.\n\n"
802
+ + help_text_with_codebase()
803
+ )
804
+
805
+
806
+ def help_text_with_codebase() -> str:
801
807
  return (
802
808
  "Howdy! Welcome to Shotgun - the context tool for software engineering. \n\nYou can research, build specs, plan, create tasks, and export context to your favorite code-gen agents.\n\n"
803
- f"{'' if already_indexed else 'Once your codebase is indexed, '}I can help with:\n\n"
809
+ "I can help with:\n\n"
804
810
  "- Speccing out a new feature\n"
805
811
  "- Onboarding you onto this project\n"
806
812
  "- Helping with a refactor spec\n"
@@ -136,8 +136,7 @@ class UserQuestionWidget(Widget):
136
136
  if part.tool_name == "ask_user" and isinstance(part.content, dict):
137
137
  acc += f"**>** {part.content['answer']}\n\n"
138
138
  else:
139
- # acc += " ∟ finished\n\n" # let's not show anything yet
140
- pass
139
+ acc += " ∟ finished\n\n" # let's not show anything yet
141
140
  elif isinstance(part, UserPromptPart):
142
141
  acc += f"**>** {part.content}\n\n"
143
142
  return acc
@@ -153,7 +152,7 @@ class AgentResponseWidget(Widget):
153
152
  if self.item is None:
154
153
  yield Markdown(markdown="")
155
154
  else:
156
- yield Markdown(markdown=self.compute_output())
155
+ yield Markdown(markdown=f"**⏺** {self.compute_output()}")
157
156
 
158
157
  def compute_output(self) -> str:
159
158
  acc = ""
@@ -161,10 +160,10 @@ class AgentResponseWidget(Widget):
161
160
  return ""
162
161
  for idx, part in enumerate(self.item.parts):
163
162
  if isinstance(part, TextPart):
164
- acc += f"**⏺** {part.content}\n\n"
163
+ acc += f"{part.content}\n\n"
165
164
  elif isinstance(part, ToolCallPart):
166
165
  parts_str = self._format_tool_call_part(part)
167
- acc += parts_str + "\n\n"
166
+ acc += f"{part.tool_name}: " + parts_str + "\n\n"
168
167
  elif isinstance(part, BuiltinToolCallPart):
169
168
  acc += f"{part.tool_name}({part.args})\n\n"
170
169
  elif isinstance(part, BuiltinToolReturnPart):