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.
- shotgun/agents/agent_manager.py +382 -60
- shotgun/agents/common.py +15 -9
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +383 -82
- shotgun/agents/config/models.py +122 -18
- shotgun/agents/config/provider.py +81 -15
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +475 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- 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 +36 -5
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
- shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
- shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
- shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +2 -2
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +8 -2
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +188 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +154 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +18 -10
- 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/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +163 -15
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +357 -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 +60 -27
- shotgun/main.py +77 -11
- shotgun/posthog_telemetry.py +38 -29
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +243 -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/telemetry.py +10 -33
- shotgun/tui/app.py +310 -46
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1531 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +91 -4
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +191 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +14 -7
- shotgun/tui/screens/github_issue.py +111 -0
- shotgun/tui/screens/model_picker.py +77 -32
- shotgun/tui/screens/onboarding.py +580 -0
- shotgun/tui/screens/pipx_migration.py +205 -0
- shotgun/tui/screens/provider_config.py +116 -35
- 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 +112 -18
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +137 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +187 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- /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_estimation.py +0 -0
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
|
{
|
|
@@ -73,13 +74,20 @@ async def async_research(
|
|
|
73
74
|
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
74
75
|
|
|
75
76
|
# Create the research agent with deps and provider
|
|
76
|
-
agent, deps = create_research_agent(agent_runtime_options, provider)
|
|
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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Pydantic models for spec CLI commands."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
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
|
+
|
|
26
|
+
class SpecMeta(BaseModel):
|
|
27
|
+
"""Metadata stored in .shotgun/meta.json after pulling a spec.
|
|
28
|
+
|
|
29
|
+
This file tracks the source of the local spec files and is used
|
|
30
|
+
by the TUI to display version information and enable future sync operations.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
version_id: str = Field(description="Pulled version UUID")
|
|
34
|
+
spec_id: str = Field(description="Spec UUID")
|
|
35
|
+
spec_name: str = Field(description="Spec name at time of pull")
|
|
36
|
+
workspace_id: str = Field(description="Workspace UUID")
|
|
37
|
+
is_latest: bool = Field(
|
|
38
|
+
description="Whether this was the latest version when pulled"
|
|
39
|
+
)
|
|
40
|
+
pulled_at: datetime = Field(description="Timestamp when spec was pulled (UTC)")
|
|
41
|
+
backup_path: str | None = Field(
|
|
42
|
+
default=None,
|
|
43
|
+
description="Path where previous .shotgun/ files were backed up",
|
|
44
|
+
)
|
|
45
|
+
web_url: str | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
description="URL to view this version in the web UI",
|
|
48
|
+
)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Shared spec pull service for CLI and TUI."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from shotgun.logging_config import get_logger
|
|
10
|
+
from shotgun.posthog_telemetry import track_event
|
|
11
|
+
from shotgun.shotgun_web.specs_client import SpecsClient
|
|
12
|
+
from shotgun.shotgun_web.supabase_client import download_file_from_url
|
|
13
|
+
|
|
14
|
+
from .backup import clear_shotgun_dir, create_backup
|
|
15
|
+
from .models import PullPhase, PullSource, SpecMeta
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PullProgress:
|
|
22
|
+
"""Progress update during spec pull."""
|
|
23
|
+
|
|
24
|
+
phase: str
|
|
25
|
+
file_index: int | None = None
|
|
26
|
+
total_files: int | None = None
|
|
27
|
+
current_file: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PullResult:
|
|
32
|
+
"""Result of a spec pull operation."""
|
|
33
|
+
|
|
34
|
+
success: bool
|
|
35
|
+
spec_name: str | None = None
|
|
36
|
+
file_count: int = 0
|
|
37
|
+
backup_path: str | None = None
|
|
38
|
+
web_url: str | None = None
|
|
39
|
+
error: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CancelledError(Exception):
|
|
43
|
+
"""Raised when pull is cancelled."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SpecPullService:
|
|
47
|
+
"""Service for pulling spec versions from cloud."""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._client = SpecsClient()
|
|
51
|
+
|
|
52
|
+
async def pull_version(
|
|
53
|
+
self,
|
|
54
|
+
version_id: str,
|
|
55
|
+
shotgun_dir: Path,
|
|
56
|
+
on_progress: Callable[[PullProgress], None] | None = None,
|
|
57
|
+
is_cancelled: Callable[[], bool] | None = None,
|
|
58
|
+
source: PullSource = PullSource.CLI,
|
|
59
|
+
) -> PullResult:
|
|
60
|
+
"""Pull a spec version to the local directory.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
version_id: The version UUID to pull
|
|
64
|
+
shotgun_dir: Target directory (typically .shotgun/)
|
|
65
|
+
on_progress: Optional callback for progress updates
|
|
66
|
+
is_cancelled: Optional callback to check if cancelled
|
|
67
|
+
source: Source of the pull request (CLI or TUI)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
PullResult with success status and details
|
|
71
|
+
"""
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
current_phase: PullPhase = PullPhase.STARTING
|
|
74
|
+
track_event("spec_pull_started", {"source": source.value})
|
|
75
|
+
|
|
76
|
+
def report(
|
|
77
|
+
phase: str,
|
|
78
|
+
file_index: int | None = None,
|
|
79
|
+
total_files: int | None = None,
|
|
80
|
+
current_file: str | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
if on_progress:
|
|
83
|
+
on_progress(
|
|
84
|
+
PullProgress(
|
|
85
|
+
phase=phase,
|
|
86
|
+
file_index=file_index,
|
|
87
|
+
total_files=total_files,
|
|
88
|
+
current_file=current_file,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def check_cancelled() -> None:
|
|
93
|
+
nonlocal current_phase
|
|
94
|
+
if is_cancelled and is_cancelled():
|
|
95
|
+
track_event(
|
|
96
|
+
"spec_pull_cancelled",
|
|
97
|
+
{"source": source.value, "phase": current_phase.value},
|
|
98
|
+
)
|
|
99
|
+
raise CancelledError()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Phase 1: Fetch version metadata
|
|
103
|
+
current_phase = PullPhase.FETCHING
|
|
104
|
+
report("Fetching version info...")
|
|
105
|
+
check_cancelled()
|
|
106
|
+
|
|
107
|
+
response = await self._client.get_version_with_files(version_id)
|
|
108
|
+
spec_name = response.spec_name
|
|
109
|
+
files = response.files
|
|
110
|
+
|
|
111
|
+
if not files:
|
|
112
|
+
track_event(
|
|
113
|
+
"spec_pull_failed",
|
|
114
|
+
{
|
|
115
|
+
"source": source.value,
|
|
116
|
+
"error_type": "EmptyVersion",
|
|
117
|
+
"phase": current_phase.value,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
return PullResult(
|
|
121
|
+
success=False,
|
|
122
|
+
spec_name=spec_name,
|
|
123
|
+
error="No files in this version.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Phase 2: Backup existing content
|
|
127
|
+
current_phase = PullPhase.BACKUP
|
|
128
|
+
backup_path: str | None = None
|
|
129
|
+
if shotgun_dir.exists():
|
|
130
|
+
report("Backing up existing files...")
|
|
131
|
+
check_cancelled()
|
|
132
|
+
|
|
133
|
+
backup_path = await create_backup(shotgun_dir)
|
|
134
|
+
if backup_path:
|
|
135
|
+
clear_shotgun_dir(shotgun_dir)
|
|
136
|
+
|
|
137
|
+
# Ensure directory exists
|
|
138
|
+
shotgun_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
# Phase 3: Download files
|
|
141
|
+
current_phase = PullPhase.DOWNLOADING
|
|
142
|
+
total_files = len(files)
|
|
143
|
+
total_bytes = 0
|
|
144
|
+
for idx, file_info in enumerate(files):
|
|
145
|
+
check_cancelled()
|
|
146
|
+
|
|
147
|
+
report(
|
|
148
|
+
f"Downloading files ({idx + 1}/{total_files})...",
|
|
149
|
+
file_index=idx,
|
|
150
|
+
total_files=total_files,
|
|
151
|
+
current_file=file_info.relative_path,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if not file_info.download_url:
|
|
155
|
+
logger.warning(
|
|
156
|
+
"Skipping file without download URL: %s",
|
|
157
|
+
file_info.relative_path,
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
content = await download_file_from_url(file_info.download_url)
|
|
162
|
+
total_bytes += file_info.size_bytes
|
|
163
|
+
|
|
164
|
+
local_path = shotgun_dir / file_info.relative_path
|
|
165
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
local_path.write_bytes(content)
|
|
167
|
+
|
|
168
|
+
# Phase 4: Write meta.json
|
|
169
|
+
current_phase = PullPhase.FINALIZING
|
|
170
|
+
report("Finalizing...")
|
|
171
|
+
check_cancelled()
|
|
172
|
+
|
|
173
|
+
meta = SpecMeta(
|
|
174
|
+
version_id=response.version.id,
|
|
175
|
+
spec_id=response.spec_id,
|
|
176
|
+
spec_name=response.spec_name,
|
|
177
|
+
workspace_id=response.workspace_id,
|
|
178
|
+
is_latest=response.version.is_latest,
|
|
179
|
+
pulled_at=datetime.now(timezone.utc),
|
|
180
|
+
backup_path=backup_path,
|
|
181
|
+
web_url=response.web_url,
|
|
182
|
+
)
|
|
183
|
+
meta_path = shotgun_dir / "meta.json"
|
|
184
|
+
meta_path.write_text(meta.model_dump_json(indent=2))
|
|
185
|
+
|
|
186
|
+
# Track successful completion
|
|
187
|
+
duration = time.time() - start_time
|
|
188
|
+
track_event(
|
|
189
|
+
"spec_pull_completed",
|
|
190
|
+
{
|
|
191
|
+
"source": source.value,
|
|
192
|
+
"file_count": total_files,
|
|
193
|
+
"total_bytes": total_bytes,
|
|
194
|
+
"duration_seconds": round(duration, 2),
|
|
195
|
+
"had_backup": backup_path is not None,
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return PullResult(
|
|
200
|
+
success=True,
|
|
201
|
+
spec_name=spec_name,
|
|
202
|
+
file_count=total_files,
|
|
203
|
+
backup_path=backup_path,
|
|
204
|
+
web_url=response.web_url,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
except CancelledError:
|
|
208
|
+
# Already tracked in check_cancelled()
|
|
209
|
+
raise
|
|
210
|
+
except Exception as e:
|
|
211
|
+
track_event(
|
|
212
|
+
"spec_pull_failed",
|
|
213
|
+
{
|
|
214
|
+
"source": source.value,
|
|
215
|
+
"error_type": type(e).__name__,
|
|
216
|
+
"phase": current_phase.value,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
raise
|
shotgun/cli/specify.py
CHANGED
|
@@ -11,6 +11,8 @@ from shotgun.agents.specify import (
|
|
|
11
11
|
create_specify_agent,
|
|
12
12
|
run_specify_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
|
|
15
17
|
|
|
16
18
|
app = typer.Typer(
|
|
@@ -44,26 +46,25 @@ def specify(
|
|
|
44
46
|
|
|
45
47
|
logger.info("📝 Specification Requirement: %s", requirement)
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
agent_runtime_options = AgentRuntimeOptions(
|
|
50
|
-
interactive_mode=not non_interactive
|
|
51
|
-
)
|
|
49
|
+
# Create agent dependencies
|
|
50
|
+
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
# Create the specify agent with deps and provider
|
|
53
|
+
agent, deps = asyncio.run(create_specify_agent(agent_runtime_options, provider))
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
result = asyncio.run(run_specify_agent(agent, requirement, deps))
|
|
55
|
+
# Start specification process with error handling
|
|
56
|
+
logger.info("📋 Starting specification generation...")
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
async def async_specify() -> None:
|
|
59
|
+
try:
|
|
60
|
+
result = await run_specify_agent(agent, requirement, deps)
|
|
61
|
+
logger.info("✅ Specification Complete!")
|
|
62
|
+
logger.info("📋 Results:")
|
|
63
|
+
logger.info("%s", result.output)
|
|
64
|
+
except ErrorNotPickedUpBySentry as e:
|
|
65
|
+
print_agent_error(e)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.exception("Unexpected error in specify command")
|
|
68
|
+
print(f"⚠️ An unexpected error occurred: {str(e)}")
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
logger.error("❌ Error during specification: %s", str(e))
|
|
67
|
-
import traceback
|
|
68
|
-
|
|
69
|
-
logger.debug("Full traceback:\n%s", traceback.format_exc())
|
|
70
|
+
asyncio.run(async_specify())
|
shotgun/cli/tasks.py
CHANGED
|
@@ -11,7 +11,10 @@ from shotgun.agents.tasks import (
|
|
|
11
11
|
create_tasks_agent,
|
|
12
12
|
run_tasks_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(name="tasks", help="Generate task lists with agentic approach")
|
|
17
20
|
logger = get_logger(__name__)
|
|
@@ -42,37 +45,34 @@ def tasks(
|
|
|
42
45
|
|
|
43
46
|
logger.info("📋 Task Creation Instruction: %s", instruction)
|
|
44
47
|
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
import traceback
|
|
77
|
-
|
|
78
|
-
logger.debug("Full traceback:\n%s", traceback.format_exc())
|
|
48
|
+
# Track tasks command usage
|
|
49
|
+
track_event(
|
|
50
|
+
"tasks_command",
|
|
51
|
+
{
|
|
52
|
+
"non_interactive": non_interactive,
|
|
53
|
+
"provider": provider.value if provider else "default",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Create agent dependencies
|
|
58
|
+
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
59
|
+
|
|
60
|
+
# Create the tasks agent with deps and provider
|
|
61
|
+
agent, deps = asyncio.run(create_tasks_agent(agent_runtime_options, provider))
|
|
62
|
+
|
|
63
|
+
# Start task creation process with error handling
|
|
64
|
+
logger.info("🎯 Starting task creation...")
|
|
65
|
+
|
|
66
|
+
async def async_tasks() -> None:
|
|
67
|
+
try:
|
|
68
|
+
result = await run_tasks_agent(agent, instruction, deps)
|
|
69
|
+
logger.info("✅ Task Creation Complete!")
|
|
70
|
+
logger.info("📋 Results:")
|
|
71
|
+
logger.info("%s", result.output)
|
|
72
|
+
except ErrorNotPickedUpBySentry as e:
|
|
73
|
+
print_agent_error(e)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.exception("Unexpected error in tasks command")
|
|
76
|
+
print(f"⚠️ An unexpected error occurred: {str(e)}")
|
|
77
|
+
|
|
78
|
+
asyncio.run(async_tasks())
|