shotgun-sh 0.3.3.dev1__py3-none-any.whl → 0.6.2__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.
- shotgun/agents/agent_manager.py +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +21 -27
- shotgun/agents/config/provider.py +44 -27
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +46 -6
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/build_constants.py +4 -7
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/error_handler.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +9 -9
- shotgun/main.py +3 -16
- shotgun/posthog_telemetry.py +165 -24
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -52
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +38 -12
- shotgun/prompts/agents/research.j2 +70 -31
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +53 -16
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -13
- shotgun/prompts/agents/tasks.j2 +72 -34
- shotgun/settings.py +49 -10
- shotgun/tui/app.py +154 -24
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +25 -28
- shotgun/tui/components/status_bar.py +14 -7
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +55 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1376 -213
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +58 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +1 -3
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +88 -35
- shotgun_sh-0.6.2.dist-info/RECORD +291 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/sentry_telemetry.py +0 -232
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +0 -229
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Attachment processor for file validation and encoding.
|
|
2
|
+
|
|
3
|
+
Provides functions to validate file sizes against provider limits
|
|
4
|
+
and encode file contents to base64 for API submission.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import aiofiles
|
|
12
|
+
|
|
13
|
+
from shotgun.agents.config.models import ProviderType
|
|
14
|
+
from shotgun.attachments.errors import (
|
|
15
|
+
cannot_read_file,
|
|
16
|
+
file_not_found,
|
|
17
|
+
file_too_large,
|
|
18
|
+
)
|
|
19
|
+
from shotgun.attachments.models import AttachmentType, FileAttachment
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Provider file size limits in bytes
|
|
24
|
+
PROVIDER_SIZE_LIMITS: dict[ProviderType, int] = {
|
|
25
|
+
ProviderType.OPENAI: 20 * 1024 * 1024, # 20MB
|
|
26
|
+
ProviderType.ANTHROPIC: 32 * 1024 * 1024, # 32MB
|
|
27
|
+
ProviderType.GOOGLE: 4 * 1024 * 1024, # 4MB
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Default limit for unknown providers (most restrictive)
|
|
31
|
+
DEFAULT_SIZE_LIMIT: int = 4 * 1024 * 1024 # 4MB
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_provider_size_limit(provider: ProviderType) -> int:
|
|
35
|
+
"""Get the maximum file size limit for a provider.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
provider: The LLM provider type.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Maximum file size in bytes.
|
|
42
|
+
"""
|
|
43
|
+
return PROVIDER_SIZE_LIMITS.get(provider, DEFAULT_SIZE_LIMIT)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def format_file_size(size_bytes: int) -> str:
|
|
47
|
+
"""Format file size in human-readable format.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
size_bytes: File size in bytes.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Human-readable string (e.g., "2.5 MB", "512 KB", "128 B").
|
|
54
|
+
"""
|
|
55
|
+
if size_bytes < 1024:
|
|
56
|
+
return f"{size_bytes} B"
|
|
57
|
+
elif size_bytes < 1024 * 1024:
|
|
58
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
59
|
+
else:
|
|
60
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_file_size(
|
|
64
|
+
attachment: FileAttachment,
|
|
65
|
+
provider: ProviderType,
|
|
66
|
+
) -> tuple[bool, str | None]:
|
|
67
|
+
"""Validate file size against provider limit.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
attachment: The file attachment to validate.
|
|
71
|
+
provider: The target LLM provider.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple of (is_valid, error_message).
|
|
75
|
+
If valid: (True, None)
|
|
76
|
+
If invalid: (False, "File size {size} exceeds {provider} limit of {limit}")
|
|
77
|
+
"""
|
|
78
|
+
limit = get_provider_size_limit(provider)
|
|
79
|
+
|
|
80
|
+
if attachment.file_size_bytes > limit:
|
|
81
|
+
size_str = format_file_size(attachment.file_size_bytes)
|
|
82
|
+
limit_str = format_file_size(limit)
|
|
83
|
+
provider_name = provider.value.capitalize()
|
|
84
|
+
return (
|
|
85
|
+
False,
|
|
86
|
+
file_too_large(size_str, limit_str, provider_name),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return (True, None)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def encode_file_to_base64(file_path: Path) -> str:
|
|
93
|
+
"""Asynchronously read and encode file contents to base64.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
file_path: Path to the file to encode.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Base64-encoded string of file contents.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
FileNotFoundError: If file does not exist.
|
|
103
|
+
PermissionError: If file cannot be read.
|
|
104
|
+
OSError: If file read fails.
|
|
105
|
+
"""
|
|
106
|
+
async with aiofiles.open(file_path, "rb") as f:
|
|
107
|
+
content = await f.read()
|
|
108
|
+
|
|
109
|
+
if not content:
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
return base64.b64encode(content).decode("utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def process_attachment(
|
|
116
|
+
attachment: FileAttachment,
|
|
117
|
+
provider: ProviderType,
|
|
118
|
+
) -> tuple[FileAttachment, str | None]:
|
|
119
|
+
"""Validate and process an attachment for submission.
|
|
120
|
+
|
|
121
|
+
Validates file size against provider limits and encodes content to base64.
|
|
122
|
+
All supported attachment types (PDF, PNG, JPG, JPEG, GIF, WEBP) work with
|
|
123
|
+
all providers (OpenAI, Anthropic, Google) via BinaryContent.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
attachment: The file attachment to process.
|
|
127
|
+
provider: The target LLM provider.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Tuple of (processed_attachment, error_message).
|
|
131
|
+
If successful: (attachment_with_base64_content, None)
|
|
132
|
+
If failed: (original_attachment, error_message)
|
|
133
|
+
"""
|
|
134
|
+
# Validate file size
|
|
135
|
+
is_valid, error = validate_file_size(attachment, provider)
|
|
136
|
+
if not is_valid:
|
|
137
|
+
return (attachment, error)
|
|
138
|
+
|
|
139
|
+
# Encode file to base64
|
|
140
|
+
try:
|
|
141
|
+
content_base64 = await encode_file_to_base64(attachment.file_path)
|
|
142
|
+
except FileNotFoundError:
|
|
143
|
+
return (attachment, file_not_found(attachment.file_path))
|
|
144
|
+
except PermissionError:
|
|
145
|
+
return (attachment, cannot_read_file(attachment.file_path, "permission denied"))
|
|
146
|
+
except OSError as e:
|
|
147
|
+
logger.warning(f"Failed to read file '{attachment.file_path}': {e}")
|
|
148
|
+
return (attachment, cannot_read_file(attachment.file_path))
|
|
149
|
+
|
|
150
|
+
# Create new attachment with base64 content
|
|
151
|
+
processed = FileAttachment(
|
|
152
|
+
file_path=attachment.file_path,
|
|
153
|
+
file_name=attachment.file_name,
|
|
154
|
+
file_type=attachment.file_type,
|
|
155
|
+
file_size_bytes=attachment.file_size_bytes,
|
|
156
|
+
content_base64=content_base64,
|
|
157
|
+
mime_type=attachment.mime_type,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Processed attachment: {processed.file_name} "
|
|
162
|
+
f"({len(content_base64)} base64 chars)"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return (processed, None)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def create_attachment_hint_display(attachment: FileAttachment) -> str:
|
|
169
|
+
"""Create display string for attachment in chat history.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
attachment: The file attachment.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Formatted display string (e.g., "document.pdf (2.5 MB)").
|
|
176
|
+
"""
|
|
177
|
+
size_str = format_file_size(attachment.file_size_bytes)
|
|
178
|
+
return f"{attachment.file_name} ({size_str})"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_attachment_icon(attachment_type: AttachmentType) -> str:
|
|
182
|
+
"""Get the appropriate icon for an attachment type.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
attachment_type: The type of attachment.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Icon string for display.
|
|
189
|
+
"""
|
|
190
|
+
if attachment_type == AttachmentType.PDF:
|
|
191
|
+
return "\U0001f4c4" # document emoji
|
|
192
|
+
else:
|
|
193
|
+
return "\U0001f5bc\ufe0f" # framed picture emoji
|
shotgun/build_constants.py
CHANGED
|
@@ -4,17 +4,14 @@ This file is auto-generated during the build process.
|
|
|
4
4
|
DO NOT EDIT MANUALLY.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
# Sentry DSN embedded at build time (empty string if not provided)
|
|
8
|
-
SENTRY_DSN = 'https://2818a6d165c64eccc94cfd51ce05d6aa@o4506813296738304.ingest.us.sentry.io/4510045952409600'
|
|
9
|
-
|
|
10
7
|
# PostHog configuration embedded at build time (empty strings if not provided)
|
|
11
8
|
POSTHOG_API_KEY = 'phc_KKnChzZUKeNqZDOTJ6soCBWNQSx3vjiULdwTR9H5Mcr'
|
|
12
9
|
POSTHOG_PROJECT_ID = '191396'
|
|
13
10
|
|
|
14
11
|
# Logfire configuration embedded at build time (only for dev builds)
|
|
15
|
-
LOGFIRE_ENABLED = '
|
|
16
|
-
LOGFIRE_TOKEN = '
|
|
12
|
+
LOGFIRE_ENABLED = ''
|
|
13
|
+
LOGFIRE_TOKEN = ''
|
|
17
14
|
|
|
18
15
|
# Build metadata
|
|
19
|
-
BUILD_TIME_ENV = "production" if
|
|
20
|
-
IS_DEV_BUILD =
|
|
16
|
+
BUILD_TIME_ENV = "production" if POSTHOG_API_KEY else "development"
|
|
17
|
+
IS_DEV_BUILD = False
|
shotgun/cli/clear.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Clear command for shotgun CLI."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
from pathlib import Path
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
from rich.console import Console
|
|
8
7
|
|
|
9
8
|
from shotgun.agents.conversation import ConversationManager
|
|
10
9
|
from shotgun.logging_config import get_logger
|
|
10
|
+
from shotgun.utils import get_shotgun_home
|
|
11
11
|
|
|
12
12
|
app = typer.Typer(
|
|
13
13
|
name="clear", help="Clear the conversation history", no_args_is_help=False
|
|
@@ -26,7 +26,7 @@ def clear() -> None:
|
|
|
26
26
|
"""
|
|
27
27
|
try:
|
|
28
28
|
# Get conversation file path
|
|
29
|
-
conversation_file =
|
|
29
|
+
conversation_file = get_shotgun_home() / "conversation.json"
|
|
30
30
|
|
|
31
31
|
# Check if file exists
|
|
32
32
|
if not conversation_file.exists():
|
shotgun/cli/codebase/commands.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
else
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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(
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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/error_handler.py
CHANGED
|
@@ -6,12 +6,12 @@ by printing formatted messages to the console.
|
|
|
6
6
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
|
-
from shotgun.exceptions import
|
|
9
|
+
from shotgun.exceptions import UserActionableError
|
|
10
10
|
|
|
11
11
|
console = Console(stderr=True)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def print_agent_error(exception:
|
|
14
|
+
def print_agent_error(exception: UserActionableError) -> None:
|
|
15
15
|
"""Print an agent error to the console in yellow.
|
|
16
16
|
|
|
17
17
|
Args:
|
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 UserActionableError
|
|
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 UserActionableError 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)}")
|
shotgun/cli/spec/backup.py
CHANGED
|
@@ -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 =
|
|
14
|
+
BACKUP_DIR = get_shotgun_home() / "backups"
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
async def create_backup(shotgun_dir: Path) -> str | None:
|