shotgun-sh 0.2.17__py3-none-any.whl → 0.4.0.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.
- shotgun/agents/agent_manager.py +219 -37
- shotgun/agents/common.py +79 -78
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +364 -53
- shotgun/agents/config/models.py +101 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +12 -13
- shotgun/agents/models.py +66 -1
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +376 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +503 -0
- shotgun/agents/router/tools/plan_tools.py +322 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/file_management.py +49 -1
- shotgun/agents/tools/registry.py +2 -0
- 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 +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/codebase/core/change_detector.py +1 -1
- shotgun/codebase/core/ingestor.py +154 -8
- shotgun/codebase/core/manager.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
- shotgun/prompts/agents/plan.j2 +29 -1
- shotgun/prompts/agents/research.j2 +75 -23
- shotgun/prompts/agents/router.j2 +440 -0
- shotgun/prompts/agents/specify.j2 +80 -4
- shotgun/prompts/agents/state/system_state.j2 +15 -8
- shotgun/prompts/agents/tasks.j2 +63 -23
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/tui/app.py +78 -15
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/status_bar.py +2 -2
- shotgun/tui/containers.py +1 -1
- shotgun/tui/dependencies.py +64 -9
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +9 -1
- shotgun/tui/screens/chat/chat_screen.py +1015 -106
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -89
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +179 -26
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- 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 +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
- shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
- shotgun_sh-0.2.17.dist-info/RECORD +0 -194
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -23,7 +23,10 @@ logger = get_logger(__name__)
|
|
|
23
23
|
# - A list of Paths: multiple allowed files/directories (e.g., [Path("specification.md"), Path("contracts")])
|
|
24
24
|
# - "*": any file except protected files (for export agent)
|
|
25
25
|
AGENT_DIRECTORIES: dict[AgentType, str | Path | list[Path]] = {
|
|
26
|
-
AgentType.RESEARCH:
|
|
26
|
+
AgentType.RESEARCH: [
|
|
27
|
+
Path("research.md"),
|
|
28
|
+
Path("research"),
|
|
29
|
+
], # Research can write main file and research folder
|
|
27
30
|
AgentType.SPECIFY: [
|
|
28
31
|
Path("specification.md"),
|
|
29
32
|
Path("contracts"),
|
|
@@ -282,3 +285,48 @@ async def append_file(ctx: RunContext[AgentDeps], filename: str, content: str) -
|
|
|
282
285
|
Success message or error message
|
|
283
286
|
"""
|
|
284
287
|
return await write_file(ctx, filename, content, mode="a")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@register_tool(
|
|
291
|
+
category=ToolCategory.ARTIFACT_MANAGEMENT,
|
|
292
|
+
display_text="Deleting file",
|
|
293
|
+
key_arg="filename",
|
|
294
|
+
)
|
|
295
|
+
async def delete_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
296
|
+
"""Delete a file from the .shotgun directory.
|
|
297
|
+
|
|
298
|
+
Uses the same permission model as write_file - agents can only delete
|
|
299
|
+
files they have permission to write to.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
filename: Relative path to file within .shotgun directory
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Success message or error message
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
ValueError: If path is outside .shotgun directory or agent lacks permission
|
|
309
|
+
FileNotFoundError: If file does not exist
|
|
310
|
+
"""
|
|
311
|
+
logger.debug("🔧 Deleting file: %s", filename)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
# Use agent-scoped validation (same as write_file)
|
|
315
|
+
file_path = _validate_agent_scoped_path(filename, ctx.deps.agent_mode)
|
|
316
|
+
|
|
317
|
+
if not await aiofiles.os.path.exists(file_path):
|
|
318
|
+
raise FileNotFoundError(f"File not found: {filename}")
|
|
319
|
+
|
|
320
|
+
# Delete the file
|
|
321
|
+
await aiofiles.os.remove(file_path)
|
|
322
|
+
logger.debug("🗑️ Deleted file: %s", filename)
|
|
323
|
+
|
|
324
|
+
# Track the file operation
|
|
325
|
+
ctx.deps.file_tracker.add_operation(file_path, FileOperationType.DELETED)
|
|
326
|
+
|
|
327
|
+
return f"Successfully deleted {filename}"
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
error_msg = f"Error deleting file '{filename}': {str(e)}"
|
|
331
|
+
logger.error("❌ File delete failed: %s", error_msg)
|
|
332
|
+
return error_msg
|
shotgun/agents/tools/registry.py
CHANGED
|
@@ -44,9 +44,8 @@ async def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
|
44
44
|
# Check if using Shotgun Account
|
|
45
45
|
config_manager = get_config_manager()
|
|
46
46
|
config = await config_manager.load()
|
|
47
|
-
has_shotgun_key = config.shotgun.api_key is not None
|
|
48
47
|
|
|
49
|
-
if
|
|
48
|
+
if config.shotgun.has_valid_account:
|
|
50
49
|
logger.debug("🔑 Shotgun Account - only Gemini web search available")
|
|
51
50
|
|
|
52
51
|
# Gemini: Only search tool available for Shotgun Account
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Gemini web search tool implementation."""
|
|
2
2
|
|
|
3
3
|
from opentelemetry import trace
|
|
4
|
-
from pydantic_ai.messages import ModelMessage, ModelRequest
|
|
4
|
+
from pydantic_ai.messages import ModelMessage, ModelRequest, TextPart
|
|
5
5
|
from pydantic_ai.settings import ModelSettings
|
|
6
6
|
|
|
7
7
|
from shotgun.agents.config import get_provider_model
|
|
@@ -82,8 +82,6 @@ async def gemini_web_search_tool(query: str) -> str:
|
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
# Extract text from response
|
|
85
|
-
from pydantic_ai.messages import TextPart
|
|
86
|
-
|
|
87
85
|
result_text = "No content returned from search"
|
|
88
86
|
if response.parts:
|
|
89
87
|
for part in response.parts:
|
|
@@ -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(
|
|
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}]}
|
shotgun/build_constants.py
CHANGED
|
@@ -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.
|
|
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.
|
|
14
|
-
from shotgun.agents.history.history_processors import token_limit_compactor
|
|
15
|
-
from shotgun.agents.history.token_estimation import
|
|
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.
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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,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)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Spec management commands for shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn
|
|
9
|
+
|
|
10
|
+
from shotgun.logging_config import get_logger
|
|
11
|
+
from shotgun.shotgun_web.exceptions import (
|
|
12
|
+
ForbiddenError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
UnauthorizedError,
|
|
15
|
+
)
|
|
16
|
+
from shotgun.tui import app as tui_app
|
|
17
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
18
|
+
|
|
19
|
+
from .models import PullSource
|
|
20
|
+
from .pull_service import CancelledError, PullProgress, SpecPullService
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="spec",
|
|
24
|
+
help="Manage shared specifications",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def pull(
|
|
33
|
+
version_id: Annotated[str, typer.Argument(help="Version ID to pull")],
|
|
34
|
+
no_tui: Annotated[
|
|
35
|
+
bool,
|
|
36
|
+
typer.Option("--no-tui", help="Run in CLI-only mode (requires existing auth)"),
|
|
37
|
+
] = False,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Pull a spec version from the cloud to local .shotgun/ directory.
|
|
40
|
+
|
|
41
|
+
Downloads all files for the specified version and writes them to the
|
|
42
|
+
local .shotgun/ directory. If the directory already has content, it
|
|
43
|
+
will be backed up to ~/.shotgun-sh/backups/ before being replaced.
|
|
44
|
+
|
|
45
|
+
By default, launches the TUI which handles authentication and shows
|
|
46
|
+
the pull progress. Use --no-tui for scripted/headless use (requires
|
|
47
|
+
existing authentication).
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
shotgun spec pull 2532e1c7-7068-4d23-9379-58ea439c592f
|
|
51
|
+
"""
|
|
52
|
+
if no_tui:
|
|
53
|
+
# CLI-only mode: do pull directly (requires existing auth)
|
|
54
|
+
success = asyncio.run(_async_pull(version_id))
|
|
55
|
+
if not success:
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
else:
|
|
58
|
+
# TUI mode: launch TUI which handles auth and pull
|
|
59
|
+
tui_app.run(pull_version_id=version_id)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _async_pull(version_id: str) -> bool:
|
|
63
|
+
"""Async implementation of spec pull command.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if pull was successful, False otherwise.
|
|
67
|
+
"""
|
|
68
|
+
shotgun_dir = get_shotgun_base_path()
|
|
69
|
+
service = SpecPullService()
|
|
70
|
+
|
|
71
|
+
# Track current progress state for rich display
|
|
72
|
+
current_task_id: TaskID | None = None
|
|
73
|
+
progress_ctx: Progress | None = None
|
|
74
|
+
|
|
75
|
+
def on_progress(p: PullProgress) -> None:
|
|
76
|
+
nonlocal current_task_id, progress_ctx
|
|
77
|
+
# For CLI, we just update the description - progress bar handled by result
|
|
78
|
+
if progress_ctx and current_task_id is not None:
|
|
79
|
+
progress_ctx.update(current_task_id, description=p.phase)
|
|
80
|
+
if p.total_files and p.file_index is not None:
|
|
81
|
+
pct = ((p.file_index + 1) / p.total_files) * 100
|
|
82
|
+
progress_ctx.update(current_task_id, completed=pct)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
with Progress(
|
|
86
|
+
SpinnerColumn(),
|
|
87
|
+
TextColumn("[progress.description]{task.description}"),
|
|
88
|
+
BarColumn(),
|
|
89
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
90
|
+
) as progress:
|
|
91
|
+
progress_ctx = progress
|
|
92
|
+
current_task_id = progress.add_task("Starting...", total=100)
|
|
93
|
+
|
|
94
|
+
result = await service.pull_version(
|
|
95
|
+
version_id=version_id,
|
|
96
|
+
shotgun_dir=shotgun_dir,
|
|
97
|
+
on_progress=on_progress,
|
|
98
|
+
source=PullSource.CLI,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if result.success:
|
|
102
|
+
console.print()
|
|
103
|
+
console.print(f"[green]Successfully pulled '{result.spec_name}'[/green]")
|
|
104
|
+
console.print(f" [dim]Files downloaded:[/dim] {result.file_count}")
|
|
105
|
+
if result.backup_path:
|
|
106
|
+
console.print(f" [dim]Previous backup:[/dim] {result.backup_path}")
|
|
107
|
+
if result.web_url:
|
|
108
|
+
console.print(f" [blue]View in browser:[/blue] {result.web_url}")
|
|
109
|
+
return True
|
|
110
|
+
else:
|
|
111
|
+
console.print(f"[red]Error: {result.error}[/red]")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
except UnauthorizedError:
|
|
115
|
+
console.print(
|
|
116
|
+
"[red]Not authenticated. Please re-run the command to login.[/red]"
|
|
117
|
+
)
|
|
118
|
+
raise typer.Exit(1) from None
|
|
119
|
+
except NotFoundError:
|
|
120
|
+
console.print(f"[red]Version not found: {version_id}[/red]")
|
|
121
|
+
console.print("[dim]Check the version ID and try again.[/dim]")
|
|
122
|
+
raise typer.Exit(1) from None
|
|
123
|
+
except ForbiddenError:
|
|
124
|
+
console.print("[red]You don't have access to this spec.[/red]")
|
|
125
|
+
raise typer.Exit(1) from None
|
|
126
|
+
except CancelledError:
|
|
127
|
+
console.print("[yellow]Pull cancelled.[/yellow]")
|
|
128
|
+
raise typer.Exit(1) from None
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.exception("Unexpected error in spec pull")
|
|
131
|
+
console.print(f"[red]Unexpected error: {e}[/red]")
|
|
132
|
+
raise typer.Exit(1) from None
|