shotgun-sh 0.1.14__py3-none-any.whl → 0.2.11__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 (143) hide show
  1. shotgun/agents/agent_manager.py +715 -75
  2. shotgun/agents/common.py +80 -75
  3. shotgun/agents/config/constants.py +21 -10
  4. shotgun/agents/config/manager.py +322 -97
  5. shotgun/agents/config/models.py +114 -84
  6. shotgun/agents/config/provider.py +232 -88
  7. shotgun/agents/context_analyzer/__init__.py +28 -0
  8. shotgun/agents/context_analyzer/analyzer.py +471 -0
  9. shotgun/agents/context_analyzer/constants.py +9 -0
  10. shotgun/agents/context_analyzer/formatter.py +115 -0
  11. shotgun/agents/context_analyzer/models.py +212 -0
  12. shotgun/agents/conversation_history.py +125 -2
  13. shotgun/agents/conversation_manager.py +57 -19
  14. shotgun/agents/export.py +6 -7
  15. shotgun/agents/history/compaction.py +10 -5
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +129 -12
  18. shotgun/agents/history/token_counting/__init__.py +31 -0
  19. shotgun/agents/history/token_counting/anthropic.py +127 -0
  20. shotgun/agents/history/token_counting/base.py +78 -0
  21. shotgun/agents/history/token_counting/openai.py +90 -0
  22. shotgun/agents/history/token_counting/sentencepiece_counter.py +127 -0
  23. shotgun/agents/history/token_counting/tokenizer_cache.py +92 -0
  24. shotgun/agents/history/token_counting/utils.py +144 -0
  25. shotgun/agents/history/token_estimation.py +12 -12
  26. shotgun/agents/llm.py +62 -0
  27. shotgun/agents/models.py +59 -4
  28. shotgun/agents/plan.py +6 -7
  29. shotgun/agents/research.py +7 -8
  30. shotgun/agents/specify.py +6 -7
  31. shotgun/agents/tasks.py +6 -7
  32. shotgun/agents/tools/__init__.py +0 -2
  33. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  34. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  35. shotgun/agents/tools/codebase/file_read.py +11 -2
  36. shotgun/agents/tools/codebase/query_graph.py +6 -0
  37. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  38. shotgun/agents/tools/file_management.py +82 -16
  39. shotgun/agents/tools/registry.py +217 -0
  40. shotgun/agents/tools/web_search/__init__.py +55 -16
  41. shotgun/agents/tools/web_search/anthropic.py +76 -51
  42. shotgun/agents/tools/web_search/gemini.py +50 -27
  43. shotgun/agents/tools/web_search/openai.py +26 -17
  44. shotgun/agents/tools/web_search/utils.py +2 -2
  45. shotgun/agents/usage_manager.py +164 -0
  46. shotgun/api_endpoints.py +15 -0
  47. shotgun/cli/clear.py +53 -0
  48. shotgun/cli/compact.py +186 -0
  49. shotgun/cli/config.py +41 -67
  50. shotgun/cli/context.py +111 -0
  51. shotgun/cli/export.py +1 -1
  52. shotgun/cli/feedback.py +50 -0
  53. shotgun/cli/models.py +3 -2
  54. shotgun/cli/plan.py +1 -1
  55. shotgun/cli/research.py +1 -1
  56. shotgun/cli/specify.py +1 -1
  57. shotgun/cli/tasks.py +1 -1
  58. shotgun/cli/update.py +16 -2
  59. shotgun/codebase/core/change_detector.py +5 -3
  60. shotgun/codebase/core/code_retrieval.py +4 -2
  61. shotgun/codebase/core/ingestor.py +57 -16
  62. shotgun/codebase/core/manager.py +20 -7
  63. shotgun/codebase/core/nl_query.py +1 -1
  64. shotgun/codebase/models.py +4 -4
  65. shotgun/exceptions.py +32 -0
  66. shotgun/llm_proxy/__init__.py +19 -0
  67. shotgun/llm_proxy/clients.py +44 -0
  68. shotgun/llm_proxy/constants.py +15 -0
  69. shotgun/logging_config.py +18 -27
  70. shotgun/main.py +91 -12
  71. shotgun/posthog_telemetry.py +81 -10
  72. shotgun/prompts/agents/export.j2 +18 -1
  73. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  74. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  75. shotgun/prompts/agents/plan.j2 +1 -1
  76. shotgun/prompts/agents/research.j2 +1 -1
  77. shotgun/prompts/agents/specify.j2 +270 -3
  78. shotgun/prompts/agents/state/system_state.j2 +4 -0
  79. shotgun/prompts/agents/tasks.j2 +1 -1
  80. shotgun/prompts/loader.py +2 -2
  81. shotgun/prompts/tools/web_search.j2 +14 -0
  82. shotgun/sentry_telemetry.py +27 -18
  83. shotgun/settings.py +238 -0
  84. shotgun/shotgun_web/__init__.py +19 -0
  85. shotgun/shotgun_web/client.py +138 -0
  86. shotgun/shotgun_web/constants.py +21 -0
  87. shotgun/shotgun_web/models.py +47 -0
  88. shotgun/telemetry.py +24 -36
  89. shotgun/tui/app.py +251 -23
  90. shotgun/tui/commands/__init__.py +1 -1
  91. shotgun/tui/components/context_indicator.py +179 -0
  92. shotgun/tui/components/mode_indicator.py +70 -0
  93. shotgun/tui/components/status_bar.py +48 -0
  94. shotgun/tui/containers.py +91 -0
  95. shotgun/tui/dependencies.py +39 -0
  96. shotgun/tui/protocols.py +45 -0
  97. shotgun/tui/screens/chat/__init__.py +5 -0
  98. shotgun/tui/screens/chat/chat.tcss +54 -0
  99. shotgun/tui/screens/chat/chat_screen.py +1234 -0
  100. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  101. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  102. shotgun/tui/screens/chat/help_text.py +40 -0
  103. shotgun/tui/screens/chat/prompt_history.py +48 -0
  104. shotgun/tui/screens/chat.tcss +11 -0
  105. shotgun/tui/screens/chat_screen/command_providers.py +226 -11
  106. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  107. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  108. shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
  109. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  110. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  111. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  112. shotgun/tui/screens/confirmation_dialog.py +151 -0
  113. shotgun/tui/screens/feedback.py +193 -0
  114. shotgun/tui/screens/github_issue.py +102 -0
  115. shotgun/tui/screens/model_picker.py +352 -0
  116. shotgun/tui/screens/onboarding.py +431 -0
  117. shotgun/tui/screens/pipx_migration.py +153 -0
  118. shotgun/tui/screens/provider_config.py +156 -39
  119. shotgun/tui/screens/shotgun_auth.py +295 -0
  120. shotgun/tui/screens/welcome.py +198 -0
  121. shotgun/tui/services/__init__.py +5 -0
  122. shotgun/tui/services/conversation_service.py +184 -0
  123. shotgun/tui/state/__init__.py +7 -0
  124. shotgun/tui/state/processing_state.py +185 -0
  125. shotgun/tui/utils/mode_progress.py +14 -7
  126. shotgun/tui/widgets/__init__.py +5 -0
  127. shotgun/tui/widgets/widget_coordinator.py +262 -0
  128. shotgun/utils/datetime_utils.py +77 -0
  129. shotgun/utils/env_utils.py +13 -0
  130. shotgun/utils/file_system_utils.py +22 -2
  131. shotgun/utils/marketing.py +110 -0
  132. shotgun/utils/update_checker.py +69 -14
  133. shotgun_sh-0.2.11.dist-info/METADATA +130 -0
  134. shotgun_sh-0.2.11.dist-info/RECORD +194 -0
  135. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/entry_points.txt +1 -0
  136. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/licenses/LICENSE +1 -1
  137. shotgun/agents/history/token_counting.py +0 -429
  138. shotgun/agents/tools/user_interaction.py +0 -37
  139. shotgun/tui/screens/chat.py +0 -797
  140. shotgun/tui/screens/chat_screen/history.py +0 -350
  141. shotgun_sh-0.1.14.dist-info/METADATA +0 -466
  142. shotgun_sh-0.1.14.dist-info/RECORD +0 -133
  143. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/WHEEL +0 -0
shotgun/cli/config.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Configuration management CLI commands."""
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  from typing import Annotated, Any
5
6
 
@@ -9,6 +10,7 @@ from rich.table import Table
9
10
 
10
11
  from shotgun.agents.config import ProviderType, get_config_manager
11
12
  from shotgun.logging_config import get_logger
13
+ from shotgun.utils.env_utils import is_shotgun_account_enabled
12
14
 
13
15
  logger = get_logger(__name__)
14
16
  console = Console()
@@ -43,11 +45,11 @@ def init(
43
45
  console.print()
44
46
 
45
47
  # Initialize with defaults
46
- config = config_manager.initialize()
48
+ asyncio.run(config_manager.initialize())
47
49
 
48
- # Ask for default provider
50
+ # Ask for provider
49
51
  provider_choices = ["openai", "anthropic", "google"]
50
- console.print("Choose your default AI provider:")
52
+ console.print("Choose your AI provider:")
51
53
  for i, provider in enumerate(provider_choices, 1):
52
54
  console.print(f" {i}. {provider}")
53
55
 
@@ -55,7 +57,7 @@ def init(
55
57
  try:
56
58
  choice = typer.prompt("Enter choice (1-3)", type=int)
57
59
  if 1 <= choice <= 3:
58
- config.default_provider = ProviderType(provider_choices[choice - 1])
60
+ provider = ProviderType(provider_choices[choice - 1])
59
61
  break
60
62
  else:
61
63
  console.print(
@@ -65,7 +67,6 @@ def init(
65
67
  console.print("❌ Please enter a valid number.", style="red")
66
68
 
67
69
  # Ask for API key for the selected provider
68
- provider = config.default_provider
69
70
  console.print(f"\n🔑 Setting up {provider.upper()} API key...")
70
71
 
71
72
  api_key = typer.prompt(
@@ -75,16 +76,16 @@ def init(
75
76
  )
76
77
 
77
78
  if api_key:
78
- config_manager.update_provider(provider, api_key=api_key)
79
+ # update_provider will automatically set selected_model for first provider
80
+ asyncio.run(config_manager.update_provider(provider, api_key=api_key))
79
81
 
80
- config_manager.save()
81
82
  console.print(
82
83
  f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
83
84
  )
84
85
  console.print("🎯 You can now use Shotgun with your configured provider!")
85
86
 
86
87
  else:
87
- config_manager.initialize()
88
+ asyncio.run(config_manager.initialize())
88
89
  console.print(f"✅ Configuration initialized at {config_manager.config_path}")
89
90
 
90
91
 
@@ -98,16 +99,12 @@ def set(
98
99
  str | None,
99
100
  typer.Option("--api-key", "-k", help="API key for the provider"),
100
101
  ] = None,
101
- default: Annotated[
102
- bool,
103
- typer.Option("--default", "-d", help="Set this provider as default"),
104
- ] = False,
105
102
  ) -> None:
106
103
  """Set configuration for a specific provider."""
107
104
  config_manager = get_config_manager()
108
105
 
109
- # If no API key provided via option and not just setting default, prompt for it
110
- if api_key is None and not default:
106
+ # If no API key provided via option, prompt for it
107
+ if api_key is None:
111
108
  api_key = typer.prompt(
112
109
  f"Enter your {provider.upper()} API key",
113
110
  hide_input=True,
@@ -116,12 +113,7 @@ def set(
116
113
 
117
114
  try:
118
115
  if api_key:
119
- config_manager.update_provider(provider, api_key=api_key)
120
-
121
- if default:
122
- config = config_manager.load()
123
- config.default_provider = provider
124
- config_manager.save(config)
116
+ asyncio.run(config_manager.update_provider(provider, api_key=api_key))
125
117
 
126
118
  console.print(f"✅ Configuration updated for {provider}")
127
119
 
@@ -130,41 +122,6 @@ def set(
130
122
  raise typer.Exit(1) from e
131
123
 
132
124
 
133
- @app.command()
134
- def set_default(
135
- provider: Annotated[
136
- ProviderType,
137
- typer.Argument(
138
- help="AI provider to set as default (openai, anthropic, google)"
139
- ),
140
- ],
141
- ) -> None:
142
- """Set the default AI provider without modifying API keys."""
143
- config_manager = get_config_manager()
144
-
145
- try:
146
- config = config_manager.load()
147
-
148
- # Check if the provider has an API key configured
149
- provider_config = getattr(config, provider.value)
150
- if not provider_config.api_key:
151
- console.print(
152
- f"⚠️ Warning: {provider.upper()} does not have an API key configured.",
153
- style="yellow",
154
- )
155
- console.print(f"Use 'shotgun config set {provider}' to configure it.")
156
-
157
- # Set as default
158
- config.default_provider = provider
159
- config_manager.save(config)
160
-
161
- console.print(f"✅ Default provider set to: {provider}")
162
-
163
- except Exception as e:
164
- console.print(f"❌ Failed to set default provider: {e}", style="red")
165
- raise typer.Exit(1) from e
166
-
167
-
168
125
  @app.command()
169
126
  def get(
170
127
  provider: Annotated[
@@ -177,8 +134,10 @@ def get(
177
134
  ] = False,
178
135
  ) -> None:
179
136
  """Display current configuration."""
137
+ import asyncio
138
+
180
139
  config_manager = get_config_manager()
181
- config = config_manager.load()
140
+ config = asyncio.run(config_manager.load())
182
141
 
183
142
  if json_output:
184
143
  # Convert to dict and mask secrets
@@ -201,16 +160,23 @@ def _show_full_config(config: Any) -> None:
201
160
  table.add_column("Setting", style="cyan")
202
161
  table.add_column("Value", style="white")
203
162
 
204
- # Default provider
205
- table.add_row("Default Provider", f"[bold]{config.default_provider}[/bold]")
163
+ # Selected model
164
+ selected_model = config.selected_model or "None (will auto-detect)"
165
+ table.add_row("Selected Model", f"[bold]{selected_model}[/bold]")
206
166
  table.add_row("", "") # Separator
207
167
 
208
168
  # Provider configurations
209
- for provider_name, provider_config in [
169
+ providers_to_show = [
210
170
  ("OpenAI", config.openai),
211
171
  ("Anthropic", config.anthropic),
212
172
  ("Google", config.google),
213
- ]:
173
+ ]
174
+
175
+ # Only show Shotgun Account if feature flag is enabled
176
+ if is_shotgun_account_enabled():
177
+ providers_to_show.append(("Shotgun Account", config.shotgun))
178
+
179
+ for provider_name, provider_config in providers_to_show:
214
180
  table.add_row(f"[bold]{provider_name}[/bold]", "")
215
181
 
216
182
  # API Key
@@ -231,6 +197,8 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
231
197
  provider_config = config.anthropic
232
198
  elif provider_str == "google":
233
199
  provider_config = config.google
200
+ elif provider_str == "shotgun":
201
+ provider_config = config.shotgun
234
202
  else:
235
203
  console.print(f"❌ Unknown provider: {provider}", style="red")
236
204
  return
@@ -248,7 +216,13 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
248
216
 
249
217
  def _mask_secrets(data: dict[str, Any]) -> None:
250
218
  """Mask secrets in configuration data."""
251
- for provider in ["openai", "anthropic", "google"]:
219
+ providers = ["openai", "anthropic", "google"]
220
+
221
+ # Only mask shotgun if feature flag is enabled
222
+ if is_shotgun_account_enabled():
223
+ providers.append("shotgun")
224
+
225
+ for provider in providers:
252
226
  if provider in data and isinstance(data[provider], dict):
253
227
  if "api_key" in data[provider] and data[provider]["api_key"]:
254
228
  data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
@@ -262,14 +236,14 @@ def _mask_value(value: str) -> str:
262
236
 
263
237
 
264
238
  @app.command()
265
- def get_user_id() -> None:
266
- """Get the anonymous user ID from configuration."""
239
+ def get_shotgun_instance_id() -> None:
240
+ """Get the anonymous shotgun instance ID from configuration."""
267
241
  config_manager = get_config_manager()
268
242
 
269
243
  try:
270
- user_id = config_manager.get_user_id()
271
- console.print(f"[green]User ID:[/green] {user_id}")
244
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
245
+ console.print(f"[green]Shotgun Instance ID:[/green] {shotgun_instance_id}")
272
246
  except Exception as e:
273
- logger.error(f"Error getting user ID: {e}")
274
- console.print(f"❌ Failed to get user ID: {str(e)}", style="red")
247
+ logger.error(f"Error getting shotgun instance ID: {e}")
248
+ console.print(f"❌ Failed to get shotgun instance ID: {str(e)}", style="red")
275
249
  raise typer.Exit(1) from e
shotgun/cli/context.py ADDED
@@ -0,0 +1,111 @@
1
+ """Context command for shotgun CLI."""
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from shotgun.agents.config import get_provider_model
12
+ from shotgun.agents.context_analyzer import (
13
+ ContextAnalysisOutput,
14
+ ContextAnalyzer,
15
+ ContextFormatter,
16
+ )
17
+ from shotgun.agents.conversation_manager import ConversationManager
18
+ from shotgun.cli.models import OutputFormat
19
+ from shotgun.logging_config import get_logger
20
+
21
+ app = typer.Typer(
22
+ name="context", help="Analyze conversation context usage", no_args_is_help=False
23
+ )
24
+ logger = get_logger(__name__)
25
+ console = Console()
26
+
27
+
28
+ @app.callback(invoke_without_command=True)
29
+ def context(
30
+ format: Annotated[
31
+ OutputFormat,
32
+ typer.Option(
33
+ "--format",
34
+ "-f",
35
+ help="Output format: markdown or json",
36
+ ),
37
+ ] = OutputFormat.MARKDOWN,
38
+ ) -> None:
39
+ """Analyze the current conversation's context usage.
40
+
41
+ This command analyzes the agent's message history from ~/.shotgun-sh/conversation.json
42
+ and displays token usage breakdown by message type. Only agent context is counted
43
+ (UI elements like hints are excluded).
44
+ """
45
+ try:
46
+ result = asyncio.run(analyze_context())
47
+
48
+ if format == OutputFormat.JSON:
49
+ # Output as JSON
50
+ console.print_json(json.dumps(result.json_data, indent=2))
51
+ else:
52
+ # Output as plain text (Markdown() reformats and makes categories inline)
53
+ console.print(result.markdown)
54
+
55
+ except FileNotFoundError as e:
56
+ console.print(
57
+ f"[red]Error:[/red] {e}\n\n"
58
+ "No conversation found. Start a TUI session first with: [cyan]shotgun[/cyan]",
59
+ style="bold",
60
+ )
61
+ raise typer.Exit(code=1) from e
62
+ except Exception as e:
63
+ console.print(f"[red]Error:[/red] Failed to analyze context: {e}", style="bold")
64
+ logger.debug("Full traceback:", exc_info=True)
65
+ raise typer.Exit(code=1) from e
66
+
67
+
68
+ async def analyze_context() -> ContextAnalysisOutput:
69
+ """Analyze the conversation context and return structured data.
70
+
71
+ Returns:
72
+ ContextAnalysisOutput with both markdown and JSON representations of the analysis
73
+ """
74
+ # Get conversation file path
75
+ conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
76
+
77
+ if not conversation_file.exists():
78
+ raise FileNotFoundError(f"Conversation file not found at {conversation_file}")
79
+
80
+ # Load conversation
81
+ manager = ConversationManager(conversation_file)
82
+ conversation = await manager.load()
83
+
84
+ if not conversation:
85
+ raise ValueError("Conversation file is empty or corrupted")
86
+
87
+ # Get agent messages only (not UI messages)
88
+ agent_messages = conversation.get_agent_messages()
89
+
90
+ if not agent_messages:
91
+ raise ValueError("No agent messages found in conversation")
92
+
93
+ # Get model config (use default provider settings)
94
+ model_config = await get_provider_model()
95
+
96
+ # Debug: Log the model being used
97
+ logger.debug(f"Using model: {model_config.name.value}")
98
+ logger.debug(f"Provider: {model_config.provider.value}")
99
+ logger.debug(f"Key provider: {model_config.key_provider.value}")
100
+ logger.debug(f"Max input tokens: {model_config.max_input_tokens}")
101
+
102
+ # Analyze with ContextAnalyzer
103
+ analyzer = ContextAnalyzer(model_config)
104
+ # For CLI, agent_messages and ui_message_history are the same (no hints in CLI mode)
105
+ analysis = await analyzer.analyze_conversation(agent_messages, list(agent_messages))
106
+
107
+ # Use formatter to generate markdown and JSON
108
+ markdown = ContextFormatter.format_markdown(analysis)
109
+ json_data = ContextFormatter.format_json(analysis)
110
+
111
+ return ContextAnalysisOutput(markdown=markdown, json_data=json_data)
shotgun/cli/export.py CHANGED
@@ -63,7 +63,7 @@ def export(
63
63
  )
64
64
 
65
65
  # Create the export agent with deps and provider
66
- agent, deps = create_export_agent(agent_runtime_options, provider)
66
+ agent, deps = asyncio.run(create_export_agent(agent_runtime_options, provider))
67
67
 
68
68
  # Start export process
69
69
  logger.info("🎯 Starting export...")
@@ -0,0 +1,50 @@
1
+ """Configuration management CLI commands."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from shotgun.agents.config import get_config_manager
9
+ from shotgun.logging_config import get_logger
10
+ from shotgun.posthog_telemetry import Feedback, FeedbackKind, submit_feedback_survey
11
+
12
+ logger = get_logger(__name__)
13
+ console = Console()
14
+
15
+ app = typer.Typer(
16
+ name="feedback",
17
+ help="Send us feedback",
18
+ no_args_is_help=True,
19
+ )
20
+
21
+
22
+ @app.callback(invoke_without_command=True)
23
+ def send_feedback(
24
+ description: Annotated[str, typer.Argument(help="Description of the feedback")],
25
+ kind: Annotated[
26
+ FeedbackKind,
27
+ typer.Option("--type", "-t", help="Feedback type"),
28
+ ],
29
+ ) -> None:
30
+ """Initialize Shotgun configuration."""
31
+ import asyncio
32
+
33
+ config_manager = get_config_manager()
34
+ asyncio.run(config_manager.load())
35
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
36
+
37
+ if not description:
38
+ console.print(
39
+ '❌ Please add your feedback (shotgun feedback "<your feedback>").',
40
+ style="red",
41
+ )
42
+ raise typer.Exit(1)
43
+
44
+ feedback = Feedback(
45
+ kind=kind, description=description, shotgun_instance_id=shotgun_instance_id
46
+ )
47
+
48
+ submit_feedback_survey(feedback)
49
+
50
+ console.print("Feedback sent. Thank you!")
shotgun/cli/models.py CHANGED
@@ -1,10 +1,11 @@
1
1
  """Common models for CLI commands."""
2
2
 
3
- from enum import Enum
3
+ from enum import StrEnum
4
4
 
5
5
 
6
- class OutputFormat(str, Enum):
6
+ class OutputFormat(StrEnum):
7
7
  """Output format options for CLI commands."""
8
8
 
9
9
  TEXT = "text"
10
10
  JSON = "json"
11
+ MARKDOWN = "markdown"
shotgun/cli/plan.py CHANGED
@@ -55,7 +55,7 @@ def plan(
55
55
  )
56
56
 
57
57
  # Create the plan agent with deps and provider
58
- agent, deps = create_plan_agent(agent_runtime_options, provider)
58
+ agent, deps = asyncio.run(create_plan_agent(agent_runtime_options, provider))
59
59
 
60
60
  # Start planning process
61
61
  logger.info("🎯 Starting planning...")
shotgun/cli/research.py CHANGED
@@ -73,7 +73,7 @@ async def async_research(
73
73
  agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
74
74
 
75
75
  # Create the research agent with deps and provider
76
- agent, deps = create_research_agent(agent_runtime_options, provider)
76
+ agent, deps = await create_research_agent(agent_runtime_options, provider)
77
77
 
78
78
  # Start research process
79
79
  logger.info("🔬 Starting research...")
shotgun/cli/specify.py CHANGED
@@ -51,7 +51,7 @@ def specify(
51
51
  )
52
52
 
53
53
  # Create the specify agent with deps and provider
54
- agent, deps = create_specify_agent(agent_runtime_options, provider)
54
+ agent, deps = asyncio.run(create_specify_agent(agent_runtime_options, provider))
55
55
 
56
56
  # Start specification process
57
57
  logger.info("📋 Starting specification generation...")
shotgun/cli/tasks.py CHANGED
@@ -60,7 +60,7 @@ def tasks(
60
60
  )
61
61
 
62
62
  # Create the tasks agent with deps and provider
63
- agent, deps = create_tasks_agent(agent_runtime_options, provider)
63
+ agent, deps = asyncio.run(create_tasks_agent(agent_runtime_options, provider))
64
64
 
65
65
  # Start task creation process
66
66
  logger.info("🎯 Starting task creation...")
shotgun/cli/update.py CHANGED
@@ -45,7 +45,7 @@ def update(
45
45
 
46
46
  This command will:
47
47
  - Check PyPI for the latest version
48
- - Detect your installation method (pipx, pip, or venv)
48
+ - Detect your installation method (uvx, uv-tool, pipx, pip, or venv)
49
49
  - Perform the appropriate upgrade command
50
50
 
51
51
  Examples:
@@ -93,6 +93,8 @@ def update(
93
93
  )
94
94
  console.print(
95
95
  "Use --force to update anyway, or install the stable version with:\n"
96
+ " uv tool install shotgun-sh\n"
97
+ " or\n"
96
98
  " pipx install shotgun-sh\n"
97
99
  " or\n"
98
100
  " pip install shotgun-sh",
@@ -134,7 +136,19 @@ def update(
134
136
  console.print(f"\n[red]✗[/red] {message}", style="bold red")
135
137
 
136
138
  # Provide manual update instructions
137
- if method == "pipx":
139
+ if method == "uvx":
140
+ console.print(
141
+ "\n[yellow]Run uvx again to use the latest version:[/yellow]\n"
142
+ " uvx shotgun-sh\n"
143
+ "\n[yellow]Or install permanently:[/yellow]\n"
144
+ " uv tool install shotgun-sh"
145
+ )
146
+ elif method == "uv-tool":
147
+ console.print(
148
+ "\n[yellow]Try updating manually:[/yellow]\n"
149
+ " uv tool upgrade shotgun-sh"
150
+ )
151
+ elif method == "pipx":
138
152
  console.print(
139
153
  "\n[yellow]Try updating manually:[/yellow]\n"
140
154
  " pipx upgrade shotgun-sh"
@@ -6,6 +6,7 @@ from enum import Enum
6
6
  from pathlib import Path
7
7
  from typing import Any, cast
8
8
 
9
+ import aiofiles
9
10
  import kuzu
10
11
 
11
12
  from shotgun.logging_config import get_logger
@@ -301,7 +302,7 @@ class ChangeDetector:
301
302
  # Direct substring match
302
303
  return pattern in filepath
303
304
 
304
- def _calculate_file_hash(self, filepath: Path) -> str:
305
+ async def _calculate_file_hash(self, filepath: Path) -> str:
305
306
  """Calculate hash of file contents.
306
307
 
307
308
  Args:
@@ -311,8 +312,9 @@ class ChangeDetector:
311
312
  SHA256 hash of file contents
312
313
  """
313
314
  try:
314
- with open(filepath, "rb") as f:
315
- return hashlib.sha256(f.read()).hexdigest()
315
+ async with aiofiles.open(filepath, "rb") as f:
316
+ content = await f.read()
317
+ return hashlib.sha256(content).hexdigest()
316
318
  except Exception as e:
317
319
  logger.error(f"Failed to calculate hash for {filepath}: {e}")
318
320
  return ""
@@ -3,6 +3,7 @@
3
3
  from pathlib import Path
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ import aiofiles
6
7
  from pydantic import BaseModel
7
8
 
8
9
  from shotgun.logging_config import get_logger
@@ -141,8 +142,9 @@ async def retrieve_code_by_qualified_name(
141
142
 
142
143
  # Read the file and extract the snippet
143
144
  try:
144
- with full_path.open("r", encoding="utf-8") as f:
145
- all_lines = f.readlines()
145
+ async with aiofiles.open(full_path, encoding="utf-8") as f:
146
+ content = await f.read()
147
+ all_lines = content.splitlines(keepends=True)
146
148
 
147
149
  # Extract the relevant lines (1-indexed to 0-indexed)
148
150
  snippet_lines = all_lines[start_line - 1 : end_line]