shotgun-sh 0.2.17__py3-none-any.whl → 0.3.3.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. shotgun/agents/agent_manager.py +28 -14
  2. shotgun/agents/common.py +1 -1
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +323 -53
  6. shotgun/agents/config/models.py +85 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/runner.py +230 -0
  23. shotgun/agents/tools/web_search/openai.py +1 -1
  24. shotgun/build_constants.py +2 -2
  25. shotgun/cli/clear.py +1 -1
  26. shotgun/cli/compact.py +5 -3
  27. shotgun/cli/context.py +44 -1
  28. shotgun/cli/error_handler.py +24 -0
  29. shotgun/cli/export.py +34 -34
  30. shotgun/cli/plan.py +34 -34
  31. shotgun/cli/research.py +17 -9
  32. shotgun/cli/spec/__init__.py +5 -0
  33. shotgun/cli/spec/backup.py +81 -0
  34. shotgun/cli/spec/commands.py +132 -0
  35. shotgun/cli/spec/models.py +48 -0
  36. shotgun/cli/spec/pull_service.py +219 -0
  37. shotgun/cli/specify.py +20 -19
  38. shotgun/cli/tasks.py +34 -34
  39. shotgun/codebase/core/ingestor.py +153 -7
  40. shotgun/codebase/models.py +2 -0
  41. shotgun/exceptions.py +325 -0
  42. shotgun/llm_proxy/__init__.py +17 -0
  43. shotgun/llm_proxy/client.py +215 -0
  44. shotgun/llm_proxy/models.py +137 -0
  45. shotgun/logging_config.py +42 -0
  46. shotgun/main.py +4 -0
  47. shotgun/posthog_telemetry.py +1 -1
  48. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -3
  49. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  50. shotgun/prompts/agents/plan.j2 +16 -0
  51. shotgun/prompts/agents/research.j2 +16 -3
  52. shotgun/prompts/agents/specify.j2 +54 -1
  53. shotgun/prompts/agents/state/system_state.j2 +0 -2
  54. shotgun/prompts/agents/tasks.j2 +16 -0
  55. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  56. shotgun/prompts/history/combine_summaries.j2 +53 -0
  57. shotgun/sdk/codebase.py +14 -3
  58. shotgun/settings.py +5 -0
  59. shotgun/shotgun_web/__init__.py +67 -1
  60. shotgun/shotgun_web/client.py +42 -1
  61. shotgun/shotgun_web/constants.py +46 -0
  62. shotgun/shotgun_web/exceptions.py +29 -0
  63. shotgun/shotgun_web/models.py +390 -0
  64. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  65. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  66. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  67. shotgun/shotgun_web/shared_specs/models.py +71 -0
  68. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  69. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  70. shotgun/shotgun_web/specs_client.py +703 -0
  71. shotgun/shotgun_web/supabase_client.py +31 -0
  72. shotgun/tui/app.py +73 -9
  73. shotgun/tui/containers.py +1 -1
  74. shotgun/tui/layout.py +5 -0
  75. shotgun/tui/screens/chat/chat_screen.py +372 -95
  76. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  77. shotgun/tui/screens/chat_screen/command_providers.py +13 -2
  78. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  79. shotgun/tui/screens/confirmation_dialog.py +40 -0
  80. shotgun/tui/screens/directory_setup.py +45 -41
  81. shotgun/tui/screens/feedback.py +10 -3
  82. shotgun/tui/screens/github_issue.py +11 -2
  83. shotgun/tui/screens/model_picker.py +28 -8
  84. shotgun/tui/screens/onboarding.py +149 -0
  85. shotgun/tui/screens/pipx_migration.py +58 -6
  86. shotgun/tui/screens/provider_config.py +66 -8
  87. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  88. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  89. shotgun/tui/screens/shared_specs/models.py +56 -0
  90. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  91. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  92. shotgun/tui/screens/shotgun_auth.py +110 -16
  93. shotgun/tui/screens/spec_pull.py +288 -0
  94. shotgun/tui/screens/welcome.py +123 -0
  95. shotgun/tui/services/conversation_service.py +5 -2
  96. shotgun/tui/widgets/widget_coordinator.py +1 -1
  97. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
  98. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
  99. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  100. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  101. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  102. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  103. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  104. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  105. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  106. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  107. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  108. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  109. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  110. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  111. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +0 -0
  112. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,230 @@
1
+ """Unified agent execution with consistent error handling.
2
+
3
+ This module provides a reusable agent runner that wraps agent execution exceptions
4
+ in user-friendly custom exceptions that can be caught and displayed by TUI or CLI.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from typing import TYPE_CHECKING, NoReturn
10
+
11
+ from anthropic import APIStatusError as AnthropicAPIStatusError
12
+ from openai import APIStatusError as OpenAIAPIStatusError
13
+ from pydantic_ai.exceptions import ModelHTTPError, UnexpectedModelBehavior
14
+
15
+ from shotgun.agents.error.models import AgentErrorContext
16
+ from shotgun.exceptions import (
17
+ AgentCancelledException,
18
+ BudgetExceededException,
19
+ BYOKAuthenticationException,
20
+ BYOKGenericAPIException,
21
+ BYOKQuotaBillingException,
22
+ BYOKRateLimitException,
23
+ BYOKServiceOverloadException,
24
+ ContextSizeLimitExceeded,
25
+ GenericAPIStatusException,
26
+ ShotgunRateLimitException,
27
+ ShotgunServiceOverloadException,
28
+ UnknownAgentException,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from shotgun.agents.agent_manager import AgentManager
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class AgentRunner:
38
+ """Unified agent execution wrapper with consistent error handling.
39
+
40
+ This class wraps agent execution and converts any exceptions into
41
+ user-friendly custom exceptions that can be caught and displayed by the
42
+ calling interface (TUI or CLI).
43
+
44
+ The runner:
45
+ - Executes the agent
46
+ - Logs errors for debugging
47
+ - Wraps exceptions in custom exception types (AgentCancelledException,
48
+ BYOKRateLimitException, etc.)
49
+ - Lets exceptions propagate to caller for display
50
+
51
+ Example:
52
+ >>> runner = AgentRunner(agent_manager)
53
+ >>> try:
54
+ >>> await runner.run("Write a hello world function")
55
+ >>> except ContextSizeLimitExceeded as e:
56
+ >>> print(e.to_markdown())
57
+ >>> except BYOKRateLimitException as e:
58
+ >>> print(e.to_plain_text())
59
+ """
60
+
61
+ def __init__(self, agent_manager: "AgentManager"):
62
+ """Initialize the agent runner.
63
+
64
+ Args:
65
+ agent_manager: The agent manager to execute
66
+ """
67
+ self.agent_manager = agent_manager
68
+
69
+ async def run(self, prompt: str) -> None:
70
+ """Run the agent with the given prompt.
71
+
72
+ Args:
73
+ prompt: The user's prompt/query
74
+
75
+ Raises:
76
+ Custom exceptions for different error types:
77
+ - AgentCancelledException: User cancelled the operation
78
+ - ContextSizeLimitExceeded: Context too large for model
79
+ - BudgetExceededException: Shotgun Account budget exceeded
80
+ - BYOKRateLimitException: BYOK rate limit hit
81
+ - BYOKQuotaBillingException: BYOK quota/billing issue
82
+ - BYOKAuthenticationException: BYOK authentication failed
83
+ - BYOKServiceOverloadException: BYOK service overloaded
84
+ - BYOKGenericAPIException: Generic BYOK API error
85
+ - ShotgunServiceOverloadException: Shotgun service overloaded
86
+ - ShotgunRateLimitException: Shotgun rate limit hit
87
+ - GenericAPIStatusException: Generic API error
88
+ - UnknownAgentException: Unknown/unclassified error
89
+ """
90
+ try:
91
+ await self.agent_manager.run(prompt=prompt)
92
+
93
+ except asyncio.CancelledError as e:
94
+ # User cancelled - wrap and re-raise as our custom exception
95
+ context = self._create_error_context(e)
96
+ self._classify_and_raise(context)
97
+
98
+ except ContextSizeLimitExceeded as e:
99
+ # Already a custom exception - log and re-raise
100
+ logger.info(
101
+ "Context size limit exceeded",
102
+ extra={
103
+ "max_tokens": e.max_tokens,
104
+ "model_name": e.model_name,
105
+ },
106
+ )
107
+ raise
108
+
109
+ except Exception as e:
110
+ # Log with full stack trace to shotgun.log
111
+ logger.exception(
112
+ "Agent run failed",
113
+ extra={
114
+ "agent_mode": self.agent_manager._current_agent_type.value,
115
+ "error_type": type(e).__name__,
116
+ },
117
+ )
118
+
119
+ # Create error context and wrap/raise custom exception
120
+ context = self._create_error_context(e)
121
+ self._classify_and_raise(context)
122
+
123
+ def _create_error_context(self, exception: BaseException) -> AgentErrorContext:
124
+ """Create error context from exception and agent state.
125
+
126
+ Args:
127
+ exception: The exception that was raised
128
+
129
+ Returns:
130
+ AgentErrorContext with all necessary information for classification
131
+ """
132
+ return AgentErrorContext(
133
+ exception=exception,
134
+ is_shotgun_account=self.agent_manager.deps.llm_model.is_shotgun_account,
135
+ )
136
+
137
+ def _classify_and_raise(self, context: AgentErrorContext) -> NoReturn:
138
+ """Classify an exception and raise the appropriate custom exception.
139
+
140
+ Args:
141
+ context: Context information about the error
142
+
143
+ Raises:
144
+ Custom exception based on the error type
145
+ """
146
+ exception = context.exception
147
+ error_name = type(exception).__name__
148
+ error_message = str(exception)
149
+
150
+ # Check for cancellation
151
+ if isinstance(exception, asyncio.CancelledError):
152
+ raise AgentCancelledException() from exception
153
+
154
+ # Check for context size limit exceeded
155
+ if isinstance(exception, ContextSizeLimitExceeded):
156
+ # Already the right exception type, re-raise it
157
+ raise exception
158
+
159
+ # Check for budget exceeded (Shotgun Account only)
160
+ if (
161
+ context.is_shotgun_account
162
+ and "apistatuserror" in error_name.lower()
163
+ and "budget" in error_message.lower()
164
+ and "exceeded" in error_message.lower()
165
+ ):
166
+ raise BudgetExceededException(message=error_message) from exception
167
+
168
+ # Check for empty model response (e.g., model unavailable or misconfigured)
169
+ if isinstance(exception, UnexpectedModelBehavior):
170
+ raise GenericAPIStatusException(
171
+ "The model returned an empty response. This may indicate:\n"
172
+ "- The model is unavailable or misconfigured\n"
173
+ "- A temporary service issue\n\n"
174
+ "Try switching to a different model or try again later."
175
+ ) from exception
176
+
177
+ # Detect API errors
178
+ is_api_error = False
179
+ if isinstance(exception, OpenAIAPIStatusError):
180
+ is_api_error = True
181
+ elif isinstance(exception, AnthropicAPIStatusError):
182
+ is_api_error = True
183
+ elif isinstance(exception, ModelHTTPError):
184
+ # pydantic_ai wraps API errors in ModelHTTPError
185
+ # Check for HTTP error status codes (4xx client errors)
186
+ if 400 <= exception.status_code < 500:
187
+ is_api_error = True
188
+
189
+ # BYOK user API errors
190
+ if not context.is_shotgun_account and is_api_error:
191
+ self._raise_byok_api_error(error_message, exception)
192
+
193
+ # Shotgun Account specific errors
194
+ if "APIStatusError" in error_name:
195
+ if "overload" in error_message.lower():
196
+ raise ShotgunServiceOverloadException(error_message) from exception
197
+ elif "rate" in error_message.lower():
198
+ raise ShotgunRateLimitException(error_message) from exception
199
+ else:
200
+ raise GenericAPIStatusException(error_message) from exception
201
+
202
+ # Unknown error - wrap in our custom exception
203
+ raise UnknownAgentException(exception) from exception
204
+
205
+ def _raise_byok_api_error(
206
+ self, error_message: str, original_exception: Exception
207
+ ) -> NoReturn:
208
+ """Classify and raise API errors for BYOK users into specific types.
209
+
210
+ Args:
211
+ error_message: The error message from the API
212
+ original_exception: The original exception
213
+
214
+ Raises:
215
+ Specific BYOK exception type
216
+ """
217
+ error_lower = error_message.lower()
218
+
219
+ if "rate" in error_lower:
220
+ raise BYOKRateLimitException(error_message) from original_exception
221
+ elif "quota" in error_lower or "billing" in error_lower:
222
+ raise BYOKQuotaBillingException(error_message) from original_exception
223
+ elif "authentication" in error_lower or (
224
+ "invalid" in error_lower and "key" in error_lower
225
+ ):
226
+ raise BYOKAuthenticationException(error_message) from original_exception
227
+ elif "overload" in error_lower:
228
+ raise BYOKServiceOverloadException(error_message) from original_exception
229
+ else:
230
+ raise BYOKGenericAPIException(error_message) from original_exception
@@ -64,7 +64,7 @@ async def openai_web_search_tool(query: str) -> str:
64
64
  )
65
65
 
66
66
  client = AsyncOpenAI(api_key=api_key)
67
- response = await client.responses.create( # type: ignore[call-overload]
67
+ response = await client.responses.create(
68
68
  model="gpt-5-mini",
69
69
  input=[
70
70
  {"role": "user", "content": [{"type": "input_text", "text": prompt}]}
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = 'phc_KKnChzZUKeNqZDOTJ6soCBWNQSx3vjiULdwTR9H5Mcr'
12
12
  POSTHOG_PROJECT_ID = '191396'
13
13
 
14
14
  # Logfire configuration embedded at build time (only for dev builds)
15
- LOGFIRE_ENABLED = ''
16
- LOGFIRE_TOKEN = ''
15
+ LOGFIRE_ENABLED = 'true'
16
+ LOGFIRE_TOKEN = 'pylf_v1_us_RwZMlJm1tX6j0PL5RWWbmZpzK2hLBNtFWStNKlySfjh8'
17
17
 
18
18
  # Build metadata
19
19
  BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
shotgun/cli/clear.py CHANGED
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  import typer
7
7
  from rich.console import Console
8
8
 
9
- from shotgun.agents.conversation_manager import ConversationManager
9
+ from shotgun.agents.conversation import ConversationManager
10
10
  from shotgun.logging_config import get_logger
11
11
 
12
12
  app = typer.Typer(
shotgun/cli/compact.py CHANGED
@@ -10,9 +10,11 @@ from pydantic_ai.usage import RequestUsage
10
10
  from rich.console import Console
11
11
 
12
12
  from shotgun.agents.config import get_provider_model
13
- from shotgun.agents.conversation_manager import ConversationManager
14
- from shotgun.agents.history.history_processors import token_limit_compactor
15
- from shotgun.agents.history.token_estimation import estimate_tokens_from_messages
13
+ from shotgun.agents.conversation import ConversationManager
14
+ from shotgun.agents.conversation.history.history_processors import token_limit_compactor
15
+ from shotgun.agents.conversation.history.token_estimation import (
16
+ estimate_tokens_from_messages,
17
+ )
16
18
  from shotgun.cli.models import OutputFormat
17
19
  from shotgun.logging_config import get_logger
18
20
 
shotgun/cli/context.py CHANGED
@@ -5,6 +5,7 @@ import json
5
5
  from pathlib import Path
6
6
  from typing import Annotated
7
7
 
8
+ import httpx
8
9
  import typer
9
10
  from rich.console import Console
10
11
 
@@ -14,8 +15,9 @@ from shotgun.agents.context_analyzer import (
14
15
  ContextAnalyzer,
15
16
  ContextFormatter,
16
17
  )
17
- from shotgun.agents.conversation_manager import ConversationManager
18
+ from shotgun.agents.conversation import ConversationManager
18
19
  from shotgun.cli.models import OutputFormat
20
+ from shotgun.llm_proxy import BudgetInfo, LiteLLMProxyClient
19
21
  from shotgun.logging_config import get_logger
20
22
 
21
23
  app = typer.Typer(
@@ -108,4 +110,45 @@ async def analyze_context() -> ContextAnalysisOutput:
108
110
  markdown = ContextFormatter.format_markdown(analysis)
109
111
  json_data = ContextFormatter.format_json(analysis)
110
112
 
113
+ # Add budget info for Shotgun Account users
114
+ if model_config.is_shotgun_account:
115
+ try:
116
+ logger.debug("Fetching budget info for Shotgun Account")
117
+ client = LiteLLMProxyClient(model_config.api_key)
118
+ budget_info = await client.get_budget_info()
119
+
120
+ # Format budget section for markdown
121
+ budget_markdown = _format_budget_markdown(budget_info)
122
+ markdown = f"{markdown}\n\n{budget_markdown}"
123
+
124
+ # Add budget info to JSON using Pydantic model
125
+ json_data["budget"] = budget_info.model_dump()
126
+ logger.debug("Successfully added budget info to context output")
127
+
128
+ except httpx.HTTPError as e:
129
+ logger.warning(f"Failed to fetch budget info: {e}")
130
+ # Don't fail the entire command if budget fetch fails
131
+ except Exception as e:
132
+ logger.warning(f"Unexpected error fetching budget info: {e}")
133
+ # Don't fail the entire command if budget fetch fails
134
+
111
135
  return ContextAnalysisOutput(markdown=markdown, json_data=json_data)
136
+
137
+
138
+ def _format_budget_markdown(budget_info: BudgetInfo) -> str:
139
+ """Format budget information as markdown.
140
+
141
+ Args:
142
+ budget_info: BudgetInfo instance
143
+
144
+ Returns:
145
+ Formatted markdown string
146
+ """
147
+ source_label = "Key" if budget_info.source == "key" else "Team"
148
+
149
+ return f"""## Shotgun Account Budget
150
+
151
+ * Max Budget: ${budget_info.max_budget:.2f}
152
+ * Current Spend: ${budget_info.spend:.2f}
153
+ * Remaining: ${budget_info.remaining:.2f} ({100 - budget_info.percentage_used:.1f}%)
154
+ * Budget Source: {source_label}-level"""
@@ -0,0 +1,24 @@
1
+ """CLI-specific error handling utilities.
2
+
3
+ This module provides utilities for displaying agent errors in the CLI
4
+ by printing formatted messages to the console.
5
+ """
6
+
7
+ from rich.console import Console
8
+
9
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
10
+
11
+ console = Console(stderr=True)
12
+
13
+
14
+ def print_agent_error(exception: ErrorNotPickedUpBySentry) -> None:
15
+ """Print an agent error to the console in yellow.
16
+
17
+ Args:
18
+ exception: The error exception with formatting methods
19
+ """
20
+ # Get plain text version for CLI
21
+ message = exception.to_plain_text()
22
+
23
+ # Print with yellow styling
24
+ console.print(message, style="yellow")
shotgun/cli/export.py CHANGED
@@ -11,7 +11,10 @@ from shotgun.agents.export import (
11
11
  run_export_agent,
12
12
  )
13
13
  from shotgun.agents.models import AgentRuntimeOptions
14
+ from shotgun.cli.error_handler import print_agent_error
15
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
14
16
  from shotgun.logging_config import get_logger
17
+ from shotgun.posthog_telemetry import track_event
15
18
 
16
19
  app = typer.Typer(
17
20
  name="export", help="Export artifacts to various formats with agentic approach"
@@ -45,37 +48,34 @@ def export(
45
48
 
46
49
  logger.info("📤 Export Instruction: %s", instruction)
47
50
 
48
- try:
49
- # Track export command usage
50
- from shotgun.posthog_telemetry import track_event
51
-
52
- track_event(
53
- "export_command",
54
- {
55
- "non_interactive": non_interactive,
56
- "provider": provider.value if provider else "default",
57
- },
58
- )
59
-
60
- # Create agent dependencies
61
- agent_runtime_options = AgentRuntimeOptions(
62
- interactive_mode=not non_interactive
63
- )
64
-
65
- # Create the export agent with deps and provider
66
- agent, deps = asyncio.run(create_export_agent(agent_runtime_options, provider))
67
-
68
- # Start export process
69
- logger.info("🎯 Starting export...")
70
- result = asyncio.run(run_export_agent(agent, instruction, deps))
71
-
72
- # Display results
73
- logger.info("✅ Export Complete!")
74
- logger.info("📤 Results:")
75
- logger.info("%s", result.output)
76
-
77
- except Exception as e:
78
- logger.error("❌ Error during export: %s", str(e))
79
- import traceback
80
-
81
- logger.debug("Full traceback:\n%s", traceback.format_exc())
51
+ # Track export command usage
52
+ track_event(
53
+ "export_command",
54
+ {
55
+ "non_interactive": non_interactive,
56
+ "provider": provider.value if provider else "default",
57
+ },
58
+ )
59
+
60
+ # Create agent dependencies
61
+ agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
62
+
63
+ # Create the export agent with deps and provider
64
+ agent, deps = asyncio.run(create_export_agent(agent_runtime_options, provider))
65
+
66
+ # Start export process with error handling
67
+ logger.info("🎯 Starting export...")
68
+
69
+ async def async_export() -> None:
70
+ try:
71
+ result = await run_export_agent(agent, instruction, deps)
72
+ logger.info(" Export Complete!")
73
+ logger.info("📤 Results:")
74
+ logger.info("%s", result.output)
75
+ except ErrorNotPickedUpBySentry as e:
76
+ print_agent_error(e)
77
+ except Exception as e:
78
+ logger.exception("Unexpected error in export command")
79
+ print(f"⚠️ An unexpected error occurred: {str(e)}")
80
+
81
+ asyncio.run(async_export())
shotgun/cli/plan.py CHANGED
@@ -8,7 +8,10 @@ import typer
8
8
  from shotgun.agents.config import ProviderType
9
9
  from shotgun.agents.models import AgentRuntimeOptions
10
10
  from shotgun.agents.plan import create_plan_agent, run_plan_agent
11
+ from shotgun.cli.error_handler import print_agent_error
12
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
11
13
  from shotgun.logging_config import get_logger
14
+ from shotgun.posthog_telemetry import track_event
12
15
 
13
16
  app = typer.Typer(name="plan", help="Generate structured plans", no_args_is_help=True)
14
17
  logger = get_logger(__name__)
@@ -37,37 +40,34 @@ def plan(
37
40
 
38
41
  logger.info("📋 Planning Goal: %s", goal)
39
42
 
40
- try:
41
- # Track plan command usage
42
- from shotgun.posthog_telemetry import track_event
43
-
44
- track_event(
45
- "plan_command",
46
- {
47
- "non_interactive": non_interactive,
48
- "provider": provider.value if provider else "default",
49
- },
50
- )
51
-
52
- # Create agent dependencies
53
- agent_runtime_options = AgentRuntimeOptions(
54
- interactive_mode=not non_interactive
55
- )
56
-
57
- # Create the plan agent with deps and provider
58
- agent, deps = asyncio.run(create_plan_agent(agent_runtime_options, provider))
59
-
60
- # Start planning process
61
- logger.info("🎯 Starting planning...")
62
- result = asyncio.run(run_plan_agent(agent, goal, deps))
63
-
64
- # Display results
65
- logger.info("✅ Planning Complete!")
66
- logger.info("📋 Results:")
67
- logger.info("%s", result.output)
68
-
69
- except Exception as e:
70
- logger.error("❌ Error during planning: %s", str(e))
71
- import traceback
72
-
73
- logger.debug("Full traceback:\n%s", traceback.format_exc())
43
+ # Track plan command usage
44
+ track_event(
45
+ "plan_command",
46
+ {
47
+ "non_interactive": non_interactive,
48
+ "provider": provider.value if provider else "default",
49
+ },
50
+ )
51
+
52
+ # Create agent dependencies
53
+ agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
54
+
55
+ # Create the plan agent with deps and provider
56
+ agent, deps = asyncio.run(create_plan_agent(agent_runtime_options, provider))
57
+
58
+ # Start planning process with error handling
59
+ logger.info("🎯 Starting planning...")
60
+
61
+ async def async_plan() -> None:
62
+ try:
63
+ result = await run_plan_agent(agent, goal, deps)
64
+ logger.info(" Planning Complete!")
65
+ logger.info("📋 Results:")
66
+ logger.info("%s", result.output)
67
+ except ErrorNotPickedUpBySentry as e:
68
+ print_agent_error(e)
69
+ except Exception as e:
70
+ logger.exception("Unexpected error in plan command")
71
+ print(f"⚠️ An unexpected error occurred: {str(e)}")
72
+
73
+ asyncio.run(async_plan())
shotgun/cli/research.py CHANGED
@@ -11,7 +11,10 @@ from shotgun.agents.research import (
11
11
  create_research_agent,
12
12
  run_research_agent,
13
13
  )
14
+ from shotgun.cli.error_handler import print_agent_error
15
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
14
16
  from shotgun.logging_config import get_logger
17
+ from shotgun.posthog_telemetry import track_event
15
18
 
16
19
  app = typer.Typer(
17
20
  name="research", help="Perform research with agentic loops", no_args_is_help=True
@@ -59,8 +62,6 @@ async def async_research(
59
62
  ) -> None:
60
63
  """Async wrapper for research process."""
61
64
  # Track research command usage
62
- from shotgun.posthog_telemetry import track_event
63
-
64
65
  track_event(
65
66
  "research_command",
66
67
  {
@@ -75,11 +76,18 @@ async def async_research(
75
76
  # Create the research agent with deps and provider
76
77
  agent, deps = await create_research_agent(agent_runtime_options, provider)
77
78
 
78
- # Start research process
79
+ # Start research process with error handling
79
80
  logger.info("🔬 Starting research...")
80
- result = await run_research_agent(agent, query, deps)
81
-
82
- # Display results
83
- print("✅ Research Complete!")
84
- print("📋 Findings:")
85
- print(result.output)
81
+ try:
82
+ result = await run_research_agent(agent, query, deps)
83
+ # Display results
84
+ print("✅ Research Complete!")
85
+ print("📋 Findings:")
86
+ print(result.output)
87
+ except ErrorNotPickedUpBySentry as e:
88
+ # All user-actionable errors - display with plain text
89
+ print_agent_error(e)
90
+ except Exception as e:
91
+ # Unexpected errors that weren't wrapped (shouldn't happen)
92
+ logger.exception("Unexpected error in research command")
93
+ print(f"⚠️ An unexpected error occurred: {str(e)}")
@@ -0,0 +1,5 @@
1
+ """Spec CLI module."""
2
+
3
+ from .commands import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1,81 @@
1
+ """Backup utility for .shotgun/ directory before pulling specs."""
2
+
3
+ import shutil
4
+ import zipfile
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from shotgun.logging_config import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+ # Backup directory location
13
+ BACKUP_DIR = Path.home() / ".shotgun-sh" / "backups"
14
+
15
+
16
+ async def create_backup(shotgun_dir: Path) -> str | None:
17
+ """Create a zip backup of the .shotgun/ directory.
18
+
19
+ Creates a timestamped backup at ~/.shotgun-sh/backups/{YYYYMMDD_HHMMSS}.zip.
20
+ Only creates backup if the directory exists and has content.
21
+
22
+ Args:
23
+ shotgun_dir: Path to the .shotgun/ directory to backup
24
+
25
+ Returns:
26
+ Path to the backup file as string, or None if no backup was created
27
+ (e.g., directory doesn't exist or is empty)
28
+
29
+ Raises:
30
+ Exception: If backup creation fails (caller should handle)
31
+ """
32
+ # Check if directory exists and has content
33
+ if not shotgun_dir.exists():
34
+ logger.debug("No .shotgun/ directory to backup")
35
+ return None
36
+
37
+ files_to_backup = list(shotgun_dir.rglob("*"))
38
+ if not any(f.is_file() for f in files_to_backup):
39
+ logger.debug(".shotgun/ directory is empty, skipping backup")
40
+ return None
41
+
42
+ # Create backup directory if needed
43
+ BACKUP_DIR.mkdir(parents=True, exist_ok=True)
44
+
45
+ # Generate timestamp-based filename
46
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
47
+ backup_path = BACKUP_DIR / f"{timestamp}.zip"
48
+
49
+ logger.info("Creating backup of .shotgun/ at %s", backup_path)
50
+
51
+ # Create zip file
52
+ with zipfile.ZipFile(backup_path, "w", zipfile.ZIP_DEFLATED) as zipf:
53
+ for file_path in files_to_backup:
54
+ if file_path.is_file():
55
+ # Store with path relative to shotgun_dir
56
+ arcname = file_path.relative_to(shotgun_dir)
57
+ zipf.write(file_path, arcname)
58
+ logger.debug("Added to backup: %s", arcname)
59
+
60
+ logger.info("Backup created successfully: %s", backup_path)
61
+ return str(backup_path)
62
+
63
+
64
+ def clear_shotgun_dir(shotgun_dir: Path) -> None:
65
+ """Clear all contents of the .shotgun/ directory.
66
+
67
+ Removes all files and subdirectories but keeps the .shotgun/ directory itself.
68
+
69
+ Args:
70
+ shotgun_dir: Path to the .shotgun/ directory to clear
71
+ """
72
+ if not shotgun_dir.exists():
73
+ return
74
+
75
+ for item in shotgun_dir.iterdir():
76
+ if item.is_dir():
77
+ shutil.rmtree(item)
78
+ else:
79
+ item.unlink()
80
+
81
+ logger.debug("Cleared contents of %s", shotgun_dir)