shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (161) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +41 -0
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -16,6 +16,8 @@ from rich.progress import (
16
16
  TimeElapsedColumn,
17
17
  )
18
18
 
19
+ from shotgun.codebase.benchmarks import BenchmarkRunner, MetricsExporter, get_formatter
20
+ from shotgun.codebase.benchmarks.formatters import MetricsDisplayOptions
19
21
  from shotgun.codebase.models import (
20
22
  CodebaseGraph,
21
23
  IndexProgress,
@@ -69,81 +71,195 @@ def index(
69
71
  format_type: Annotated[
70
72
  OutputFormat, typer.Option("--format", "-f", help="Output format")
71
73
  ] = OutputFormat.TEXT,
74
+ # Benchmark flags
75
+ benchmark: Annotated[
76
+ bool,
77
+ typer.Option("--benchmark", help="Enable benchmark mode with detailed metrics"),
78
+ ] = False,
79
+ iterations: Annotated[
80
+ int,
81
+ typer.Option(
82
+ "--benchmark-iterations",
83
+ help="Number of benchmark runs (requires --benchmark)",
84
+ ),
85
+ ] = 1,
86
+ warmup: Annotated[
87
+ int,
88
+ typer.Option(
89
+ "--benchmark-warmup", help="Number of warmup runs (requires --benchmark)"
90
+ ),
91
+ ] = 0,
92
+ benchmark_output: Annotated[
93
+ str,
94
+ typer.Option(
95
+ "--benchmark-output",
96
+ help="Benchmark output format: json|markdown",
97
+ ),
98
+ ] = "json",
99
+ benchmark_export: Annotated[
100
+ str | None,
101
+ typer.Option("--benchmark-export", help="Export metrics to file"),
102
+ ] = None,
103
+ show_files: Annotated[
104
+ bool,
105
+ typer.Option("--show-files", help="Show per-file metrics in benchmark output"),
106
+ ] = False,
107
+ show_workers: Annotated[
108
+ bool,
109
+ typer.Option(
110
+ "--show-workers", help="Show per-worker metrics in benchmark output"
111
+ ),
112
+ ] = False,
113
+ top_n: Annotated[
114
+ int | None,
115
+ typer.Option("--top-n", help="Show N slowest files (requires --benchmark)"),
116
+ ] = None,
117
+ sequential: Annotated[
118
+ bool,
119
+ typer.Option(
120
+ "--sequential", help="Force sequential mode (disable parallelization)"
121
+ ),
122
+ ] = False,
72
123
  ) -> None:
73
- """Index a new codebase."""
74
- sdk = CodebaseSDK()
124
+ """Index a new codebase.
125
+
126
+ By default, runs with a TUI progress display. Use --benchmark for detailed
127
+ metrics reporting.
128
+ """
75
129
  console = Console()
76
130
 
77
- # Create progress display
78
- progress = Progress(
79
- SpinnerColumn(),
80
- TextColumn("[bold blue]{task.description}"),
81
- BarColumn(),
82
- TaskProgressColumn(),
83
- TimeElapsedColumn(),
84
- console=console,
85
- )
86
-
87
- # Track tasks by phase
88
- tasks = {}
89
-
90
- def progress_callback(progress_info: IndexProgress) -> None:
91
- """Update progress display based on indexing phase."""
92
- phase = progress_info.phase
93
-
94
- # Create task if it doesn't exist
95
- if phase not in tasks:
96
- if progress_info.total is not None:
97
- tasks[phase] = progress.add_task(
98
- progress_info.phase_name, total=progress_info.total
99
- )
100
- else:
101
- # Indeterminate progress (spinner only)
102
- tasks[phase] = progress.add_task(progress_info.phase_name, total=None)
103
-
104
- task_id = tasks[phase]
105
-
106
- # Update task
107
- if progress_info.total is not None:
108
- progress.update(
109
- task_id,
110
- completed=progress_info.current,
111
- total=progress_info.total,
112
- description=f"[bold blue]{progress_info.phase_name}",
131
+ # Validate path
132
+ repo_path = Path(path).resolve()
133
+ if not repo_path.exists():
134
+ error_result = ErrorResult(error_message=f"Path does not exist: {repo_path}")
135
+ output_result(error_result, format_type)
136
+ raise typer.Exit(1)
137
+
138
+ # Benchmark mode
139
+ if benchmark:
140
+ try:
141
+ # Create and run benchmark
142
+ runner = BenchmarkRunner(
143
+ codebase_path=repo_path,
144
+ codebase_name=name,
145
+ iterations=iterations,
146
+ warmup_iterations=warmup,
147
+ parallel=not sequential,
148
+ collect_file_metrics=show_files or top_n is not None,
149
+ collect_worker_metrics=show_workers,
150
+ )
151
+
152
+ console.print(f"\n[bold blue]Starting benchmark: {name}[/bold blue]")
153
+ console.print(f"Path: {repo_path}")
154
+ console.print(f"Mode: {'Sequential' if sequential else 'Parallel'}")
155
+ console.print(f"Iterations: {iterations} ({warmup} warmup)")
156
+ console.print()
157
+
158
+ results = asyncio.run(runner.run())
159
+
160
+ # Format output
161
+ formatter = get_formatter(benchmark_output)
162
+ options = MetricsDisplayOptions(
163
+ show_phase_metrics=True,
164
+ show_worker_metrics=show_workers,
165
+ show_file_metrics=show_files or top_n is not None,
166
+ top_n_files=top_n,
113
167
  )
114
- else:
115
- # Just update description for indeterminate tasks
116
- progress.update(
117
- task_id,
118
- description=f"[bold blue]{progress_info.phase_name} ({progress_info.current} items)",
168
+ output = formatter.format_results(results, options)
169
+ console.print(output)
170
+
171
+ # Export if requested
172
+ if benchmark_export:
173
+ exporter = MetricsExporter()
174
+ exporter.export(results, Path(benchmark_export), options=options)
175
+ console.print(
176
+ f"\n[green]Metrics exported to: {benchmark_export}[/green]"
177
+ )
178
+
179
+ except Exception as e:
180
+ error_result = ErrorResult(
181
+ error_message=f"Benchmark error: {e}",
182
+ details=f"Full traceback:\n{traceback.format_exc()}",
119
183
  )
184
+ output_result(error_result, format_type)
185
+ raise typer.Exit(1) from e
186
+
187
+ else:
188
+ # Normal mode with TUI progress display
189
+ sdk = CodebaseSDK()
190
+
191
+ # Create progress display
192
+ progress = Progress(
193
+ SpinnerColumn(),
194
+ TextColumn("[bold blue]{task.description}"),
195
+ BarColumn(),
196
+ TaskProgressColumn(),
197
+ TimeElapsedColumn(),
198
+ console=console,
199
+ )
200
+
201
+ # Track tasks by phase
202
+ tasks = {}
120
203
 
121
- # Mark as complete if phase is done
122
- if progress_info.phase_complete:
204
+ def progress_callback(progress_info: IndexProgress) -> None:
205
+ """Update progress display based on indexing phase."""
206
+ phase = progress_info.phase
207
+
208
+ # Create task if it doesn't exist
209
+ if phase not in tasks:
210
+ if progress_info.total is not None:
211
+ tasks[phase] = progress.add_task(
212
+ progress_info.phase_name, total=progress_info.total
213
+ )
214
+ else:
215
+ # Indeterminate progress (spinner only)
216
+ tasks[phase] = progress.add_task(
217
+ progress_info.phase_name, total=None
218
+ )
219
+
220
+ task_id = tasks[phase]
221
+
222
+ # Update task
123
223
  if progress_info.total is not None:
124
- progress.update(task_id, completed=progress_info.total)
224
+ progress.update(
225
+ task_id,
226
+ completed=progress_info.current,
227
+ total=progress_info.total,
228
+ description=f"[bold blue]{progress_info.phase_name}",
229
+ )
230
+ else:
231
+ # Just update description for indeterminate tasks
232
+ progress.update(
233
+ task_id,
234
+ description=f"[bold blue]{progress_info.phase_name} ({progress_info.current} items)",
235
+ )
125
236
 
126
- try:
127
- repo_path = Path(path)
237
+ # Mark as complete if phase is done
238
+ if progress_info.phase_complete:
239
+ if progress_info.total is not None:
240
+ progress.update(task_id, completed=progress_info.total)
241
+
242
+ try:
243
+ # Run indexing with progress display
244
+ with progress:
245
+ result = asyncio.run(
246
+ sdk.index_codebase(
247
+ repo_path, name, progress_callback=progress_callback
248
+ )
249
+ )
128
250
 
129
- # Run indexing with progress display
130
- with progress:
131
- result = asyncio.run(
132
- sdk.index_codebase(repo_path, name, progress_callback=progress_callback)
251
+ output_result(result, format_type)
252
+ except InvalidPathError as e:
253
+ error_result = ErrorResult(error_message=str(e))
254
+ output_result(error_result, format_type)
255
+ raise typer.Exit(1) from e
256
+ except Exception as e:
257
+ error_result = ErrorResult(
258
+ error_message=f"Error indexing codebase: {e}",
259
+ details=f"Full traceback:\n{traceback.format_exc()}",
133
260
  )
134
-
135
- output_result(result, format_type)
136
- except InvalidPathError as e:
137
- error_result = ErrorResult(error_message=str(e))
138
- output_result(error_result, format_type)
139
- raise typer.Exit(1) from e
140
- except Exception as e:
141
- error_result = ErrorResult(
142
- error_message=f"Error indexing codebase: {e}",
143
- details=f"Full traceback:\n{traceback.format_exc()}",
144
- )
145
- output_result(error_result, format_type)
146
- raise typer.Exit(1) from e
261
+ output_result(error_result, format_type)
262
+ raise typer.Exit(1) from e
147
263
 
148
264
 
149
265
  @app.command()
shotgun/cli/compact.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
- from pathlib import Path
6
5
  from typing import Annotated, Any
7
6
 
8
7
  import typer
@@ -17,6 +16,7 @@ from shotgun.agents.conversation.history.token_estimation import (
17
16
  )
18
17
  from shotgun.cli.models import OutputFormat
19
18
  from shotgun.logging_config import get_logger
19
+ from shotgun.utils import get_shotgun_home
20
20
 
21
21
  app = typer.Typer(
22
22
  name="compact", help="Compact the conversation history", no_args_is_help=False
@@ -74,7 +74,7 @@ async def compact_conversation() -> dict[str, Any]:
74
74
  Dictionary with compaction statistics including before/after metrics
75
75
  """
76
76
  # Get conversation file path
77
- conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
77
+ conversation_file = get_shotgun_home() / "conversation.json"
78
78
 
79
79
  if not conversation_file.exists():
80
80
  raise FileNotFoundError(f"Conversation file not found at {conversation_file}")
shotgun/cli/context.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
- from pathlib import Path
6
5
  from typing import Annotated
7
6
 
8
7
  import httpx
@@ -19,6 +18,7 @@ from shotgun.agents.conversation import ConversationManager
19
18
  from shotgun.cli.models import OutputFormat
20
19
  from shotgun.llm_proxy import BudgetInfo, LiteLLMProxyClient
21
20
  from shotgun.logging_config import get_logger
21
+ from shotgun.utils import get_shotgun_home
22
22
 
23
23
  app = typer.Typer(
24
24
  name="context", help="Analyze conversation context usage", no_args_is_help=False
@@ -74,7 +74,7 @@ async def analyze_context() -> ContextAnalysisOutput:
74
74
  ContextAnalysisOutput with both markdown and JSON representations of the analysis
75
75
  """
76
76
  # Get conversation file path
77
- conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
77
+ conversation_file = get_shotgun_home() / "conversation.json"
78
78
 
79
79
  if not conversation_file.exists():
80
80
  raise FileNotFoundError(f"Conversation file not found at {conversation_file}")
shotgun/cli/run.py ADDED
@@ -0,0 +1,90 @@
1
+ """Run command for shotgun CLI - executes prompts using the Router agent."""
2
+
3
+ import asyncio
4
+ import traceback
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from shotgun.agents.config import ProviderType
10
+ from shotgun.agents.models import AgentRuntimeOptions
11
+ from shotgun.agents.router import (
12
+ RouterMode,
13
+ create_router_agent,
14
+ run_router_agent,
15
+ )
16
+ from shotgun.cli.error_handler import print_agent_error
17
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
18
+ from shotgun.logging_config import get_logger
19
+ from shotgun.posthog_telemetry import track_event
20
+
21
+ app = typer.Typer(
22
+ name="run", help="Run a prompt using the Router agent", no_args_is_help=True
23
+ )
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ def run(
29
+ prompt: Annotated[str, typer.Argument(help="The prompt to execute")],
30
+ non_interactive: Annotated[
31
+ bool,
32
+ typer.Option(
33
+ "--non-interactive", "-n", help="Disable user interaction (for CI/CD)"
34
+ ),
35
+ ] = False,
36
+ provider: Annotated[
37
+ ProviderType | None,
38
+ typer.Option("--provider", "-p", help="AI provider to use (overrides default)"),
39
+ ] = None,
40
+ ) -> None:
41
+ """Execute a prompt using the Router agent in drafting mode.
42
+
43
+ The Router agent orchestrates sub-agents (Research, Specify, Plan, Tasks, Export)
44
+ based on your prompt. In drafting mode, it auto-executes without confirmation.
45
+ """
46
+ logger.info("Running prompt: %s", prompt[:100])
47
+
48
+ try:
49
+ asyncio.run(async_run(prompt, non_interactive, provider))
50
+ except Exception as e:
51
+ logger.error("Error during execution: %s", str(e))
52
+ logger.debug("Full traceback:\n%s", traceback.format_exc())
53
+
54
+
55
+ async def async_run(
56
+ prompt: str,
57
+ non_interactive: bool,
58
+ provider: ProviderType | None = None,
59
+ ) -> None:
60
+ """Async implementation of the run command."""
61
+ track_event(
62
+ "run_command",
63
+ {
64
+ "non_interactive": non_interactive,
65
+ "provider": provider.value if provider else "default",
66
+ },
67
+ )
68
+
69
+ # Create agent runtime options
70
+ agent_runtime_options = AgentRuntimeOptions(
71
+ interactive_mode=not non_interactive,
72
+ )
73
+
74
+ # Create the router agent
75
+ agent, deps = await create_router_agent(agent_runtime_options, provider)
76
+
77
+ # Set drafting mode for CLI (auto-execute without confirmation)
78
+ deps.router_mode = RouterMode.DRAFTING
79
+
80
+ logger.info("Starting Router agent in drafting mode...")
81
+ try:
82
+ result = await run_router_agent(agent, prompt, deps)
83
+ print("Complete!")
84
+ print("Response:")
85
+ print(result.output)
86
+ except ErrorNotPickedUpBySentry as e:
87
+ print_agent_error(e)
88
+ except Exception as e:
89
+ logger.exception("Unexpected error in run command")
90
+ print(f"An unexpected error occurred: {str(e)}")
@@ -6,11 +6,12 @@ from datetime import datetime, timezone
6
6
  from pathlib import Path
7
7
 
8
8
  from shotgun.logging_config import get_logger
9
+ from shotgun.utils import get_shotgun_home
9
10
 
10
11
  logger = get_logger(__name__)
11
12
 
12
13
  # Backup directory location
13
- BACKUP_DIR = Path.home() / ".shotgun-sh" / "backups"
14
+ BACKUP_DIR = get_shotgun_home() / "backups"
14
15
 
15
16
 
16
17
  async def create_backup(shotgun_dir: Path) -> str | None:
@@ -16,6 +16,7 @@ from shotgun.shotgun_web.exceptions import (
16
16
  from shotgun.tui import app as tui_app
17
17
  from shotgun.utils.file_system_utils import get_shotgun_base_path
18
18
 
19
+ from .models import PullSource
19
20
  from .pull_service import CancelledError, PullProgress, SpecPullService
20
21
 
21
22
  app = typer.Typer(
@@ -94,6 +95,7 @@ async def _async_pull(version_id: str) -> bool:
94
95
  version_id=version_id,
95
96
  shotgun_dir=shotgun_dir,
96
97
  on_progress=on_progress,
98
+ source=PullSource.CLI,
97
99
  )
98
100
 
99
101
  if result.success:
@@ -1,10 +1,28 @@
1
1
  """Pydantic models for spec CLI commands."""
2
2
 
3
3
  from datetime import datetime
4
+ from enum import StrEnum
4
5
 
5
6
  from pydantic import BaseModel, Field
6
7
 
7
8
 
9
+ class PullSource(StrEnum):
10
+ """Source of spec pull operation for analytics."""
11
+
12
+ CLI = "cli"
13
+ TUI = "tui"
14
+
15
+
16
+ class PullPhase(StrEnum):
17
+ """Phases during spec pull operation for analytics."""
18
+
19
+ STARTING = "starting"
20
+ FETCHING = "fetching"
21
+ BACKUP = "backup"
22
+ DOWNLOADING = "downloading"
23
+ FINALIZING = "finalizing"
24
+
25
+
8
26
  class SpecMeta(BaseModel):
9
27
  """Metadata stored in .shotgun/meta.json after pulling a spec.
10
28