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/tui/app.py ADDED
@@ -0,0 +1,116 @@
1
+ from collections.abc import Iterable
2
+ from typing import Any
3
+
4
+ from textual.app import App, SystemCommand
5
+ from textual.binding import Binding
6
+ from textual.screen import Screen
7
+
8
+ from shotgun.agents.config import ConfigManager, get_config_manager
9
+ from shotgun.logging_config import get_logger
10
+ from shotgun.tui.screens.splash import SplashScreen
11
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
12
+ from shotgun.utils.update_checker import check_for_updates_async
13
+
14
+ from .screens.chat import ChatScreen
15
+ from .screens.directory_setup import DirectorySetupScreen
16
+ from .screens.provider_config import ProviderConfigScreen
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class ShotgunApp(App[None]):
22
+ SCREENS = {
23
+ "chat": ChatScreen,
24
+ "provider_config": ProviderConfigScreen,
25
+ "directory_setup": DirectorySetupScreen,
26
+ }
27
+ BINDINGS = [
28
+ Binding("ctrl+c", "quit", "Quit the app"),
29
+ ]
30
+ CSS_PATH = "styles.tcss"
31
+
32
+ def __init__(
33
+ self, no_update_check: bool = False, continue_session: bool = False
34
+ ) -> None:
35
+ super().__init__()
36
+ self.config_manager: ConfigManager = get_config_manager()
37
+ self.no_update_check = no_update_check
38
+ self.continue_session = continue_session
39
+ self.update_notification: str | None = None
40
+
41
+ # Start async update check
42
+ if not no_update_check:
43
+ check_for_updates_async(callback=self._update_callback)
44
+
45
+ def _update_callback(self, notification: str) -> None:
46
+ """Store update notification to show later."""
47
+ self.update_notification = notification
48
+ logger.debug(f"Update notification received: {notification}")
49
+
50
+ def on_mount(self) -> None:
51
+ self.theme = "gruvbox"
52
+ # Track TUI startup
53
+ from shotgun.posthog_telemetry import track_event
54
+
55
+ track_event("tui_started", {})
56
+
57
+ self.push_screen(
58
+ SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
59
+ )
60
+
61
+ def refresh_startup_screen(self) -> None:
62
+ """Push the appropriate screen based on configured providers."""
63
+ if not self.config_manager.has_any_provider_key():
64
+ if isinstance(self.screen, ProviderConfigScreen):
65
+ return
66
+
67
+ self.push_screen(
68
+ "provider_config", callback=lambda _arg: self.refresh_startup_screen()
69
+ )
70
+ return
71
+
72
+ if not self.check_local_shotgun_directory_exists():
73
+ if isinstance(self.screen, DirectorySetupScreen):
74
+ return
75
+
76
+ self.push_screen(
77
+ "directory_setup", callback=lambda _arg: self.refresh_startup_screen()
78
+ )
79
+ return
80
+
81
+ if isinstance(self.screen, ChatScreen):
82
+ return
83
+ # Pass continue_session flag to ChatScreen
84
+ self.push_screen(ChatScreen(continue_session=self.continue_session))
85
+
86
+ def check_local_shotgun_directory_exists(self) -> bool:
87
+ shotgun_dir = get_shotgun_base_path()
88
+ return shotgun_dir.exists() and shotgun_dir.is_dir()
89
+
90
+ async def action_quit(self) -> None:
91
+ """Override quit action to show update notification."""
92
+ if self.update_notification:
93
+ # Show notification before quitting
94
+ from rich.console import Console
95
+
96
+ console = Console()
97
+ console.print(f"\n[cyan]{self.update_notification}[/cyan]", style="bold")
98
+ self.exit()
99
+
100
+ def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
101
+ return [] # we don't want any system commands
102
+
103
+
104
+ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
105
+ """Run the TUI application.
106
+
107
+ Args:
108
+ no_update_check: If True, disable automatic update checks.
109
+ continue_session: If True, continue from previous conversation.
110
+ """
111
+ app = ShotgunApp(no_update_check=no_update_check, continue_session=continue_session)
112
+ app.run(inline_no_clear=True)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ run()
@@ -0,0 +1,76 @@
1
+ """Command handling for the TUI chat interface."""
2
+
3
+ from collections.abc import Callable
4
+
5
+
6
+ class CommandHandler:
7
+ """Handles slash commands in the TUI chat interface."""
8
+
9
+ def __init__(self) -> None:
10
+ """Initialize the command handler with available commands."""
11
+ self.commands: dict[str, Callable[[], str]] = {
12
+ "help": self.get_help_text,
13
+ }
14
+
15
+ def is_command(self, text: str) -> bool:
16
+ """Check if the text is a command (starts with /)."""
17
+ return text.strip().startswith("/")
18
+
19
+ def parse_command(self, text: str) -> str:
20
+ """Extract the command name from the text."""
21
+ text = text.strip()
22
+ if not text.startswith("/"):
23
+ return ""
24
+
25
+ # Split on whitespace and get the command part
26
+ parts = text[1:].split()
27
+ return parts[0] if parts else ""
28
+
29
+ def handle_command(self, text: str) -> tuple[bool, str]:
30
+ """
31
+ Handle a command and return success status and response text.
32
+
33
+ Args:
34
+ text: The full command text including the leading /
35
+
36
+ Returns:
37
+ Tuple of (success, response_text)
38
+ """
39
+ if not self.is_command(text):
40
+ return False, ""
41
+
42
+ command = self.parse_command(text)
43
+
44
+ if command in self.commands:
45
+ response = self.commands[command]()
46
+ return True, response
47
+ else:
48
+ return False, self.get_error_message(command)
49
+
50
+ def get_help_text(self) -> str:
51
+ """Return the help text for the /help command."""
52
+ return """📚 **Shotgun Help**
53
+
54
+ **Commands:**
55
+ • `/help` - Show this help message
56
+
57
+ **Keyboard Shortcuts:**
58
+
59
+ * `Enter` - Send message
60
+ * `Ctrl+P` - Open command palette
61
+ * `Shift+Tab` - Cycle agent modes
62
+ * `Ctrl+C` - Quit application
63
+
64
+ **Agent Modes:**
65
+ * **Research** - Research topics with web search and synthesize findings
66
+ * **Specify** - Create detailed specifications and requirements documents
67
+ * **Planning** - Create comprehensive, actionable plans with milestones
68
+ * **Tasks** - Generate specific, actionable tasks from research and plans
69
+ * **Export** - Export artifacts and findings to various formats
70
+
71
+ **Usage:**
72
+ Type your message and press Enter to chat with the AI. The AI will respond based on the current mode."""
73
+
74
+ def get_error_message(self, command: str) -> str:
75
+ """Return a polite error message for unknown commands."""
76
+ return f"⚠️ Sorry, `/{command}` is not a recognized command. Type `/help` to see available commands."
@@ -0,0 +1,69 @@
1
+ from textual import events
2
+ from textual.message import Message
3
+ from textual.widgets import TextArea
4
+
5
+
6
+ class PromptInput(TextArea):
7
+ """A TextArea with a submit binding."""
8
+
9
+ DEFAULT_CSS = """
10
+ PromptInput {
11
+ outline: round $primary;
12
+ background: transparent;
13
+ }
14
+ """
15
+
16
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
17
+ if action != "copy":
18
+ return True
19
+ # run copy action if there is selected text
20
+ # otherwise, do nothing, so global ctrl+c still works.
21
+ return bool(self.selected_text)
22
+
23
+ class Submitted(Message):
24
+ """A message to indicate that the text has been submitted."""
25
+
26
+ def __init__(self, text: str) -> None:
27
+ super().__init__()
28
+ self.text = text
29
+
30
+ def action_submit(self) -> None:
31
+ """An action to submit the text."""
32
+ self.post_message(self.Submitted(self.text))
33
+
34
+ async def _on_key(self, event: events.Key) -> None:
35
+ """Handle key presses which correspond to document inserts."""
36
+
37
+ # Don't handle Enter key here - let the binding handle it
38
+ if event.key == "enter":
39
+ self.action_submit()
40
+
41
+ self._restart_blink()
42
+
43
+ if self.read_only:
44
+ return
45
+
46
+ key = event.key
47
+ insert_values = {
48
+ "ctrl+j": "\n",
49
+ }
50
+ if self.tab_behavior == "indent":
51
+ if key == "escape":
52
+ event.stop()
53
+ event.prevent_default()
54
+ self.screen.focus_next()
55
+ return
56
+ if self.indent_type == "tabs":
57
+ insert_values["tab"] = "\t"
58
+ else:
59
+ insert_values["tab"] = " " * self._find_columns_to_next_tab_stop()
60
+
61
+ if event.is_printable or key in insert_values:
62
+ event.stop()
63
+ event.prevent_default()
64
+ insert = insert_values.get(key, event.character)
65
+ # `insert` is not None because event.character cannot be
66
+ # None because we've checked that it's printable.
67
+ assert insert is not None # noqa: S101
68
+ start, end = self.selection
69
+ self._replace_via_keyboard(insert, start, end)
@@ -0,0 +1,86 @@
1
+ """Spinner component for showing loading/working state."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Container
5
+ from textual.css.query import NoMatches
6
+ from textual.reactive import reactive
7
+ from textual.timer import Timer
8
+ from textual.widget import Widget
9
+ from textual.widgets import Static
10
+
11
+
12
+ class Spinner(Widget):
13
+ """A spinner widget that shows a rotating animation when working."""
14
+
15
+ DEFAULT_CSS = """
16
+ Spinner {
17
+ width: auto;
18
+ height: 1;
19
+ }
20
+
21
+ Spinner > Container {
22
+ width: auto;
23
+ height: 1;
24
+ layout: horizontal;
25
+ }
26
+
27
+ Spinner .spinner-icon {
28
+ width: 1;
29
+ margin-right: 1;
30
+ }
31
+
32
+ Spinner .spinner-text {
33
+ width: auto;
34
+ }
35
+ """
36
+
37
+ # Animation frames for the spinner
38
+ FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
39
+
40
+ text = reactive("Working...")
41
+ _frame_index = reactive(0)
42
+
43
+ def __init__(
44
+ self,
45
+ text: str = "Working...",
46
+ *,
47
+ name: str | None = None,
48
+ id: str | None = None,
49
+ classes: str | None = None,
50
+ ) -> None:
51
+ super().__init__(name=name, id=id, classes=classes)
52
+ self.text = text
53
+ self._timer: Timer | None = None
54
+
55
+ def compose(self) -> ComposeResult:
56
+ """Compose the spinner widget."""
57
+ with Container():
58
+ yield Static("", classes="spinner-icon")
59
+ yield Static(self.text, classes="spinner-text")
60
+
61
+ def on_mount(self) -> None:
62
+ """Set up the animation timer when mounted."""
63
+ self._timer = self.set_interval(0.1, self._advance_frame)
64
+
65
+ def _advance_frame(self) -> None:
66
+ """Advance to the next animation frame."""
67
+ self._frame_index = (self._frame_index + 1) % len(self.FRAMES)
68
+ self._update_display()
69
+
70
+ def _update_display(self) -> None:
71
+ """Update the spinner display."""
72
+ try:
73
+ icon_widget = self.query_one(".spinner-icon", Static)
74
+ icon_widget.update(self.FRAMES[self._frame_index])
75
+ except NoMatches:
76
+ # Widget not mounted yet, ignore
77
+ pass
78
+
79
+ def watch_text(self, text: str) -> None:
80
+ """React to changes in the text."""
81
+ try:
82
+ text_widget = self.query_one(".spinner-text", Static)
83
+ text_widget.update(text)
84
+ except NoMatches:
85
+ # Widget not mounted yet, ignore
86
+ return
@@ -0,0 +1,25 @@
1
+ from textual.app import RenderResult
2
+ from textual.widgets import Static
3
+
4
+ ART = """
5
+
6
+ ███████╗██╗ ██╗ ██████╗ ████████╗ ██████╗ ██╗ ██╗███╗ ██╗
7
+ ██╔════╝██║ ██║██╔═══██╗╚══██╔══╝██╔════╝ ██║ ██║████╗ ██║
8
+ ███████╗███████║██║ ██║ ██║ ██║ ███╗██║ ██║██╔██╗ ██║
9
+ ╚════██║██╔══██║██║ ██║ ██║ ██║ ██║██║ ██║██║╚██╗██║
10
+ ███████║██║ ██║╚██████╔╝ ██║ ╚██████╔╝╚██████╔╝██║ ╚████║
11
+ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝
12
+
13
+ """
14
+
15
+
16
+ class SplashWidget(Static):
17
+ DEFAULT_CSS = """
18
+ SplashWidget {
19
+ text-align: center;
20
+ width: 64;
21
+ }
22
+ """
23
+
24
+ def render(self) -> RenderResult:
25
+ return ART
@@ -0,0 +1,13 @@
1
+ from textual.containers import VerticalScroll
2
+ from textual.reactive import reactive
3
+
4
+
5
+ class VerticalTail(VerticalScroll):
6
+ """A vertical scroll container that automatically scrolls to the bottom when content is added."""
7
+
8
+ auto_scroll = reactive(True, layout=False)
9
+
10
+ def watch_auto_scroll(self, value: bool) -> None:
11
+ """Handle auto_scroll property changes."""
12
+ if value:
13
+ self.scroll_end(animate=False)