shotgun-sh 0.2.9__py3-none-any.whl → 0.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/config/manager.py +2 -2
- shotgun/agents/config/models.py +8 -0
- shotgun/agents/config/provider.py +31 -3
- shotgun/api_endpoints.py +8 -2
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/manager.py +10 -1
- shotgun/main.py +64 -10
- shotgun/tui/app.py +170 -8
- shotgun/tui/screens/chat.py +131 -49
- shotgun/tui/screens/model_picker.py +3 -2
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/utils/update_checker.py +69 -14
- {shotgun_sh-0.2.9.dist-info → shotgun_sh-0.2.10.dist-info}/METADATA +22 -4
- {shotgun_sh-0.2.9.dist-info → shotgun_sh-0.2.10.dist-info}/RECORD +17 -16
- {shotgun_sh-0.2.9.dist-info → shotgun_sh-0.2.10.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.9.dist-info → shotgun_sh-0.2.10.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.9.dist-info → shotgun_sh-0.2.10.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/config/manager.py
CHANGED
|
@@ -142,7 +142,7 @@ class ConfigManager:
|
|
|
142
142
|
# Find default model for this provider
|
|
143
143
|
provider_models = {
|
|
144
144
|
ProviderType.OPENAI: ModelName.GPT_5,
|
|
145
|
-
ProviderType.ANTHROPIC: ModelName.
|
|
145
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_HAIKU_4_5,
|
|
146
146
|
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -243,7 +243,7 @@ class ConfigManager:
|
|
|
243
243
|
|
|
244
244
|
provider_models = {
|
|
245
245
|
ProviderType.OPENAI: ModelName.GPT_5,
|
|
246
|
-
ProviderType.ANTHROPIC: ModelName.
|
|
246
|
+
ProviderType.ANTHROPIC: ModelName.CLAUDE_HAIKU_4_5,
|
|
247
247
|
ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
|
|
248
248
|
}
|
|
249
249
|
if provider_enum in provider_models:
|
shotgun/agents/config/models.py
CHANGED
|
@@ -28,6 +28,7 @@ class ModelName(StrEnum):
|
|
|
28
28
|
GPT_5_MINI = "gpt-5-mini"
|
|
29
29
|
CLAUDE_OPUS_4_1 = "claude-opus-4-1"
|
|
30
30
|
CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
|
|
31
|
+
CLAUDE_HAIKU_4_5 = "claude-haiku-4-5"
|
|
31
32
|
GEMINI_2_5_PRO = "gemini-2.5-pro"
|
|
32
33
|
GEMINI_2_5_FLASH = "gemini-2.5-flash"
|
|
33
34
|
|
|
@@ -110,6 +111,13 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
|
|
|
110
111
|
max_output_tokens=16_000,
|
|
111
112
|
litellm_proxy_model_name="anthropic/claude-sonnet-4-5",
|
|
112
113
|
),
|
|
114
|
+
ModelName.CLAUDE_HAIKU_4_5: ModelSpec(
|
|
115
|
+
name=ModelName.CLAUDE_HAIKU_4_5,
|
|
116
|
+
provider=ProviderType.ANTHROPIC,
|
|
117
|
+
max_input_tokens=200_000,
|
|
118
|
+
max_output_tokens=64_000,
|
|
119
|
+
litellm_proxy_model_name="anthropic/claude-haiku-4-5",
|
|
120
|
+
),
|
|
113
121
|
ModelName.GEMINI_2_5_PRO: ModelSpec(
|
|
114
122
|
name=ModelName.GEMINI_2_5_PRO,
|
|
115
123
|
provider=ProviderType.GOOGLE,
|
|
@@ -32,6 +32,34 @@ logger = get_logger(__name__)
|
|
|
32
32
|
_model_cache: dict[tuple[ProviderType, KeyProvider, ModelName, str], Model] = {}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def get_default_model_for_provider(config: ShotgunConfig) -> ModelName:
|
|
36
|
+
"""Get the default model based on which provider/account is configured.
|
|
37
|
+
|
|
38
|
+
Checks API keys in priority order and returns appropriate default model.
|
|
39
|
+
Treats Shotgun Account as a provider context.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config: Shotgun configuration containing API keys
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Default ModelName for the configured provider/account
|
|
46
|
+
"""
|
|
47
|
+
# Priority 1: Shotgun Account
|
|
48
|
+
if _get_api_key(config.shotgun.api_key):
|
|
49
|
+
return ModelName.CLAUDE_HAIKU_4_5
|
|
50
|
+
|
|
51
|
+
# Priority 2: Individual provider keys
|
|
52
|
+
if _get_api_key(config.anthropic.api_key):
|
|
53
|
+
return ModelName.CLAUDE_HAIKU_4_5
|
|
54
|
+
if _get_api_key(config.openai.api_key):
|
|
55
|
+
return ModelName.GPT_5
|
|
56
|
+
if _get_api_key(config.google.api_key):
|
|
57
|
+
return ModelName.GEMINI_2_5_PRO
|
|
58
|
+
|
|
59
|
+
# Fallback: system-wide default
|
|
60
|
+
return ModelName.CLAUDE_HAIKU_4_5
|
|
61
|
+
|
|
62
|
+
|
|
35
63
|
def get_or_create_model(
|
|
36
64
|
provider: ProviderType,
|
|
37
65
|
key_provider: "KeyProvider",
|
|
@@ -172,7 +200,7 @@ def get_provider_model(
|
|
|
172
200
|
model_name = provider_or_model
|
|
173
201
|
else:
|
|
174
202
|
# No specific model requested - use selected or default
|
|
175
|
-
model_name = config.selected_model or ModelName.
|
|
203
|
+
model_name = config.selected_model or ModelName.CLAUDE_HAIKU_4_5
|
|
176
204
|
|
|
177
205
|
if model_name not in MODEL_SPECS:
|
|
178
206
|
raise ValueError(f"Model '{model_name.value}' not found")
|
|
@@ -247,8 +275,8 @@ def get_provider_model(
|
|
|
247
275
|
if not api_key:
|
|
248
276
|
raise ValueError("Anthropic API key not configured. Set via config.")
|
|
249
277
|
|
|
250
|
-
# Use requested model or default to claude-
|
|
251
|
-
model_name = requested_model if requested_model else ModelName.
|
|
278
|
+
# Use requested model or default to claude-haiku-4-5
|
|
279
|
+
model_name = requested_model if requested_model else ModelName.CLAUDE_HAIKU_4_5
|
|
252
280
|
if model_name not in MODEL_SPECS:
|
|
253
281
|
raise ValueError(f"Model '{model_name.value}' not found")
|
|
254
282
|
spec = MODEL_SPECS[model_name]
|
shotgun/api_endpoints.py
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"""Shotgun backend service API endpoints and URLs."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
|
|
3
5
|
# Shotgun Web API base URL (for authentication/subscription)
|
|
4
6
|
# Can be overridden with environment variable
|
|
5
|
-
SHOTGUN_WEB_BASE_URL =
|
|
7
|
+
SHOTGUN_WEB_BASE_URL = os.getenv(
|
|
8
|
+
"SHOTGUN_WEB_BASE_URL", "https://api-219702594231.us-east4.run.app"
|
|
9
|
+
)
|
|
6
10
|
# Shotgun's LiteLLM proxy base URL (for AI model requests)
|
|
7
|
-
LITELLM_PROXY_BASE_URL =
|
|
11
|
+
LITELLM_PROXY_BASE_URL = os.getenv(
|
|
12
|
+
"SHOTGUN_ACCOUNT_LLM_BASE_URL", "https://litellm-219702594231.us-east4.run.app"
|
|
13
|
+
)
|
|
8
14
|
|
|
9
15
|
# Provider-specific LiteLLM proxy endpoints
|
|
10
16
|
LITELLM_PROXY_ANTHROPIC_BASE = f"{LITELLM_PROXY_BASE_URL}/anthropic"
|
shotgun/cli/update.py
CHANGED
|
@@ -45,7 +45,7 @@ def update(
|
|
|
45
45
|
|
|
46
46
|
This command will:
|
|
47
47
|
- Check PyPI for the latest version
|
|
48
|
-
- Detect your installation method (pipx, pip, or venv)
|
|
48
|
+
- Detect your installation method (uvx, uv-tool, pipx, pip, or venv)
|
|
49
49
|
- Perform the appropriate upgrade command
|
|
50
50
|
|
|
51
51
|
Examples:
|
|
@@ -93,6 +93,8 @@ def update(
|
|
|
93
93
|
)
|
|
94
94
|
console.print(
|
|
95
95
|
"Use --force to update anyway, or install the stable version with:\n"
|
|
96
|
+
" uv tool install shotgun-sh\n"
|
|
97
|
+
" or\n"
|
|
96
98
|
" pipx install shotgun-sh\n"
|
|
97
99
|
" or\n"
|
|
98
100
|
" pip install shotgun-sh",
|
|
@@ -134,7 +136,19 @@ def update(
|
|
|
134
136
|
console.print(f"\n[red]✗[/red] {message}", style="bold red")
|
|
135
137
|
|
|
136
138
|
# Provide manual update instructions
|
|
137
|
-
if method == "
|
|
139
|
+
if method == "uvx":
|
|
140
|
+
console.print(
|
|
141
|
+
"\n[yellow]Run uvx again to use the latest version:[/yellow]\n"
|
|
142
|
+
" uvx shotgun-sh\n"
|
|
143
|
+
"\n[yellow]Or install permanently:[/yellow]\n"
|
|
144
|
+
" uv tool install shotgun-sh"
|
|
145
|
+
)
|
|
146
|
+
elif method == "uv-tool":
|
|
147
|
+
console.print(
|
|
148
|
+
"\n[yellow]Try updating manually:[/yellow]\n"
|
|
149
|
+
" uv tool upgrade shotgun-sh"
|
|
150
|
+
)
|
|
151
|
+
elif method == "pipx":
|
|
138
152
|
console.print(
|
|
139
153
|
"\n[yellow]Try updating manually:[/yellow]\n"
|
|
140
154
|
" pipx upgrade shotgun-sh"
|
shotgun/codebase/core/manager.py
CHANGED
|
@@ -371,7 +371,16 @@ class CodebaseGraphManager:
|
|
|
371
371
|
)
|
|
372
372
|
import shutil
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
# Handle both files and directories (kuzu v0.11.2+ uses files)
|
|
375
|
+
if graph_path.is_file():
|
|
376
|
+
graph_path.unlink() # Delete file
|
|
377
|
+
# Also delete WAL file if it exists
|
|
378
|
+
wal_path = graph_path.with_suffix(graph_path.suffix + ".wal")
|
|
379
|
+
if wal_path.exists():
|
|
380
|
+
wal_path.unlink()
|
|
381
|
+
logger.debug(f"Deleted WAL file: {wal_path}")
|
|
382
|
+
else:
|
|
383
|
+
shutil.rmtree(graph_path) # Delete directory
|
|
375
384
|
|
|
376
385
|
# Import the builder from local core module
|
|
377
386
|
from shotgun.codebase.core import CodebaseIngestor
|
shotgun/main.py
CHANGED
|
@@ -125,6 +125,41 @@ def main(
|
|
|
125
125
|
help="Continue previous TUI conversation",
|
|
126
126
|
),
|
|
127
127
|
] = False,
|
|
128
|
+
web: Annotated[
|
|
129
|
+
bool,
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--web",
|
|
132
|
+
help="Serve TUI as web application",
|
|
133
|
+
),
|
|
134
|
+
] = False,
|
|
135
|
+
port: Annotated[
|
|
136
|
+
int,
|
|
137
|
+
typer.Option(
|
|
138
|
+
"--port",
|
|
139
|
+
help="Port for web server (only used with --web)",
|
|
140
|
+
),
|
|
141
|
+
] = 8000,
|
|
142
|
+
host: Annotated[
|
|
143
|
+
str,
|
|
144
|
+
typer.Option(
|
|
145
|
+
"--host",
|
|
146
|
+
help="Host address for web server (only used with --web)",
|
|
147
|
+
),
|
|
148
|
+
] = "localhost",
|
|
149
|
+
public_url: Annotated[
|
|
150
|
+
str | None,
|
|
151
|
+
typer.Option(
|
|
152
|
+
"--public-url",
|
|
153
|
+
help="Public URL if behind proxy (only used with --web)",
|
|
154
|
+
),
|
|
155
|
+
] = None,
|
|
156
|
+
force_reindex: Annotated[
|
|
157
|
+
bool,
|
|
158
|
+
typer.Option(
|
|
159
|
+
"--force-reindex",
|
|
160
|
+
help="Force re-indexing of codebase (ignores existing index)",
|
|
161
|
+
),
|
|
162
|
+
] = False,
|
|
128
163
|
) -> None:
|
|
129
164
|
"""Shotgun - AI-powered CLI tool."""
|
|
130
165
|
logger.debug("Starting shotgun CLI application")
|
|
@@ -134,16 +169,35 @@ def main(
|
|
|
134
169
|
perform_auto_update_async(no_update_check=no_update_check)
|
|
135
170
|
|
|
136
171
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
172
|
+
if web:
|
|
173
|
+
logger.debug("Launching shotgun TUI as web application")
|
|
174
|
+
try:
|
|
175
|
+
tui_app.serve(
|
|
176
|
+
host=host,
|
|
177
|
+
port=port,
|
|
178
|
+
public_url=public_url,
|
|
179
|
+
no_update_check=no_update_check,
|
|
180
|
+
continue_session=continue_session,
|
|
181
|
+
force_reindex=force_reindex,
|
|
182
|
+
)
|
|
183
|
+
finally:
|
|
184
|
+
# Ensure PostHog is shut down cleanly even if server exits unexpectedly
|
|
185
|
+
from shotgun.posthog_telemetry import shutdown
|
|
186
|
+
|
|
187
|
+
shutdown()
|
|
188
|
+
else:
|
|
189
|
+
logger.debug("Launching shotgun TUI application")
|
|
190
|
+
try:
|
|
191
|
+
tui_app.run(
|
|
192
|
+
no_update_check=no_update_check,
|
|
193
|
+
continue_session=continue_session,
|
|
194
|
+
force_reindex=force_reindex,
|
|
195
|
+
)
|
|
196
|
+
finally:
|
|
197
|
+
# Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
|
|
198
|
+
from shotgun.posthog_telemetry import shutdown
|
|
199
|
+
|
|
200
|
+
shutdown()
|
|
147
201
|
raise typer.Exit()
|
|
148
202
|
|
|
149
203
|
# For CLI commands, register PostHog shutdown handler
|
shotgun/tui/app.py
CHANGED
|
@@ -9,12 +9,16 @@ from shotgun.agents.config import ConfigManager, get_config_manager
|
|
|
9
9
|
from shotgun.logging_config import get_logger
|
|
10
10
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
11
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
|
-
from shotgun.utils.update_checker import
|
|
12
|
+
from shotgun.utils.update_checker import (
|
|
13
|
+
detect_installation_method,
|
|
14
|
+
perform_auto_update_async,
|
|
15
|
+
)
|
|
13
16
|
|
|
14
17
|
from .screens.chat import ChatScreen
|
|
15
18
|
from .screens.directory_setup import DirectorySetupScreen
|
|
16
19
|
from .screens.feedback import FeedbackScreen
|
|
17
20
|
from .screens.model_picker import ModelPickerScreen
|
|
21
|
+
from .screens.pipx_migration import PipxMigrationScreen
|
|
18
22
|
from .screens.provider_config import ProviderConfigScreen
|
|
19
23
|
from .screens.welcome import WelcomeScreen
|
|
20
24
|
|
|
@@ -36,12 +40,16 @@ class ShotgunApp(App[None]):
|
|
|
36
40
|
CSS_PATH = "styles.tcss"
|
|
37
41
|
|
|
38
42
|
def __init__(
|
|
39
|
-
self,
|
|
43
|
+
self,
|
|
44
|
+
no_update_check: bool = False,
|
|
45
|
+
continue_session: bool = False,
|
|
46
|
+
force_reindex: bool = False,
|
|
40
47
|
) -> None:
|
|
41
48
|
super().__init__()
|
|
42
49
|
self.config_manager: ConfigManager = get_config_manager()
|
|
43
50
|
self.no_update_check = no_update_check
|
|
44
51
|
self.continue_session = continue_session
|
|
52
|
+
self.force_reindex = force_reindex
|
|
45
53
|
|
|
46
54
|
# Start async update check and install
|
|
47
55
|
if not no_update_check:
|
|
@@ -52,14 +60,35 @@ class ShotgunApp(App[None]):
|
|
|
52
60
|
# Track TUI startup
|
|
53
61
|
from shotgun.posthog_telemetry import track_event
|
|
54
62
|
|
|
55
|
-
track_event(
|
|
63
|
+
track_event(
|
|
64
|
+
"tui_started",
|
|
65
|
+
{
|
|
66
|
+
"installation_method": detect_installation_method(),
|
|
67
|
+
},
|
|
68
|
+
)
|
|
56
69
|
|
|
57
70
|
self.push_screen(
|
|
58
71
|
SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
|
|
59
72
|
)
|
|
60
73
|
|
|
61
|
-
def refresh_startup_screen(self) -> None:
|
|
74
|
+
def refresh_startup_screen(self, skip_pipx_check: bool = False) -> None:
|
|
62
75
|
"""Push the appropriate screen based on configured providers."""
|
|
76
|
+
# Check for pipx installation and show migration modal first
|
|
77
|
+
if not skip_pipx_check:
|
|
78
|
+
installation_method = detect_installation_method()
|
|
79
|
+
if installation_method == "pipx":
|
|
80
|
+
if isinstance(self.screen, PipxMigrationScreen):
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Show pipx migration modal as a blocking modal screen
|
|
84
|
+
self.push_screen(
|
|
85
|
+
PipxMigrationScreen(),
|
|
86
|
+
callback=lambda _arg: self.refresh_startup_screen(
|
|
87
|
+
skip_pipx_check=True
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
63
92
|
# Show welcome screen if no providers are configured OR if user hasn't seen it yet
|
|
64
93
|
config = self.config_manager.load()
|
|
65
94
|
if (
|
|
@@ -87,8 +116,12 @@ class ShotgunApp(App[None]):
|
|
|
87
116
|
|
|
88
117
|
if isinstance(self.screen, ChatScreen):
|
|
89
118
|
return
|
|
90
|
-
# Pass continue_session
|
|
91
|
-
self.push_screen(
|
|
119
|
+
# Pass continue_session and force_reindex flags to ChatScreen
|
|
120
|
+
self.push_screen(
|
|
121
|
+
ChatScreen(
|
|
122
|
+
continue_session=self.continue_session, force_reindex=self.force_reindex
|
|
123
|
+
)
|
|
124
|
+
)
|
|
92
125
|
|
|
93
126
|
def check_local_shotgun_directory_exists(self) -> bool:
|
|
94
127
|
shotgun_dir = get_shotgun_base_path()
|
|
@@ -121,12 +154,17 @@ class ShotgunApp(App[None]):
|
|
|
121
154
|
self.push_screen(FeedbackScreen(), callback=handle_feedback)
|
|
122
155
|
|
|
123
156
|
|
|
124
|
-
def run(
|
|
157
|
+
def run(
|
|
158
|
+
no_update_check: bool = False,
|
|
159
|
+
continue_session: bool = False,
|
|
160
|
+
force_reindex: bool = False,
|
|
161
|
+
) -> None:
|
|
125
162
|
"""Run the TUI application.
|
|
126
163
|
|
|
127
164
|
Args:
|
|
128
165
|
no_update_check: If True, disable automatic update checks.
|
|
129
166
|
continue_session: If True, continue from previous conversation.
|
|
167
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
130
168
|
"""
|
|
131
169
|
# Clean up any corrupted databases BEFORE starting the TUI
|
|
132
170
|
# This prevents crashes from corrupted databases during initialization
|
|
@@ -148,9 +186,133 @@ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
|
|
148
186
|
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
149
187
|
# Continue anyway - the TUI can still function
|
|
150
188
|
|
|
151
|
-
app = ShotgunApp(
|
|
189
|
+
app = ShotgunApp(
|
|
190
|
+
no_update_check=no_update_check,
|
|
191
|
+
continue_session=continue_session,
|
|
192
|
+
force_reindex=force_reindex,
|
|
193
|
+
)
|
|
152
194
|
app.run(inline_no_clear=True)
|
|
153
195
|
|
|
154
196
|
|
|
197
|
+
def serve(
|
|
198
|
+
host: str = "localhost",
|
|
199
|
+
port: int = 8000,
|
|
200
|
+
public_url: str | None = None,
|
|
201
|
+
no_update_check: bool = False,
|
|
202
|
+
continue_session: bool = False,
|
|
203
|
+
force_reindex: bool = False,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Serve the TUI application as a web application.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
host: Host address for the web server.
|
|
209
|
+
port: Port number for the web server.
|
|
210
|
+
public_url: Public URL if behind a proxy.
|
|
211
|
+
no_update_check: If True, disable automatic update checks.
|
|
212
|
+
continue_session: If True, continue from previous conversation.
|
|
213
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
214
|
+
"""
|
|
215
|
+
# Clean up any corrupted databases BEFORE starting the TUI
|
|
216
|
+
# This prevents crashes from corrupted databases during initialization
|
|
217
|
+
import asyncio
|
|
218
|
+
|
|
219
|
+
from textual_serve.server import Server
|
|
220
|
+
|
|
221
|
+
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
222
|
+
from shotgun.utils import get_shotgun_home
|
|
223
|
+
|
|
224
|
+
storage_dir = get_shotgun_home() / "codebases"
|
|
225
|
+
manager = CodebaseGraphManager(storage_dir)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
removed = asyncio.run(manager.cleanup_corrupted_databases())
|
|
229
|
+
if removed:
|
|
230
|
+
logger.info(
|
|
231
|
+
f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
235
|
+
# Continue anyway - the TUI can still function
|
|
236
|
+
|
|
237
|
+
# Create a new event loop after asyncio.run() closes the previous one
|
|
238
|
+
# This is needed for the Server.serve() method
|
|
239
|
+
loop = asyncio.new_event_loop()
|
|
240
|
+
asyncio.set_event_loop(loop)
|
|
241
|
+
|
|
242
|
+
# Build the command string based on flags
|
|
243
|
+
command = "shotgun"
|
|
244
|
+
if no_update_check:
|
|
245
|
+
command += " --no-update-check"
|
|
246
|
+
if continue_session:
|
|
247
|
+
command += " --continue"
|
|
248
|
+
if force_reindex:
|
|
249
|
+
command += " --force-reindex"
|
|
250
|
+
|
|
251
|
+
# Create and start the server with hardcoded title and debug=False
|
|
252
|
+
server = Server(
|
|
253
|
+
command=command,
|
|
254
|
+
host=host,
|
|
255
|
+
port=port,
|
|
256
|
+
title="The Shotgun",
|
|
257
|
+
public_url=public_url,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Set up graceful shutdown on SIGTERM/SIGINT
|
|
261
|
+
import signal
|
|
262
|
+
import sys
|
|
263
|
+
|
|
264
|
+
def signal_handler(_signum: int, _frame: Any) -> None:
|
|
265
|
+
"""Handle shutdown signals gracefully."""
|
|
266
|
+
from shotgun.posthog_telemetry import shutdown
|
|
267
|
+
|
|
268
|
+
logger.info("Received shutdown signal, cleaning up...")
|
|
269
|
+
# Restore stdout/stderr before shutting down
|
|
270
|
+
sys.stdout = original_stdout
|
|
271
|
+
sys.stderr = original_stderr
|
|
272
|
+
shutdown()
|
|
273
|
+
sys.exit(0)
|
|
274
|
+
|
|
275
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
276
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
277
|
+
|
|
278
|
+
# Suppress the textual-serve banner by redirecting stdout/stderr
|
|
279
|
+
import io
|
|
280
|
+
|
|
281
|
+
# Capture and suppress the banner, but show the actual serving URL
|
|
282
|
+
original_stdout = sys.stdout
|
|
283
|
+
original_stderr = sys.stderr
|
|
284
|
+
|
|
285
|
+
captured_output = io.StringIO()
|
|
286
|
+
sys.stdout = captured_output
|
|
287
|
+
sys.stderr = captured_output
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
# This will print the banner to our captured output
|
|
291
|
+
import logging
|
|
292
|
+
|
|
293
|
+
# Temporarily set logging to ERROR level to suppress INFO messages
|
|
294
|
+
textual_serve_logger = logging.getLogger("textual_serve")
|
|
295
|
+
original_level = textual_serve_logger.level
|
|
296
|
+
textual_serve_logger.setLevel(logging.ERROR)
|
|
297
|
+
|
|
298
|
+
# Print our own message to the original stdout
|
|
299
|
+
sys.stdout = original_stdout
|
|
300
|
+
sys.stderr = original_stderr
|
|
301
|
+
print(f"Serving Shotgun TUI at http://{host}:{port}")
|
|
302
|
+
print("Press Ctrl+C to quit")
|
|
303
|
+
|
|
304
|
+
# Now suppress output again for the serve call
|
|
305
|
+
sys.stdout = captured_output
|
|
306
|
+
sys.stderr = captured_output
|
|
307
|
+
|
|
308
|
+
server.serve(debug=False)
|
|
309
|
+
finally:
|
|
310
|
+
# Restore original stdout/stderr
|
|
311
|
+
sys.stdout = original_stdout
|
|
312
|
+
sys.stderr = original_stderr
|
|
313
|
+
if "textual_serve_logger" in locals():
|
|
314
|
+
textual_serve_logger.setLevel(original_level)
|
|
315
|
+
|
|
316
|
+
|
|
155
317
|
if __name__ == "__main__":
|
|
156
318
|
run()
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -39,7 +39,10 @@ from shotgun.agents.models import (
|
|
|
39
39
|
AgentType,
|
|
40
40
|
FileOperationTracker,
|
|
41
41
|
)
|
|
42
|
-
from shotgun.codebase.core.manager import
|
|
42
|
+
from shotgun.codebase.core.manager import (
|
|
43
|
+
CodebaseAlreadyIndexedError,
|
|
44
|
+
CodebaseGraphManager,
|
|
45
|
+
)
|
|
43
46
|
from shotgun.codebase.models import IndexProgress, ProgressPhase
|
|
44
47
|
from shotgun.posthog_telemetry import track_event
|
|
45
48
|
from shotgun.sdk.codebase import CodebaseSDK
|
|
@@ -291,7 +294,9 @@ class ChatScreen(Screen[None]):
|
|
|
291
294
|
qa_current_index = reactive(0)
|
|
292
295
|
qa_answers: list[str] = []
|
|
293
296
|
|
|
294
|
-
def __init__(
|
|
297
|
+
def __init__(
|
|
298
|
+
self, continue_session: bool = False, force_reindex: bool = False
|
|
299
|
+
) -> None:
|
|
295
300
|
super().__init__()
|
|
296
301
|
# Get the model configuration and services
|
|
297
302
|
model_config = get_provider_model()
|
|
@@ -319,6 +324,7 @@ class ChatScreen(Screen[None]):
|
|
|
319
324
|
self.placeholder_hints = PlaceholderHints()
|
|
320
325
|
self.conversation_manager = ConversationManager()
|
|
321
326
|
self.continue_session = continue_session
|
|
327
|
+
self.force_reindex = force_reindex
|
|
322
328
|
|
|
323
329
|
def on_mount(self) -> None:
|
|
324
330
|
self.query_one(PromptInput).focus(scroll_visible=True)
|
|
@@ -378,6 +384,22 @@ class ChatScreen(Screen[None]):
|
|
|
378
384
|
if is_empty or self.continue_session:
|
|
379
385
|
return
|
|
380
386
|
|
|
387
|
+
# If force_reindex is True, delete any existing graphs for this directory
|
|
388
|
+
if self.force_reindex:
|
|
389
|
+
accessible_graphs = (
|
|
390
|
+
await self.codebase_sdk.list_codebases_for_directory()
|
|
391
|
+
).graphs
|
|
392
|
+
for graph in accessible_graphs:
|
|
393
|
+
try:
|
|
394
|
+
await self.codebase_sdk.delete_codebase(graph.graph_id)
|
|
395
|
+
logger.info(
|
|
396
|
+
f"Deleted existing graph {graph.graph_id} due to --force-reindex"
|
|
397
|
+
)
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.warning(
|
|
400
|
+
f"Failed to delete graph {graph.graph_id} during force reindex: {e}"
|
|
401
|
+
)
|
|
402
|
+
|
|
381
403
|
# Check if the current directory has any accessible codebases
|
|
382
404
|
accessible_graphs = (
|
|
383
405
|
await self.codebase_sdk.list_codebases_for_directory()
|
|
@@ -766,6 +788,28 @@ class ChatScreen(Screen[None]):
|
|
|
766
788
|
except Exception as exc: # pragma: no cover - defensive UI path
|
|
767
789
|
self.notify(f"Failed to delete codebase: {exc}", severity="error")
|
|
768
790
|
|
|
791
|
+
def _is_kuzu_corruption_error(self, exception: Exception) -> bool:
|
|
792
|
+
"""Check if error is related to kuzu database corruption.
|
|
793
|
+
|
|
794
|
+
Args:
|
|
795
|
+
exception: The exception to check
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
True if the error indicates kuzu database corruption
|
|
799
|
+
"""
|
|
800
|
+
error_str = str(exception).lower()
|
|
801
|
+
error_indicators = [
|
|
802
|
+
"not a directory",
|
|
803
|
+
"errno 20",
|
|
804
|
+
"corrupted",
|
|
805
|
+
".kuzu",
|
|
806
|
+
"ioexception",
|
|
807
|
+
"unordered_map", # C++ STL map errors from kuzu
|
|
808
|
+
"key not found", # unordered_map::at errors
|
|
809
|
+
"std::exception", # Generic C++ exceptions from kuzu
|
|
810
|
+
]
|
|
811
|
+
return any(indicator in error_str for indicator in error_indicators)
|
|
812
|
+
|
|
769
813
|
@work
|
|
770
814
|
async def index_codebase(self, selection: CodebaseIndexSelection) -> None:
|
|
771
815
|
label = self.query_one("#indexing-job-display", Static)
|
|
@@ -834,58 +878,96 @@ class ChatScreen(Screen[None]):
|
|
|
834
878
|
# Start progress animation timer (10 fps = 100ms interval)
|
|
835
879
|
progress_timer = self.set_interval(0.1, update_progress_display)
|
|
836
880
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
881
|
+
# Retry logic for handling kuzu corruption
|
|
882
|
+
max_retries = 3
|
|
883
|
+
|
|
884
|
+
for attempt in range(max_retries):
|
|
885
|
+
try:
|
|
886
|
+
# Clean up corrupted DBs before retry (skip on first attempt)
|
|
887
|
+
if attempt > 0:
|
|
888
|
+
logger.info(
|
|
889
|
+
f"Retry attempt {attempt + 1}/{max_retries} - cleaning up corrupted databases"
|
|
890
|
+
)
|
|
891
|
+
manager = CodebaseGraphManager(
|
|
892
|
+
self.codebase_sdk.service.storage_dir
|
|
893
|
+
)
|
|
894
|
+
cleaned = await manager.cleanup_corrupted_databases()
|
|
895
|
+
logger.info(f"Cleaned up {len(cleaned)} corrupted database(s)")
|
|
896
|
+
self.notify(
|
|
897
|
+
f"Retrying indexing after cleanup (attempt {attempt + 1}/{max_retries})...",
|
|
898
|
+
severity="information",
|
|
899
|
+
)
|
|
849
900
|
|
|
850
|
-
|
|
851
|
-
|
|
901
|
+
# Pass the current working directory as the indexed_from_cwd
|
|
902
|
+
logger.debug(
|
|
903
|
+
f"Starting indexing - repo_path: {selection.repo_path}, "
|
|
904
|
+
f"name: {selection.name}, cwd: {Path.cwd().resolve()}"
|
|
905
|
+
)
|
|
906
|
+
result = await self.codebase_sdk.index_codebase(
|
|
907
|
+
selection.repo_path,
|
|
908
|
+
selection.name,
|
|
909
|
+
indexed_from_cwd=str(Path.cwd().resolve()),
|
|
910
|
+
progress_callback=progress_callback,
|
|
911
|
+
)
|
|
852
912
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
label.update(f"[$foreground-muted]Indexing codebase: {final_bar} 100%[/]")
|
|
856
|
-
label.refresh()
|
|
913
|
+
# Success! Stop progress animation
|
|
914
|
+
progress_timer.stop()
|
|
857
915
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
timeout=8,
|
|
865
|
-
)
|
|
916
|
+
# Show 100% completion after indexing finishes
|
|
917
|
+
final_bar = create_progress_bar(100.0)
|
|
918
|
+
label.update(
|
|
919
|
+
f"[$foreground-muted]Indexing codebase: {final_bar} 100%[/]"
|
|
920
|
+
)
|
|
921
|
+
label.refresh()
|
|
866
922
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
923
|
+
logger.info(
|
|
924
|
+
f"Successfully indexed codebase '{result.name}' (ID: {result.graph_id})"
|
|
925
|
+
)
|
|
926
|
+
self.notify(
|
|
927
|
+
f"Indexed codebase '{result.name}' (ID: {result.graph_id})",
|
|
928
|
+
severity="information",
|
|
929
|
+
timeout=8,
|
|
930
|
+
)
|
|
931
|
+
break # Success - exit retry loop
|
|
932
|
+
|
|
933
|
+
except CodebaseAlreadyIndexedError as exc:
|
|
934
|
+
progress_timer.stop()
|
|
935
|
+
logger.warning(f"Codebase already indexed: {exc}")
|
|
936
|
+
self.notify(str(exc), severity="warning")
|
|
937
|
+
return
|
|
938
|
+
except InvalidPathError as exc:
|
|
939
|
+
progress_timer.stop()
|
|
940
|
+
logger.error(f"Invalid path error: {exc}")
|
|
941
|
+
self.notify(str(exc), severity="error")
|
|
942
|
+
return
|
|
943
|
+
|
|
944
|
+
except Exception as exc: # pragma: no cover - defensive UI path
|
|
945
|
+
# Check if this is a kuzu corruption error and we have retries left
|
|
946
|
+
if attempt < max_retries - 1 and self._is_kuzu_corruption_error(exc):
|
|
947
|
+
logger.warning(
|
|
948
|
+
f"Kuzu corruption detected on attempt {attempt + 1}/{max_retries}: {exc}. "
|
|
949
|
+
f"Will retry after cleanup..."
|
|
950
|
+
)
|
|
951
|
+
# Exponential backoff: 1s, 2s
|
|
952
|
+
await asyncio.sleep(2**attempt)
|
|
953
|
+
continue
|
|
954
|
+
|
|
955
|
+
# Either final retry failed OR not a corruption error - show error
|
|
956
|
+
logger.exception(
|
|
957
|
+
f"Failed to index codebase after {attempt + 1} attempts - "
|
|
958
|
+
f"repo_path: {selection.repo_path}, name: {selection.name}, error: {exc}"
|
|
959
|
+
)
|
|
960
|
+
self.notify(
|
|
961
|
+
f"Failed to index codebase after {attempt + 1} attempts: {exc}",
|
|
962
|
+
severity="error",
|
|
963
|
+
timeout=30, # Keep error visible for 30 seconds
|
|
964
|
+
)
|
|
965
|
+
break
|
|
876
966
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
f"name: {selection.name}, error: {exc}"
|
|
882
|
-
)
|
|
883
|
-
self.notify(f"Failed to index codebase: {exc}", severity="error")
|
|
884
|
-
finally:
|
|
885
|
-
# Always stop the progress timer
|
|
886
|
-
progress_timer.stop()
|
|
887
|
-
label.update("")
|
|
888
|
-
label.refresh()
|
|
967
|
+
# Always stop the progress timer and clean up label
|
|
968
|
+
progress_timer.stop()
|
|
969
|
+
label.update("")
|
|
970
|
+
label.refresh()
|
|
889
971
|
|
|
890
972
|
@work
|
|
891
973
|
async def run_agent(self, message: str) -> None:
|
|
@@ -13,6 +13,7 @@ from textual.widgets import Button, Label, ListItem, ListView, Static
|
|
|
13
13
|
|
|
14
14
|
from shotgun.agents.config import ConfigManager
|
|
15
15
|
from shotgun.agents.config.models import MODEL_SPECS, ModelName, ShotgunConfig
|
|
16
|
+
from shotgun.agents.config.provider import get_default_model_for_provider
|
|
16
17
|
from shotgun.logging_config import get_logger
|
|
17
18
|
|
|
18
19
|
if TYPE_CHECKING:
|
|
@@ -111,7 +112,7 @@ class ModelPickerScreen(Screen[None]):
|
|
|
111
112
|
config_manager._provider_has_api_key(config.shotgun),
|
|
112
113
|
)
|
|
113
114
|
|
|
114
|
-
current_model = config.selected_model or
|
|
115
|
+
current_model = config.selected_model or get_default_model_for_provider(config)
|
|
115
116
|
self.selected_model = current_model
|
|
116
117
|
logger.debug("Current selected model: %s", current_model)
|
|
117
118
|
|
|
@@ -193,7 +194,7 @@ class ModelPickerScreen(Screen[None]):
|
|
|
193
194
|
"""
|
|
194
195
|
# Load config once with force_reload
|
|
195
196
|
config = self.config_manager.load(force_reload=True)
|
|
196
|
-
current_model = config.selected_model or
|
|
197
|
+
current_model = config.selected_model or get_default_model_for_provider(config)
|
|
197
198
|
|
|
198
199
|
# Update labels for available models only
|
|
199
200
|
for model_name in AVAILABLE_MODELS:
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Migration notice screen for pipx users."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from textual import on
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Container, Horizontal, VerticalScroll
|
|
10
|
+
from textual.screen import ModalScreen
|
|
11
|
+
from textual.widgets import Button, Markdown
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PipxMigrationScreen(ModalScreen[None]):
|
|
18
|
+
"""Modal screen warning pipx users about migration to uvx."""
|
|
19
|
+
|
|
20
|
+
CSS = """
|
|
21
|
+
PipxMigrationScreen {
|
|
22
|
+
align: center middle;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#migration-container {
|
|
26
|
+
width: 90;
|
|
27
|
+
height: auto;
|
|
28
|
+
max-height: 90%;
|
|
29
|
+
border: thick $error;
|
|
30
|
+
background: $surface;
|
|
31
|
+
padding: 2;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#migration-content {
|
|
35
|
+
height: 1fr;
|
|
36
|
+
padding: 1 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#buttons-container {
|
|
40
|
+
height: auto;
|
|
41
|
+
padding: 2 0 1 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#action-buttons {
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: auto;
|
|
47
|
+
align: center middle;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.action-button {
|
|
51
|
+
margin: 0 1;
|
|
52
|
+
min-width: 20;
|
|
53
|
+
}
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
BINDINGS = [
|
|
57
|
+
("escape", "dismiss", "Continue Anyway"),
|
|
58
|
+
("ctrl+c", "app.quit", "Quit"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def compose(self) -> ComposeResult:
|
|
62
|
+
"""Compose the migration notice modal."""
|
|
63
|
+
with Container(id="migration-container"):
|
|
64
|
+
with VerticalScroll(id="migration-content"):
|
|
65
|
+
yield Markdown(
|
|
66
|
+
"""
|
|
67
|
+
## We've Switched to uvx
|
|
68
|
+
|
|
69
|
+
We've switched from `pipx` to `uvx` as the primary installation method due to critical build issues with our `kuzu` dependency.
|
|
70
|
+
|
|
71
|
+
### The Problem
|
|
72
|
+
Users with pipx encounter cmake build errors during installation because pip falls back to building from source instead of using pre-built binary wheels.
|
|
73
|
+
|
|
74
|
+
### The Solution: uvx
|
|
75
|
+
- ✅ **No build tools required** - Binary wheels enforced
|
|
76
|
+
- ✅ **10-100x faster** - Much faster than pipx
|
|
77
|
+
- ✅ **Better reliability** - No cmake/build errors
|
|
78
|
+
|
|
79
|
+
### How to Migrate
|
|
80
|
+
|
|
81
|
+
**1. Uninstall shotgun-sh from pipx:**
|
|
82
|
+
```bash
|
|
83
|
+
pipx uninstall shotgun-sh
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**2. Install uv:**
|
|
87
|
+
```bash
|
|
88
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
89
|
+
```
|
|
90
|
+
Or with Homebrew: `brew install uv`
|
|
91
|
+
|
|
92
|
+
**3. Run shotgun-sh with uvx:**
|
|
93
|
+
```bash
|
|
94
|
+
uvx shotgun-sh
|
|
95
|
+
```
|
|
96
|
+
Or install permanently: `uv tool install shotgun-sh`
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Need Help?
|
|
101
|
+
|
|
102
|
+
**Discord:** https://discord.gg/5RmY6J2N7s
|
|
103
|
+
|
|
104
|
+
**Full Migration Guide:** https://github.com/shotgun-sh/shotgun/blob/main/PIPX_MIGRATION.md
|
|
105
|
+
"""
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
with Container(id="buttons-container"):
|
|
109
|
+
with Horizontal(id="action-buttons"):
|
|
110
|
+
yield Button(
|
|
111
|
+
"Copy Instructions to Clipboard",
|
|
112
|
+
variant="default",
|
|
113
|
+
id="copy-instructions",
|
|
114
|
+
classes="action-button",
|
|
115
|
+
)
|
|
116
|
+
yield Button(
|
|
117
|
+
"Continue Anyway",
|
|
118
|
+
variant="primary",
|
|
119
|
+
id="continue",
|
|
120
|
+
classes="action-button",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def on_mount(self) -> None:
|
|
124
|
+
"""Focus the continue button and ensure scroll starts at top."""
|
|
125
|
+
self.query_one("#continue", Button).focus()
|
|
126
|
+
self.query_one("#migration-content", VerticalScroll).scroll_home(animate=False)
|
|
127
|
+
|
|
128
|
+
@on(Button.Pressed, "#copy-instructions")
|
|
129
|
+
def _copy_instructions(self) -> None:
|
|
130
|
+
"""Copy all migration instructions to clipboard."""
|
|
131
|
+
instructions = """# Step 1: Uninstall from pipx
|
|
132
|
+
pipx uninstall shotgun-sh
|
|
133
|
+
|
|
134
|
+
# Step 2: Install uv
|
|
135
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
136
|
+
|
|
137
|
+
# Step 3: Run shotgun with uvx
|
|
138
|
+
uvx shotgun-sh"""
|
|
139
|
+
try:
|
|
140
|
+
import pyperclip # type: ignore[import-untyped] # noqa: PGH003
|
|
141
|
+
|
|
142
|
+
pyperclip.copy(instructions)
|
|
143
|
+
self.notify("Copied migration instructions to clipboard!")
|
|
144
|
+
except ImportError:
|
|
145
|
+
self.notify(
|
|
146
|
+
"Clipboard not available. See instructions above.",
|
|
147
|
+
severity="warning",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@on(Button.Pressed, "#continue")
|
|
151
|
+
def _continue(self) -> None:
|
|
152
|
+
"""Dismiss the modal and continue."""
|
|
153
|
+
self.dismiss()
|
shotgun/utils/update_checker.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Simple auto-update functionality for shotgun-sh CLI."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
import threading
|
|
@@ -18,8 +19,34 @@ def detect_installation_method() -> str:
|
|
|
18
19
|
"""Detect how shotgun-sh was installed.
|
|
19
20
|
|
|
20
21
|
Returns:
|
|
21
|
-
Installation method: 'pipx', 'pip', 'venv', or 'unknown'.
|
|
22
|
+
Installation method: 'uvx', 'uv-tool', 'pipx', 'pip', 'venv', or 'unknown'.
|
|
22
23
|
"""
|
|
24
|
+
# Check for simulation environment variable (for testing)
|
|
25
|
+
if os.getenv("PIPX_SIMULATE", "").lower() in ("true", "1"):
|
|
26
|
+
logger.debug("PIPX_SIMULATE enabled, simulating pipx installation")
|
|
27
|
+
return "pipx"
|
|
28
|
+
|
|
29
|
+
# Check for uvx (ephemeral execution) by looking at executable path
|
|
30
|
+
# uvx runs from a temporary cache directory
|
|
31
|
+
executable = Path(sys.executable)
|
|
32
|
+
if ".cache/uv" in str(executable) or "uv/cache" in str(executable):
|
|
33
|
+
logger.debug("Detected uvx (ephemeral) execution")
|
|
34
|
+
return "uvx"
|
|
35
|
+
|
|
36
|
+
# Check for uv tool installation
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["uv", "tool", "list"], # noqa: S607, S603
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=5,
|
|
43
|
+
)
|
|
44
|
+
if result.returncode == 0 and "shotgun-sh" in result.stdout:
|
|
45
|
+
logger.debug("Detected uv tool installation")
|
|
46
|
+
return "uv-tool"
|
|
47
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
23
50
|
# Check for pipx installation
|
|
24
51
|
try:
|
|
25
52
|
result = subprocess.run(
|
|
@@ -59,7 +86,7 @@ def detect_installation_method() -> str:
|
|
|
59
86
|
|
|
60
87
|
|
|
61
88
|
def perform_auto_update(no_update_check: bool = False) -> None:
|
|
62
|
-
"""Perform automatic update if installed via pipx.
|
|
89
|
+
"""Perform automatic update if installed via pipx or uv tool.
|
|
63
90
|
|
|
64
91
|
Args:
|
|
65
92
|
no_update_check: If True, skip the update.
|
|
@@ -68,23 +95,40 @@ def perform_auto_update(no_update_check: bool = False) -> None:
|
|
|
68
95
|
return
|
|
69
96
|
|
|
70
97
|
try:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
method = detect_installation_method()
|
|
99
|
+
|
|
100
|
+
# Skip auto-update for ephemeral uvx executions
|
|
101
|
+
if method == "uvx":
|
|
102
|
+
logger.debug("uvx (ephemeral) execution, skipping auto-update")
|
|
74
103
|
return
|
|
75
104
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
# Only auto-update for pipx and uv-tool installations
|
|
106
|
+
if method not in ["pipx", "uv-tool"]:
|
|
107
|
+
logger.debug(f"Installation method '{method}', skipping auto-update")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Determine the appropriate upgrade command
|
|
111
|
+
if method == "pipx":
|
|
112
|
+
command = ["pipx", "upgrade", "shotgun-sh", "--quiet"]
|
|
113
|
+
logger.debug("Running pipx upgrade shotgun-sh --quiet")
|
|
114
|
+
elif method == "uv-tool":
|
|
115
|
+
command = ["uv", "tool", "upgrade", "shotgun-sh"]
|
|
116
|
+
logger.debug("Running uv tool upgrade shotgun-sh")
|
|
117
|
+
else:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Run upgrade command
|
|
121
|
+
result = subprocess.run( # noqa: S603, S607
|
|
122
|
+
command,
|
|
80
123
|
capture_output=True,
|
|
81
124
|
text=True,
|
|
82
125
|
timeout=30,
|
|
83
126
|
)
|
|
84
127
|
|
|
85
128
|
if result.returncode == 0:
|
|
86
|
-
# Check if there was an actual update
|
|
87
|
-
|
|
129
|
+
# Check if there was an actual update
|
|
130
|
+
output = result.stdout.lower()
|
|
131
|
+
if "upgraded" in output or "updated" in output:
|
|
88
132
|
logger.info("Shotgun-sh has been updated to the latest version")
|
|
89
133
|
else:
|
|
90
134
|
# Only log errors at debug level to not annoy users
|
|
@@ -166,16 +210,18 @@ def compare_versions(current: str, latest: str) -> bool:
|
|
|
166
210
|
return False
|
|
167
211
|
|
|
168
212
|
|
|
169
|
-
def get_update_command(method: str) -> list[str]:
|
|
213
|
+
def get_update_command(method: str) -> list[str] | None:
|
|
170
214
|
"""Get the appropriate update command based on installation method.
|
|
171
215
|
|
|
172
216
|
Args:
|
|
173
|
-
method: Installation method ('pipx', 'pip', 'venv', or 'unknown').
|
|
217
|
+
method: Installation method ('uvx', 'uv-tool', 'pipx', 'pip', 'venv', or 'unknown').
|
|
174
218
|
|
|
175
219
|
Returns:
|
|
176
|
-
Command list to execute for updating.
|
|
220
|
+
Command list to execute for updating, or None for uvx (ephemeral).
|
|
177
221
|
"""
|
|
178
222
|
commands = {
|
|
223
|
+
"uvx": None, # uvx is ephemeral, no update command
|
|
224
|
+
"uv-tool": ["uv", "tool", "upgrade", "shotgun-sh"],
|
|
179
225
|
"pipx": ["pipx", "upgrade", "shotgun-sh"],
|
|
180
226
|
"pip": [sys.executable, "-m", "pip", "install", "--upgrade", "shotgun-sh"],
|
|
181
227
|
"venv": [sys.executable, "-m", "pip", "install", "--upgrade", "shotgun-sh"],
|
|
@@ -210,6 +256,15 @@ def perform_update(force: bool = False) -> tuple[bool, str]:
|
|
|
210
256
|
method = detect_installation_method()
|
|
211
257
|
command = get_update_command(method)
|
|
212
258
|
|
|
259
|
+
# Handle uvx (ephemeral) installations
|
|
260
|
+
if method == "uvx" or command is None:
|
|
261
|
+
return (
|
|
262
|
+
False,
|
|
263
|
+
"You're running shotgun-sh via uvx (ephemeral mode). "
|
|
264
|
+
"To get the latest version, simply run 'uvx shotgun-sh' again, "
|
|
265
|
+
"or install permanently with 'uv tool install shotgun-sh'.",
|
|
266
|
+
)
|
|
267
|
+
|
|
213
268
|
# Perform update
|
|
214
269
|
try:
|
|
215
270
|
logger.info(f"Updating shotgun-sh using {method}...")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shotgun-sh
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
4
4
|
Summary: AI-powered research, planning, and task management CLI tool
|
|
5
5
|
Project-URL: Homepage, https://shotgun.sh/
|
|
6
6
|
Project-URL: Repository, https://github.com/shotgun-sh/shotgun
|
|
@@ -26,7 +26,7 @@ Requires-Dist: genai-prices>=0.0.27
|
|
|
26
26
|
Requires-Dist: httpx>=0.27.0
|
|
27
27
|
Requires-Dist: jinja2>=3.1.0
|
|
28
28
|
Requires-Dist: kuzu>=0.7.0
|
|
29
|
-
Requires-Dist: logfire
|
|
29
|
+
Requires-Dist: logfire>=2.0.0
|
|
30
30
|
Requires-Dist: openai>=1.0.0
|
|
31
31
|
Requires-Dist: packaging>=23.0
|
|
32
32
|
Requires-Dist: posthog>=3.0.0
|
|
@@ -36,6 +36,7 @@ Requires-Dist: sentencepiece>=0.2.0
|
|
|
36
36
|
Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
|
|
37
37
|
Requires-Dist: tenacity>=8.0.0
|
|
38
38
|
Requires-Dist: textual-dev>=1.7.0
|
|
39
|
+
Requires-Dist: textual-serve>=0.1.0
|
|
39
40
|
Requires-Dist: textual>=6.1.0
|
|
40
41
|
Requires-Dist: tiktoken>=0.7.0
|
|
41
42
|
Requires-Dist: tree-sitter-go>=0.23.0
|
|
@@ -83,13 +84,30 @@ Every research finding, every architectural decision, every "here's why we didn'
|
|
|
83
84
|
|
|
84
85
|
## Installation
|
|
85
86
|
|
|
86
|
-
### Using
|
|
87
|
+
### Using uvx (Recommended)
|
|
88
|
+
|
|
89
|
+
**Quick start (ephemeral):**
|
|
90
|
+
```bash
|
|
91
|
+
uvx shotgun-sh
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Install permanently:**
|
|
95
|
+
```bash
|
|
96
|
+
uv tool install shotgun-sh
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Why uvx?** It's 10-100x faster than pipx and handles binary wheels more reliably. If you don't have `uv` installed, get it at [astral.sh/uv](https://astral.sh/uv) or `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
|
100
|
+
|
|
101
|
+
### Using pipx
|
|
87
102
|
|
|
88
103
|
```bash
|
|
89
104
|
pipx install shotgun-sh
|
|
90
105
|
```
|
|
91
106
|
|
|
92
|
-
|
|
107
|
+
If you encounter build errors with kuzu on macOS:
|
|
108
|
+
```bash
|
|
109
|
+
pipx install --pip-args="--only-binary kuzu" shotgun-sh
|
|
110
|
+
```
|
|
93
111
|
|
|
94
112
|
### Using pip
|
|
95
113
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
shotgun/__init__.py,sha256=P40K0fnIsb7SKcQrFnXZ4aREjpWchVDhvM1HxI4cyIQ,104
|
|
2
|
-
shotgun/api_endpoints.py,sha256=
|
|
2
|
+
shotgun/api_endpoints.py,sha256=fBMTAQDyDXFY622MSrya5i_0ur_KYrCNg6wMf7QJVb4,627
|
|
3
3
|
shotgun/build_constants.py,sha256=hDFr6eO0lwN0iCqHQ1A5s0D68txR8sYrTJLGa7tSi0o,654
|
|
4
4
|
shotgun/logging_config.py,sha256=UKenihvgH8OA3W0b8ZFcItYaFJVe9MlsMYlcevyW1HY,7440
|
|
5
|
-
shotgun/main.py,sha256=
|
|
5
|
+
shotgun/main.py,sha256=4HLlnSmkVdm97yFCsXpY7d598xkOL7jtJLTq5sHPzrs,6762
|
|
6
6
|
shotgun/posthog_telemetry.py,sha256=TOiyBtLg21SttHGWKc4-e-PQgpbq6Uz_4OzlvlxMcZ0,6099
|
|
7
7
|
shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
shotgun/sentry_telemetry.py,sha256=VD8es-tREfgtRKhDsEVvqpo0_kM_ab6iVm2lkOEmTlI,2950
|
|
@@ -23,9 +23,9 @@ shotgun/agents/tasks.py,sha256=AuriwtDn6uZz2G0eKfqBHYQrxYfJlbiAd-fcsw9lU3I,2949
|
|
|
23
23
|
shotgun/agents/usage_manager.py,sha256=5d9JC4_cthXwhTSytMfMExMDAUYp8_nkPepTJZXk13w,5017
|
|
24
24
|
shotgun/agents/config/__init__.py,sha256=Fl8K_81zBpm-OfOW27M_WWLSFdaHHek6lWz95iDREjQ,318
|
|
25
25
|
shotgun/agents/config/constants.py,sha256=JNuLpeBUKikEsxGSjwX3RVWUQpbCKnDKstF2NczuDqk,932
|
|
26
|
-
shotgun/agents/config/manager.py,sha256=
|
|
27
|
-
shotgun/agents/config/models.py,sha256=
|
|
28
|
-
shotgun/agents/config/provider.py,sha256=
|
|
26
|
+
shotgun/agents/config/manager.py,sha256=Z8EQoWPQM7uj3MyklmxHQ1GR6va1uv9BjT5DDvAv3pY,18920
|
|
27
|
+
shotgun/agents/config/models.py,sha256=tM0JnMf-hWWzJCmOsJBA6tIiwrdFTQI_4B0zYFNg6CU,5736
|
|
28
|
+
shotgun/agents/config/provider.py,sha256=8TLiyTowkjT5p0zjocv9zGjem5hgQKnNuiN1nESguok,13412
|
|
29
29
|
shotgun/agents/history/__init__.py,sha256=XFQj2a6fxDqVg0Q3juvN9RjV_RJbgvFZtQOCOjVJyp4,147
|
|
30
30
|
shotgun/agents/history/compaction.py,sha256=9RMpG0aY_7L4TecbgwHSOkGtbd9W5XZTg-MbzZmNl00,3515
|
|
31
31
|
shotgun/agents/history/constants.py,sha256=yWY8rrTZarLA3flCCMB_hS2NMvUDRDTwP4D4j7MIh1w,446
|
|
@@ -64,7 +64,7 @@ shotgun/cli/plan.py,sha256=T-eu-I9z-dSoKqJ-KI8X5i5Mm0VL1BfornxRiUjTgnk,2324
|
|
|
64
64
|
shotgun/cli/research.py,sha256=qvBBtX3Wyn6pDZlJpcEvbeK-0iTOXegi71tm8HKVYaE,2490
|
|
65
65
|
shotgun/cli/specify.py,sha256=ErRQ72Zc75fmxopZbKy0vvnLPuYBLsGynpjj1X6-BwI,2166
|
|
66
66
|
shotgun/cli/tasks.py,sha256=17qWoGCVYpNIxa2vaoIH1P-xz2RcGLaK8SF4JlPsOWI,2420
|
|
67
|
-
shotgun/cli/update.py,sha256=
|
|
67
|
+
shotgun/cli/update.py,sha256=sc3uuw3AXFF0kpskWah1JEoTwrKv67fCnqp9BjeND3o,5328
|
|
68
68
|
shotgun/cli/utils.py,sha256=umVWXDx8pelovMk-nT8B7m0c39AKY9hHsuAMnbw_Hcg,732
|
|
69
69
|
shotgun/cli/codebase/__init__.py,sha256=rKdvx33p0i_BYbNkz5_4DCFgEMwzOOqLi9f5p7XTLKM,73
|
|
70
70
|
shotgun/cli/codebase/commands.py,sha256=1N2yOGmok0ZarqXPIpWGcsQrwm_ZJcyWiMxy6tm0j70,8711
|
|
@@ -78,7 +78,7 @@ shotgun/codebase/core/code_retrieval.py,sha256=_JVyyQKHDFm3dxOOua1mw9eIIOHIVz3-I
|
|
|
78
78
|
shotgun/codebase/core/cypher_models.py,sha256=Yfysfa9lLguILftkmtuJCN3kLBFIo7WW7NigM-Zr-W4,1735
|
|
79
79
|
shotgun/codebase/core/ingestor.py,sha256=CNYbdoJycnbA2psYCD9uKcUwIe3Ao7I7T6NrPhTQE9k,64613
|
|
80
80
|
shotgun/codebase/core/language_config.py,sha256=vsqHyuFnumRPRBV1lMOxWKNOIiClO6FyfKQR0fGrtl4,8934
|
|
81
|
-
shotgun/codebase/core/manager.py,sha256=
|
|
81
|
+
shotgun/codebase/core/manager.py,sha256=1ykkGSrR4ooEKiluGp-17q3n9hd2LSjonvZL10Xb0pk,66697
|
|
82
82
|
shotgun/codebase/core/nl_query.py,sha256=kPoSJXBlm5rLhzOofZhqPVMJ_Lj3rV2H6sld6BwtMdg,16115
|
|
83
83
|
shotgun/codebase/core/parser_loader.py,sha256=LZRrDS8Sp518jIu3tQW-BxdwJ86lnsTteI478ER9Td8,4278
|
|
84
84
|
shotgun/llm_proxy/__init__.py,sha256=3ST3ygtf2sXXSOjIFHxVZ5xqRbT3TF7jpNHwuZAtIwA,452
|
|
@@ -119,7 +119,7 @@ shotgun/shotgun_web/client.py,sha256=n5DDuVfSa6VPZjhSsfSxQlSFOnhgDHyidRnB8Hv9XF4
|
|
|
119
119
|
shotgun/shotgun_web/constants.py,sha256=eNvtjlu81bAVQaCwZXOVjSpDopUm9pf34XuZEvuMiko,661
|
|
120
120
|
shotgun/shotgun_web/models.py,sha256=Ie9VfqKZM2tIJhIjentU9qLoNaMZvnUJaIu-xg9kQsA,1391
|
|
121
121
|
shotgun/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
122
|
-
shotgun/tui/app.py,sha256=
|
|
122
|
+
shotgun/tui/app.py,sha256=I4rPtHIp_XiO4Vy3TQe3Rjfp20xHkxDvijdlY7x6wi8,10636
|
|
123
123
|
shotgun/tui/filtered_codebase_service.py,sha256=lJ8gTMhIveTatmvmGLP299msWWTkVYKwvY_2FhuL2s4,1687
|
|
124
124
|
shotgun/tui/styles.tcss,sha256=ETyyw1bpMBOqTi5RLcAJUScdPWTvAWEqE9YcT0kVs_E,121
|
|
125
125
|
shotgun/tui/commands/__init__.py,sha256=8D5lvtpqMW5-fF7Bg3oJtUzU75cKOv6aUaHYYszydU8,2518
|
|
@@ -127,11 +127,12 @@ shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4Ugad
|
|
|
127
127
|
shotgun/tui/components/spinner.py,sha256=ovTDeaJ6FD6chZx_Aepia6R3UkPOVJ77EKHfRmn39MY,2427
|
|
128
128
|
shotgun/tui/components/splash.py,sha256=vppy9vEIEvywuUKRXn2y11HwXSRkQZHLYoVjhDVdJeU,1267
|
|
129
129
|
shotgun/tui/components/vertical_tail.py,sha256=kROwTaRjUwVB7H35dtmNcUVPQqNYvvfq7K2tXBKEb6c,638
|
|
130
|
-
shotgun/tui/screens/chat.py,sha256
|
|
130
|
+
shotgun/tui/screens/chat.py,sha256=-MPbvdeWSWLK2VTgPYicMXger2vMHA9t_YrEzaux-1Q,42255
|
|
131
131
|
shotgun/tui/screens/chat.tcss,sha256=2Yq3E23jxsySYsgZf4G1AYrYVcpX0UDW6kNNI0tDmtM,437
|
|
132
132
|
shotgun/tui/screens/directory_setup.py,sha256=lIZ1J4A6g5Q2ZBX8epW7BhR96Dmdcg22CyiM5S-I5WU,3237
|
|
133
133
|
shotgun/tui/screens/feedback.py,sha256=VxpW0PVxMp22ZvSfQkTtgixNrpEOlfWtekjqlVfYEjA,5708
|
|
134
|
-
shotgun/tui/screens/model_picker.py,sha256=
|
|
134
|
+
shotgun/tui/screens/model_picker.py,sha256=kPvBnMK20SJrAGAC0HHM4Yi8n7biHraz58eiE8jiPxg,11927
|
|
135
|
+
shotgun/tui/screens/pipx_migration.py,sha256=BY6R1Z__htCLjWwffXbHUpxfAk1nnLQnzGRUCmXfwiY,4321
|
|
135
136
|
shotgun/tui/screens/provider_config.py,sha256=UCnAzjXPoP7Y73gsXxZF2PNA4LdSgpgoGYwiOd6fERA,10902
|
|
136
137
|
shotgun/tui/screens/shotgun_auth.py,sha256=Y--7LZewV6gfDkucxymfAO7BCd7eI2C3H1ClDMztVio,10663
|
|
137
138
|
shotgun/tui/screens/splash.py,sha256=E2MsJihi3c9NY1L28o_MstDxGwrCnnV7zdq00MrGAsw,706
|
|
@@ -147,9 +148,9 @@ shotgun/utils/datetime_utils.py,sha256=x_uYmG1n9rkhSO2oR2uV9ttiuPL0nKa9os8YYaPfd
|
|
|
147
148
|
shotgun/utils/env_utils.py,sha256=ulM3BRi9ZhS7uC-zorGeDQm4SHvsyFuuU9BtVPqdrHY,1418
|
|
148
149
|
shotgun/utils/file_system_utils.py,sha256=l-0p1bEHF34OU19MahnRFdClHufThfGAjQ431teAIp0,1004
|
|
149
150
|
shotgun/utils/source_detection.py,sha256=Co6Q03R3fT771TF3RzB-70stfjNP2S4F_ArZKibwzm8,454
|
|
150
|
-
shotgun/utils/update_checker.py,sha256=
|
|
151
|
-
shotgun_sh-0.2.
|
|
152
|
-
shotgun_sh-0.2.
|
|
153
|
-
shotgun_sh-0.2.
|
|
154
|
-
shotgun_sh-0.2.
|
|
155
|
-
shotgun_sh-0.2.
|
|
151
|
+
shotgun/utils/update_checker.py,sha256=5pK9ZXEjgnE-BQLvibm9Fj6SJHVYeG0U-WspRf0bJec,9660
|
|
152
|
+
shotgun_sh-0.2.10.dist-info/METADATA,sha256=57UVH3orQmmh8ahq3pJYX6NpHiGZCxPvPJV0x1MWr2k,4665
|
|
153
|
+
shotgun_sh-0.2.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
154
|
+
shotgun_sh-0.2.10.dist-info/entry_points.txt,sha256=GQmtjKaPtviqYOuB3C0SMGlG5RZS9-VDDIKxV_IVHmY,75
|
|
155
|
+
shotgun_sh-0.2.10.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
|
|
156
|
+
shotgun_sh-0.2.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|