shotgun-sh 0.2.8.dev2__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 (175) hide show
  1. shotgun/agents/agent_manager.py +382 -60
  2. shotgun/agents/common.py +15 -9
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/constants.py +0 -6
  6. shotgun/agents/config/manager.py +383 -82
  7. shotgun/agents/config/models.py +122 -18
  8. shotgun/agents/config/provider.py +81 -15
  9. shotgun/agents/config/streaming_test.py +119 -0
  10. shotgun/agents/context_analyzer/__init__.py +28 -0
  11. shotgun/agents/context_analyzer/analyzer.py +475 -0
  12. shotgun/agents/context_analyzer/constants.py +9 -0
  13. shotgun/agents/context_analyzer/formatter.py +115 -0
  14. shotgun/agents/context_analyzer/models.py +212 -0
  15. shotgun/agents/conversation/__init__.py +18 -0
  16. shotgun/agents/conversation/filters.py +164 -0
  17. shotgun/agents/conversation/history/chunking.py +278 -0
  18. shotgun/agents/{history → conversation/history}/compaction.py +36 -5
  19. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  20. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  21. shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
  22. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
  23. shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
  24. shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
  25. shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
  26. shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
  27. shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
  28. shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
  29. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
  30. shotgun/agents/error/__init__.py +11 -0
  31. shotgun/agents/error/models.py +19 -0
  32. shotgun/agents/export.py +2 -2
  33. shotgun/agents/plan.py +2 -2
  34. shotgun/agents/research.py +3 -3
  35. shotgun/agents/runner.py +230 -0
  36. shotgun/agents/specify.py +2 -2
  37. shotgun/agents/tasks.py +2 -2
  38. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  39. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  40. shotgun/agents/tools/codebase/file_read.py +11 -2
  41. shotgun/agents/tools/codebase/query_graph.py +6 -0
  42. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  43. shotgun/agents/tools/file_management.py +27 -7
  44. shotgun/agents/tools/registry.py +217 -0
  45. shotgun/agents/tools/web_search/__init__.py +8 -8
  46. shotgun/agents/tools/web_search/anthropic.py +8 -2
  47. shotgun/agents/tools/web_search/gemini.py +7 -1
  48. shotgun/agents/tools/web_search/openai.py +8 -2
  49. shotgun/agents/tools/web_search/utils.py +2 -2
  50. shotgun/agents/usage_manager.py +16 -11
  51. shotgun/api_endpoints.py +7 -3
  52. shotgun/build_constants.py +2 -2
  53. shotgun/cli/clear.py +53 -0
  54. shotgun/cli/compact.py +188 -0
  55. shotgun/cli/config.py +8 -5
  56. shotgun/cli/context.py +154 -0
  57. shotgun/cli/error_handler.py +24 -0
  58. shotgun/cli/export.py +34 -34
  59. shotgun/cli/feedback.py +4 -2
  60. shotgun/cli/models.py +1 -0
  61. shotgun/cli/plan.py +34 -34
  62. shotgun/cli/research.py +18 -10
  63. shotgun/cli/spec/__init__.py +5 -0
  64. shotgun/cli/spec/backup.py +81 -0
  65. shotgun/cli/spec/commands.py +132 -0
  66. shotgun/cli/spec/models.py +48 -0
  67. shotgun/cli/spec/pull_service.py +219 -0
  68. shotgun/cli/specify.py +20 -19
  69. shotgun/cli/tasks.py +34 -34
  70. shotgun/cli/update.py +16 -2
  71. shotgun/codebase/core/change_detector.py +5 -3
  72. shotgun/codebase/core/code_retrieval.py +4 -2
  73. shotgun/codebase/core/ingestor.py +163 -15
  74. shotgun/codebase/core/manager.py +13 -4
  75. shotgun/codebase/core/nl_query.py +1 -1
  76. shotgun/codebase/models.py +2 -0
  77. shotgun/exceptions.py +357 -0
  78. shotgun/llm_proxy/__init__.py +17 -0
  79. shotgun/llm_proxy/client.py +215 -0
  80. shotgun/llm_proxy/models.py +137 -0
  81. shotgun/logging_config.py +60 -27
  82. shotgun/main.py +77 -11
  83. shotgun/posthog_telemetry.py +38 -29
  84. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
  85. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  86. shotgun/prompts/agents/plan.j2 +16 -0
  87. shotgun/prompts/agents/research.j2 +16 -3
  88. shotgun/prompts/agents/specify.j2 +54 -1
  89. shotgun/prompts/agents/state/system_state.j2 +0 -2
  90. shotgun/prompts/agents/tasks.j2 +16 -0
  91. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  92. shotgun/prompts/history/combine_summaries.j2 +53 -0
  93. shotgun/sdk/codebase.py +14 -3
  94. shotgun/sentry_telemetry.py +163 -16
  95. shotgun/settings.py +243 -0
  96. shotgun/shotgun_web/__init__.py +67 -1
  97. shotgun/shotgun_web/client.py +42 -1
  98. shotgun/shotgun_web/constants.py +46 -0
  99. shotgun/shotgun_web/exceptions.py +29 -0
  100. shotgun/shotgun_web/models.py +390 -0
  101. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  102. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  103. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  104. shotgun/shotgun_web/shared_specs/models.py +71 -0
  105. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  106. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  107. shotgun/shotgun_web/specs_client.py +703 -0
  108. shotgun/shotgun_web/supabase_client.py +31 -0
  109. shotgun/telemetry.py +10 -33
  110. shotgun/tui/app.py +310 -46
  111. shotgun/tui/commands/__init__.py +1 -1
  112. shotgun/tui/components/context_indicator.py +179 -0
  113. shotgun/tui/components/mode_indicator.py +70 -0
  114. shotgun/tui/components/status_bar.py +48 -0
  115. shotgun/tui/containers.py +91 -0
  116. shotgun/tui/dependencies.py +39 -0
  117. shotgun/tui/layout.py +5 -0
  118. shotgun/tui/protocols.py +45 -0
  119. shotgun/tui/screens/chat/__init__.py +5 -0
  120. shotgun/tui/screens/chat/chat.tcss +54 -0
  121. shotgun/tui/screens/chat/chat_screen.py +1531 -0
  122. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
  123. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  124. shotgun/tui/screens/chat/help_text.py +40 -0
  125. shotgun/tui/screens/chat/prompt_history.py +48 -0
  126. shotgun/tui/screens/chat.tcss +11 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +91 -4
  128. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  129. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  130. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  131. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  132. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  133. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  134. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  135. shotgun/tui/screens/confirmation_dialog.py +191 -0
  136. shotgun/tui/screens/directory_setup.py +45 -41
  137. shotgun/tui/screens/feedback.py +14 -7
  138. shotgun/tui/screens/github_issue.py +111 -0
  139. shotgun/tui/screens/model_picker.py +77 -32
  140. shotgun/tui/screens/onboarding.py +580 -0
  141. shotgun/tui/screens/pipx_migration.py +205 -0
  142. shotgun/tui/screens/provider_config.py +116 -35
  143. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  144. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  145. shotgun/tui/screens/shared_specs/models.py +56 -0
  146. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  147. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  148. shotgun/tui/screens/shotgun_auth.py +112 -18
  149. shotgun/tui/screens/spec_pull.py +288 -0
  150. shotgun/tui/screens/welcome.py +137 -11
  151. shotgun/tui/services/__init__.py +5 -0
  152. shotgun/tui/services/conversation_service.py +187 -0
  153. shotgun/tui/state/__init__.py +7 -0
  154. shotgun/tui/state/processing_state.py +185 -0
  155. shotgun/tui/utils/mode_progress.py +14 -7
  156. shotgun/tui/widgets/__init__.py +5 -0
  157. shotgun/tui/widgets/widget_coordinator.py +263 -0
  158. shotgun/utils/file_system_utils.py +22 -2
  159. shotgun/utils/marketing.py +110 -0
  160. shotgun/utils/update_checker.py +69 -14
  161. shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
  162. shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
  163. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  164. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
  165. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
  166. shotgun/tui/screens/chat.py +0 -996
  167. shotgun/tui/screens/chat_screen/history.py +0 -335
  168. shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
  169. shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
  170. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  171. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  172. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  173. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  174. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  175. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
shotgun/cli/clear.py ADDED
@@ -0,0 +1,53 @@
1
+ """Clear command for shotgun CLI."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from shotgun.agents.conversation import ConversationManager
10
+ from shotgun.logging_config import get_logger
11
+
12
+ app = typer.Typer(
13
+ name="clear", help="Clear the conversation history", no_args_is_help=False
14
+ )
15
+ logger = get_logger(__name__)
16
+ console = Console()
17
+
18
+
19
+ @app.callback(invoke_without_command=True)
20
+ def clear() -> None:
21
+ """Clear the current conversation history.
22
+
23
+ This command deletes the conversation file at ~/.shotgun-sh/conversation.json,
24
+ removing all conversation history. Other files in ~/.shotgun-sh/ (config, usage,
25
+ codebases, logs) are preserved.
26
+ """
27
+ try:
28
+ # Get conversation file path
29
+ conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
30
+
31
+ # Check if file exists
32
+ if not conversation_file.exists():
33
+ console.print(
34
+ "[yellow]No conversation file found.[/yellow] Nothing to clear.",
35
+ style="bold",
36
+ )
37
+ return
38
+
39
+ # Clear the conversation
40
+ manager = ConversationManager(conversation_file)
41
+ asyncio.run(manager.clear())
42
+
43
+ console.print(
44
+ "[green]✓[/green] Conversation cleared successfully", style="bold"
45
+ )
46
+ logger.info("Conversation cleared successfully")
47
+
48
+ except Exception as e:
49
+ console.print(
50
+ f"[red]Error:[/red] Failed to clear conversation: {e}", style="bold"
51
+ )
52
+ logger.debug("Full traceback:", exc_info=True)
53
+ raise typer.Exit(code=1) from e
shotgun/cli/compact.py ADDED
@@ -0,0 +1,188 @@
1
+ """Compact command for shotgun CLI."""
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+ from pydantic_ai.usage import RequestUsage
10
+ from rich.console import Console
11
+
12
+ from shotgun.agents.config import get_provider_model
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
+ )
18
+ from shotgun.cli.models import OutputFormat
19
+ from shotgun.logging_config import get_logger
20
+
21
+ app = typer.Typer(
22
+ name="compact", help="Compact the conversation history", 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 compact(
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
+ """Compact the current conversation history to reduce size.
40
+
41
+ This command compacts the conversation in ~/.shotgun-sh/conversation.json
42
+ by summarizing older messages while preserving recent context. The compacted
43
+ conversation is automatically saved back to the file.
44
+ """
45
+ try:
46
+ result = asyncio.run(compact_conversation())
47
+
48
+ if format == OutputFormat.JSON:
49
+ # Output as JSON
50
+ console.print_json(json.dumps(result, indent=2))
51
+ else:
52
+ # Output as markdown
53
+ console.print(format_markdown(result))
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(
64
+ f"[red]Error:[/red] Failed to compact conversation: {e}", style="bold"
65
+ )
66
+ logger.debug("Full traceback:", exc_info=True)
67
+ raise typer.Exit(code=1) from e
68
+
69
+
70
+ async def compact_conversation() -> dict[str, Any]:
71
+ """Compact the conversation and return statistics.
72
+
73
+ Returns:
74
+ Dictionary with compaction statistics including before/after metrics
75
+ """
76
+ # Get conversation file path
77
+ conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
78
+
79
+ if not conversation_file.exists():
80
+ raise FileNotFoundError(f"Conversation file not found at {conversation_file}")
81
+
82
+ # Load conversation
83
+ manager = ConversationManager(conversation_file)
84
+ conversation = await manager.load()
85
+
86
+ if not conversation:
87
+ raise ValueError("Conversation file is empty or corrupted")
88
+
89
+ # Get agent messages only (not UI messages)
90
+ agent_messages = conversation.get_agent_messages()
91
+
92
+ if not agent_messages:
93
+ raise ValueError("No agent messages found in conversation")
94
+
95
+ # Get model config
96
+ model_config = await get_provider_model()
97
+
98
+ # Calculate before metrics
99
+ original_message_count = len(agent_messages)
100
+ original_tokens = await estimate_tokens_from_messages(agent_messages, model_config)
101
+
102
+ # For CLI, we can call token_limit_compactor directly without full AgentDeps
103
+ # since we only need the model config and message history
104
+ # Create a minimal context object for compaction
105
+ class CompactContext:
106
+ def __init__(self, model_config: Any, usage: RequestUsage) -> None:
107
+ self.deps = type("Deps", (), {"llm_model": model_config})()
108
+ self.usage = usage
109
+
110
+ # Create minimal usage info for compaction check
111
+ usage = RequestUsage(input_tokens=original_tokens, output_tokens=0)
112
+ ctx = CompactContext(model_config, usage)
113
+
114
+ # Apply compaction with force=True to bypass threshold checks
115
+ compacted_messages = await token_limit_compactor(ctx, agent_messages, force=True)
116
+
117
+ # Calculate after metrics
118
+ compacted_message_count = len(compacted_messages)
119
+ compacted_tokens = await estimate_tokens_from_messages(
120
+ compacted_messages, model_config
121
+ )
122
+
123
+ # Calculate reduction percentages
124
+ message_reduction = (
125
+ ((original_message_count - compacted_message_count) / original_message_count)
126
+ * 100
127
+ if original_message_count > 0
128
+ else 0
129
+ )
130
+ token_reduction = (
131
+ ((original_tokens - compacted_tokens) / original_tokens) * 100
132
+ if original_tokens > 0
133
+ else 0
134
+ )
135
+
136
+ # Save compacted conversation
137
+ conversation.set_agent_messages(compacted_messages)
138
+ await manager.save(conversation)
139
+
140
+ logger.info(
141
+ f"Compacted conversation: {original_message_count} → {compacted_message_count} messages "
142
+ f"({message_reduction:.1f}% reduction)"
143
+ )
144
+
145
+ return {
146
+ "success": True,
147
+ "before": {
148
+ "messages": original_message_count,
149
+ "estimated_tokens": original_tokens,
150
+ },
151
+ "after": {
152
+ "messages": compacted_message_count,
153
+ "estimated_tokens": compacted_tokens,
154
+ },
155
+ "reduction": {
156
+ "messages_percent": round(message_reduction, 1),
157
+ "tokens_percent": round(token_reduction, 1),
158
+ },
159
+ }
160
+
161
+
162
+ def format_markdown(result: dict[str, Any]) -> str:
163
+ """Format compaction result as markdown.
164
+
165
+ Args:
166
+ result: Dictionary with compaction statistics
167
+
168
+ Returns:
169
+ Formatted markdown string
170
+ """
171
+ before = result["before"]
172
+ after = result["after"]
173
+ reduction = result["reduction"]
174
+
175
+ return f"""# Conversation Compacted ✓
176
+
177
+ ## Before
178
+ - **Messages:** {before["messages"]:,}
179
+ - **Estimated Tokens:** {before["estimated_tokens"]:,}
180
+
181
+ ## After
182
+ - **Messages:** {after["messages"]:,}
183
+ - **Estimated Tokens:** {after["estimated_tokens"]:,}
184
+
185
+ ## Reduction
186
+ - **Messages:** {reduction["messages_percent"]}%
187
+ - **Tokens:** {reduction["tokens_percent"]}%
188
+ """
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
 
@@ -44,7 +45,7 @@ def init(
44
45
  console.print()
45
46
 
46
47
  # Initialize with defaults
47
- config_manager.initialize()
48
+ asyncio.run(config_manager.initialize())
48
49
 
49
50
  # Ask for provider
50
51
  provider_choices = ["openai", "anthropic", "google"]
@@ -76,7 +77,7 @@ def init(
76
77
 
77
78
  if api_key:
78
79
  # update_provider will automatically set selected_model for first provider
79
- config_manager.update_provider(provider, api_key=api_key)
80
+ asyncio.run(config_manager.update_provider(provider, api_key=api_key))
80
81
 
81
82
  console.print(
82
83
  f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
@@ -84,7 +85,7 @@ def init(
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
 
@@ -112,7 +113,7 @@ def set(
112
113
 
113
114
  try:
114
115
  if api_key:
115
- config_manager.update_provider(provider, api_key=api_key)
116
+ asyncio.run(config_manager.update_provider(provider, api_key=api_key))
116
117
 
117
118
  console.print(f"✅ Configuration updated for {provider}")
118
119
 
@@ -133,8 +134,10 @@ def get(
133
134
  ] = False,
134
135
  ) -> None:
135
136
  """Display current configuration."""
137
+ import asyncio
138
+
136
139
  config_manager = get_config_manager()
137
- config = config_manager.load()
140
+ config = asyncio.run(config_manager.load())
138
141
 
139
142
  if json_output:
140
143
  # Convert to dict and mask secrets
shotgun/cli/context.py ADDED
@@ -0,0 +1,154 @@
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 httpx
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from shotgun.agents.config import get_provider_model
13
+ from shotgun.agents.context_analyzer import (
14
+ ContextAnalysisOutput,
15
+ ContextAnalyzer,
16
+ ContextFormatter,
17
+ )
18
+ from shotgun.agents.conversation import ConversationManager
19
+ from shotgun.cli.models import OutputFormat
20
+ from shotgun.llm_proxy import BudgetInfo, LiteLLMProxyClient
21
+ from shotgun.logging_config import get_logger
22
+
23
+ app = typer.Typer(
24
+ name="context", help="Analyze conversation context usage", no_args_is_help=False
25
+ )
26
+ logger = get_logger(__name__)
27
+ console = Console()
28
+
29
+
30
+ @app.callback(invoke_without_command=True)
31
+ def context(
32
+ format: Annotated[
33
+ OutputFormat,
34
+ typer.Option(
35
+ "--format",
36
+ "-f",
37
+ help="Output format: markdown or json",
38
+ ),
39
+ ] = OutputFormat.MARKDOWN,
40
+ ) -> None:
41
+ """Analyze the current conversation's context usage.
42
+
43
+ This command analyzes the agent's message history from ~/.shotgun-sh/conversation.json
44
+ and displays token usage breakdown by message type. Only agent context is counted
45
+ (UI elements like hints are excluded).
46
+ """
47
+ try:
48
+ result = asyncio.run(analyze_context())
49
+
50
+ if format == OutputFormat.JSON:
51
+ # Output as JSON
52
+ console.print_json(json.dumps(result.json_data, indent=2))
53
+ else:
54
+ # Output as plain text (Markdown() reformats and makes categories inline)
55
+ console.print(result.markdown)
56
+
57
+ except FileNotFoundError as e:
58
+ console.print(
59
+ f"[red]Error:[/red] {e}\n\n"
60
+ "No conversation found. Start a TUI session first with: [cyan]shotgun[/cyan]",
61
+ style="bold",
62
+ )
63
+ raise typer.Exit(code=1) from e
64
+ except Exception as e:
65
+ console.print(f"[red]Error:[/red] Failed to analyze context: {e}", style="bold")
66
+ logger.debug("Full traceback:", exc_info=True)
67
+ raise typer.Exit(code=1) from e
68
+
69
+
70
+ async def analyze_context() -> ContextAnalysisOutput:
71
+ """Analyze the conversation context and return structured data.
72
+
73
+ Returns:
74
+ ContextAnalysisOutput with both markdown and JSON representations of the analysis
75
+ """
76
+ # Get conversation file path
77
+ conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
78
+
79
+ if not conversation_file.exists():
80
+ raise FileNotFoundError(f"Conversation file not found at {conversation_file}")
81
+
82
+ # Load conversation
83
+ manager = ConversationManager(conversation_file)
84
+ conversation = await manager.load()
85
+
86
+ if not conversation:
87
+ raise ValueError("Conversation file is empty or corrupted")
88
+
89
+ # Get agent messages only (not UI messages)
90
+ agent_messages = conversation.get_agent_messages()
91
+
92
+ if not agent_messages:
93
+ raise ValueError("No agent messages found in conversation")
94
+
95
+ # Get model config (use default provider settings)
96
+ model_config = await get_provider_model()
97
+
98
+ # Debug: Log the model being used
99
+ logger.debug(f"Using model: {model_config.name.value}")
100
+ logger.debug(f"Provider: {model_config.provider.value}")
101
+ logger.debug(f"Key provider: {model_config.key_provider.value}")
102
+ logger.debug(f"Max input tokens: {model_config.max_input_tokens}")
103
+
104
+ # Analyze with ContextAnalyzer
105
+ analyzer = ContextAnalyzer(model_config)
106
+ # For CLI, agent_messages and ui_message_history are the same (no hints in CLI mode)
107
+ analysis = await analyzer.analyze_conversation(agent_messages, list(agent_messages))
108
+
109
+ # Use formatter to generate markdown and JSON
110
+ markdown = ContextFormatter.format_markdown(analysis)
111
+ json_data = ContextFormatter.format_json(analysis)
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
+
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 = 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/feedback.py CHANGED
@@ -28,9 +28,11 @@ def send_feedback(
28
28
  ],
29
29
  ) -> None:
30
30
  """Initialize Shotgun configuration."""
31
+ import asyncio
32
+
31
33
  config_manager = get_config_manager()
32
- config_manager.load()
33
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
34
+ asyncio.run(config_manager.load())
35
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
34
36
 
35
37
  if not description:
36
38
  console.print(
shotgun/cli/models.py CHANGED
@@ -8,3 +8,4 @@ class OutputFormat(StrEnum):
8
8
 
9
9
  TEXT = "text"
10
10
  JSON = "json"
11
+ MARKDOWN = "markdown"
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 = 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())