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/clear.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Clear command for shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from shotgun.agents.conversation import ConversationManager
|
|
10
|
+
from shotgun.logging_config import get_logger
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="clear", help="Clear the conversation history", no_args_is_help=False
|
|
14
|
+
)
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback(invoke_without_command=True)
|
|
20
|
+
def clear() -> None:
|
|
21
|
+
"""Clear the current conversation history.
|
|
22
|
+
|
|
23
|
+
This command deletes the conversation file at ~/.shotgun-sh/conversation.json,
|
|
24
|
+
removing all conversation history. Other files in ~/.shotgun-sh/ (config, usage,
|
|
25
|
+
codebases, logs) are preserved.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
# Get conversation file path
|
|
29
|
+
conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
|
|
30
|
+
|
|
31
|
+
# Check if file exists
|
|
32
|
+
if not conversation_file.exists():
|
|
33
|
+
console.print(
|
|
34
|
+
"[yellow]No conversation file found.[/yellow] Nothing to clear.",
|
|
35
|
+
style="bold",
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Clear the conversation
|
|
40
|
+
manager = ConversationManager(conversation_file)
|
|
41
|
+
asyncio.run(manager.clear())
|
|
42
|
+
|
|
43
|
+
console.print(
|
|
44
|
+
"[green]✓[/green] Conversation cleared successfully", style="bold"
|
|
45
|
+
)
|
|
46
|
+
logger.info("Conversation cleared successfully")
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
console.print(
|
|
50
|
+
f"[red]Error:[/red] Failed to clear conversation: {e}", style="bold"
|
|
51
|
+
)
|
|
52
|
+
logger.debug("Full traceback:", exc_info=True)
|
|
53
|
+
raise typer.Exit(code=1) from e
|
shotgun/cli/compact.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Compact command for shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from pydantic_ai.usage import RequestUsage
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from shotgun.agents.config import get_provider_model
|
|
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
|
+
)
|
|
18
|
+
from shotgun.cli.models import OutputFormat
|
|
19
|
+
from shotgun.logging_config import get_logger
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(
|
|
22
|
+
name="compact", help="Compact the conversation history", no_args_is_help=False
|
|
23
|
+
)
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback(invoke_without_command=True)
|
|
29
|
+
def compact(
|
|
30
|
+
format: Annotated[
|
|
31
|
+
OutputFormat,
|
|
32
|
+
typer.Option(
|
|
33
|
+
"--format",
|
|
34
|
+
"-f",
|
|
35
|
+
help="Output format: markdown or json",
|
|
36
|
+
),
|
|
37
|
+
] = OutputFormat.MARKDOWN,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Compact the current conversation history to reduce size.
|
|
40
|
+
|
|
41
|
+
This command compacts the conversation in ~/.shotgun-sh/conversation.json
|
|
42
|
+
by summarizing older messages while preserving recent context. The compacted
|
|
43
|
+
conversation is automatically saved back to the file.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
result = asyncio.run(compact_conversation())
|
|
47
|
+
|
|
48
|
+
if format == OutputFormat.JSON:
|
|
49
|
+
# Output as JSON
|
|
50
|
+
console.print_json(json.dumps(result, indent=2))
|
|
51
|
+
else:
|
|
52
|
+
# Output as markdown
|
|
53
|
+
console.print(format_markdown(result))
|
|
54
|
+
|
|
55
|
+
except FileNotFoundError as e:
|
|
56
|
+
console.print(
|
|
57
|
+
f"[red]Error:[/red] {e}\n\n"
|
|
58
|
+
"No conversation found. Start a TUI session first with: [cyan]shotgun[/cyan]",
|
|
59
|
+
style="bold",
|
|
60
|
+
)
|
|
61
|
+
raise typer.Exit(code=1) from e
|
|
62
|
+
except Exception as e:
|
|
63
|
+
console.print(
|
|
64
|
+
f"[red]Error:[/red] Failed to compact conversation: {e}", style="bold"
|
|
65
|
+
)
|
|
66
|
+
logger.debug("Full traceback:", exc_info=True)
|
|
67
|
+
raise typer.Exit(code=1) from e
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def compact_conversation() -> dict[str, Any]:
|
|
71
|
+
"""Compact the conversation and return statistics.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dictionary with compaction statistics including before/after metrics
|
|
75
|
+
"""
|
|
76
|
+
# Get conversation file path
|
|
77
|
+
conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
|
|
78
|
+
|
|
79
|
+
if not conversation_file.exists():
|
|
80
|
+
raise FileNotFoundError(f"Conversation file not found at {conversation_file}")
|
|
81
|
+
|
|
82
|
+
# Load conversation
|
|
83
|
+
manager = ConversationManager(conversation_file)
|
|
84
|
+
conversation = await manager.load()
|
|
85
|
+
|
|
86
|
+
if not conversation:
|
|
87
|
+
raise ValueError("Conversation file is empty or corrupted")
|
|
88
|
+
|
|
89
|
+
# Get agent messages only (not UI messages)
|
|
90
|
+
agent_messages = conversation.get_agent_messages()
|
|
91
|
+
|
|
92
|
+
if not agent_messages:
|
|
93
|
+
raise ValueError("No agent messages found in conversation")
|
|
94
|
+
|
|
95
|
+
# Get model config
|
|
96
|
+
model_config = await get_provider_model()
|
|
97
|
+
|
|
98
|
+
# Calculate before metrics
|
|
99
|
+
original_message_count = len(agent_messages)
|
|
100
|
+
original_tokens = await estimate_tokens_from_messages(agent_messages, model_config)
|
|
101
|
+
|
|
102
|
+
# For CLI, we can call token_limit_compactor directly without full AgentDeps
|
|
103
|
+
# since we only need the model config and message history
|
|
104
|
+
# Create a minimal context object for compaction
|
|
105
|
+
class CompactContext:
|
|
106
|
+
def __init__(self, model_config: Any, usage: RequestUsage) -> None:
|
|
107
|
+
self.deps = type("Deps", (), {"llm_model": model_config})()
|
|
108
|
+
self.usage = usage
|
|
109
|
+
|
|
110
|
+
# Create minimal usage info for compaction check
|
|
111
|
+
usage = RequestUsage(input_tokens=original_tokens, output_tokens=0)
|
|
112
|
+
ctx = CompactContext(model_config, usage)
|
|
113
|
+
|
|
114
|
+
# Apply compaction with force=True to bypass threshold checks
|
|
115
|
+
compacted_messages = await token_limit_compactor(ctx, agent_messages, force=True)
|
|
116
|
+
|
|
117
|
+
# Calculate after metrics
|
|
118
|
+
compacted_message_count = len(compacted_messages)
|
|
119
|
+
compacted_tokens = await estimate_tokens_from_messages(
|
|
120
|
+
compacted_messages, model_config
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Calculate reduction percentages
|
|
124
|
+
message_reduction = (
|
|
125
|
+
((original_message_count - compacted_message_count) / original_message_count)
|
|
126
|
+
* 100
|
|
127
|
+
if original_message_count > 0
|
|
128
|
+
else 0
|
|
129
|
+
)
|
|
130
|
+
token_reduction = (
|
|
131
|
+
((original_tokens - compacted_tokens) / original_tokens) * 100
|
|
132
|
+
if original_tokens > 0
|
|
133
|
+
else 0
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Save compacted conversation
|
|
137
|
+
conversation.set_agent_messages(compacted_messages)
|
|
138
|
+
await manager.save(conversation)
|
|
139
|
+
|
|
140
|
+
logger.info(
|
|
141
|
+
f"Compacted conversation: {original_message_count} → {compacted_message_count} messages "
|
|
142
|
+
f"({message_reduction:.1f}% reduction)"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"success": True,
|
|
147
|
+
"before": {
|
|
148
|
+
"messages": original_message_count,
|
|
149
|
+
"estimated_tokens": original_tokens,
|
|
150
|
+
},
|
|
151
|
+
"after": {
|
|
152
|
+
"messages": compacted_message_count,
|
|
153
|
+
"estimated_tokens": compacted_tokens,
|
|
154
|
+
},
|
|
155
|
+
"reduction": {
|
|
156
|
+
"messages_percent": round(message_reduction, 1),
|
|
157
|
+
"tokens_percent": round(token_reduction, 1),
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def format_markdown(result: dict[str, Any]) -> str:
|
|
163
|
+
"""Format compaction result as markdown.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
result: Dictionary with compaction statistics
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Formatted markdown string
|
|
170
|
+
"""
|
|
171
|
+
before = result["before"]
|
|
172
|
+
after = result["after"]
|
|
173
|
+
reduction = result["reduction"]
|
|
174
|
+
|
|
175
|
+
return f"""# Conversation Compacted ✓
|
|
176
|
+
|
|
177
|
+
## Before
|
|
178
|
+
- **Messages:** {before["messages"]:,}
|
|
179
|
+
- **Estimated Tokens:** {before["estimated_tokens"]:,}
|
|
180
|
+
|
|
181
|
+
## After
|
|
182
|
+
- **Messages:** {after["messages"]:,}
|
|
183
|
+
- **Estimated Tokens:** {after["estimated_tokens"]:,}
|
|
184
|
+
|
|
185
|
+
## Reduction
|
|
186
|
+
- **Messages:** {reduction["messages_percent"]}%
|
|
187
|
+
- **Tokens:** {reduction["tokens_percent"]}%
|
|
188
|
+
"""
|
shotgun/cli/config.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Configuration management CLI commands."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
from typing import Annotated, Any
|
|
5
6
|
|
|
@@ -44,7 +45,7 @@ def init(
|
|
|
44
45
|
console.print()
|
|
45
46
|
|
|
46
47
|
# Initialize with defaults
|
|
47
|
-
config_manager.initialize()
|
|
48
|
+
asyncio.run(config_manager.initialize())
|
|
48
49
|
|
|
49
50
|
# Ask for provider
|
|
50
51
|
provider_choices = ["openai", "anthropic", "google"]
|
|
@@ -76,7 +77,7 @@ def init(
|
|
|
76
77
|
|
|
77
78
|
if api_key:
|
|
78
79
|
# update_provider will automatically set selected_model for first provider
|
|
79
|
-
config_manager.update_provider(provider, api_key=api_key)
|
|
80
|
+
asyncio.run(config_manager.update_provider(provider, api_key=api_key))
|
|
80
81
|
|
|
81
82
|
console.print(
|
|
82
83
|
f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
|
|
@@ -84,7 +85,7 @@ def init(
|
|
|
84
85
|
console.print("🎯 You can now use Shotgun with your configured provider!")
|
|
85
86
|
|
|
86
87
|
else:
|
|
87
|
-
config_manager.initialize()
|
|
88
|
+
asyncio.run(config_manager.initialize())
|
|
88
89
|
console.print(f"✅ Configuration initialized at {config_manager.config_path}")
|
|
89
90
|
|
|
90
91
|
|
|
@@ -112,7 +113,7 @@ def set(
|
|
|
112
113
|
|
|
113
114
|
try:
|
|
114
115
|
if api_key:
|
|
115
|
-
config_manager.update_provider(provider, api_key=api_key)
|
|
116
|
+
asyncio.run(config_manager.update_provider(provider, api_key=api_key))
|
|
116
117
|
|
|
117
118
|
console.print(f"✅ Configuration updated for {provider}")
|
|
118
119
|
|
|
@@ -133,8 +134,10 @@ def get(
|
|
|
133
134
|
] = False,
|
|
134
135
|
) -> None:
|
|
135
136
|
"""Display current configuration."""
|
|
137
|
+
import asyncio
|
|
138
|
+
|
|
136
139
|
config_manager = get_config_manager()
|
|
137
|
-
config = config_manager.load()
|
|
140
|
+
config = asyncio.run(config_manager.load())
|
|
138
141
|
|
|
139
142
|
if json_output:
|
|
140
143
|
# Convert to dict and mask secrets
|
shotgun/cli/context.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Context command for shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from shotgun.agents.config import get_provider_model
|
|
13
|
+
from shotgun.agents.context_analyzer import (
|
|
14
|
+
ContextAnalysisOutput,
|
|
15
|
+
ContextAnalyzer,
|
|
16
|
+
ContextFormatter,
|
|
17
|
+
)
|
|
18
|
+
from shotgun.agents.conversation import ConversationManager
|
|
19
|
+
from shotgun.cli.models import OutputFormat
|
|
20
|
+
from shotgun.llm_proxy import BudgetInfo, LiteLLMProxyClient
|
|
21
|
+
from shotgun.logging_config import get_logger
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name="context", help="Analyze conversation context usage", no_args_is_help=False
|
|
25
|
+
)
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback(invoke_without_command=True)
|
|
31
|
+
def context(
|
|
32
|
+
format: Annotated[
|
|
33
|
+
OutputFormat,
|
|
34
|
+
typer.Option(
|
|
35
|
+
"--format",
|
|
36
|
+
"-f",
|
|
37
|
+
help="Output format: markdown or json",
|
|
38
|
+
),
|
|
39
|
+
] = OutputFormat.MARKDOWN,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Analyze the current conversation's context usage.
|
|
42
|
+
|
|
43
|
+
This command analyzes the agent's message history from ~/.shotgun-sh/conversation.json
|
|
44
|
+
and displays token usage breakdown by message type. Only agent context is counted
|
|
45
|
+
(UI elements like hints are excluded).
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
result = asyncio.run(analyze_context())
|
|
49
|
+
|
|
50
|
+
if format == OutputFormat.JSON:
|
|
51
|
+
# Output as JSON
|
|
52
|
+
console.print_json(json.dumps(result.json_data, indent=2))
|
|
53
|
+
else:
|
|
54
|
+
# Output as plain text (Markdown() reformats and makes categories inline)
|
|
55
|
+
console.print(result.markdown)
|
|
56
|
+
|
|
57
|
+
except FileNotFoundError as e:
|
|
58
|
+
console.print(
|
|
59
|
+
f"[red]Error:[/red] {e}\n\n"
|
|
60
|
+
"No conversation found. Start a TUI session first with: [cyan]shotgun[/cyan]",
|
|
61
|
+
style="bold",
|
|
62
|
+
)
|
|
63
|
+
raise typer.Exit(code=1) from e
|
|
64
|
+
except Exception as e:
|
|
65
|
+
console.print(f"[red]Error:[/red] Failed to analyze context: {e}", style="bold")
|
|
66
|
+
logger.debug("Full traceback:", exc_info=True)
|
|
67
|
+
raise typer.Exit(code=1) from e
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def analyze_context() -> ContextAnalysisOutput:
|
|
71
|
+
"""Analyze the conversation context and return structured data.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
ContextAnalysisOutput with both markdown and JSON representations of the analysis
|
|
75
|
+
"""
|
|
76
|
+
# Get conversation file path
|
|
77
|
+
conversation_file = Path.home() / ".shotgun-sh" / "conversation.json"
|
|
78
|
+
|
|
79
|
+
if not conversation_file.exists():
|
|
80
|
+
raise FileNotFoundError(f"Conversation file not found at {conversation_file}")
|
|
81
|
+
|
|
82
|
+
# Load conversation
|
|
83
|
+
manager = ConversationManager(conversation_file)
|
|
84
|
+
conversation = await manager.load()
|
|
85
|
+
|
|
86
|
+
if not conversation:
|
|
87
|
+
raise ValueError("Conversation file is empty or corrupted")
|
|
88
|
+
|
|
89
|
+
# Get agent messages only (not UI messages)
|
|
90
|
+
agent_messages = conversation.get_agent_messages()
|
|
91
|
+
|
|
92
|
+
if not agent_messages:
|
|
93
|
+
raise ValueError("No agent messages found in conversation")
|
|
94
|
+
|
|
95
|
+
# Get model config (use default provider settings)
|
|
96
|
+
model_config = await get_provider_model()
|
|
97
|
+
|
|
98
|
+
# Debug: Log the model being used
|
|
99
|
+
logger.debug(f"Using model: {model_config.name.value}")
|
|
100
|
+
logger.debug(f"Provider: {model_config.provider.value}")
|
|
101
|
+
logger.debug(f"Key provider: {model_config.key_provider.value}")
|
|
102
|
+
logger.debug(f"Max input tokens: {model_config.max_input_tokens}")
|
|
103
|
+
|
|
104
|
+
# Analyze with ContextAnalyzer
|
|
105
|
+
analyzer = ContextAnalyzer(model_config)
|
|
106
|
+
# For CLI, agent_messages and ui_message_history are the same (no hints in CLI mode)
|
|
107
|
+
analysis = await analyzer.analyze_conversation(agent_messages, list(agent_messages))
|
|
108
|
+
|
|
109
|
+
# Use formatter to generate markdown and JSON
|
|
110
|
+
markdown = ContextFormatter.format_markdown(analysis)
|
|
111
|
+
json_data = ContextFormatter.format_json(analysis)
|
|
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
|
+
|
|
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/feedback.py
CHANGED
|
@@ -28,9 +28,11 @@ def send_feedback(
|
|
|
28
28
|
],
|
|
29
29
|
) -> None:
|
|
30
30
|
"""Initialize Shotgun configuration."""
|
|
31
|
+
import asyncio
|
|
32
|
+
|
|
31
33
|
config_manager = get_config_manager()
|
|
32
|
-
config_manager.load()
|
|
33
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
34
|
+
asyncio.run(config_manager.load())
|
|
35
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
34
36
|
|
|
35
37
|
if not description:
|
|
36
38
|
console.print(
|
shotgun/cli/models.py
CHANGED
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())
|