shotgun-sh 0.1.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (94) hide show
  1. shotgun/__init__.py +3 -0
  2. shotgun/agents/__init__.py +1 -0
  3. shotgun/agents/agent_manager.py +196 -0
  4. shotgun/agents/common.py +295 -0
  5. shotgun/agents/config/__init__.py +13 -0
  6. shotgun/agents/config/manager.py +215 -0
  7. shotgun/agents/config/models.py +120 -0
  8. shotgun/agents/config/provider.py +91 -0
  9. shotgun/agents/history/__init__.py +5 -0
  10. shotgun/agents/history/history_processors.py +213 -0
  11. shotgun/agents/models.py +94 -0
  12. shotgun/agents/plan.py +119 -0
  13. shotgun/agents/research.py +131 -0
  14. shotgun/agents/tasks.py +122 -0
  15. shotgun/agents/tools/__init__.py +26 -0
  16. shotgun/agents/tools/codebase/__init__.py +28 -0
  17. shotgun/agents/tools/codebase/codebase_shell.py +256 -0
  18. shotgun/agents/tools/codebase/directory_lister.py +141 -0
  19. shotgun/agents/tools/codebase/file_read.py +144 -0
  20. shotgun/agents/tools/codebase/models.py +252 -0
  21. shotgun/agents/tools/codebase/query_graph.py +67 -0
  22. shotgun/agents/tools/codebase/retrieve_code.py +81 -0
  23. shotgun/agents/tools/file_management.py +130 -0
  24. shotgun/agents/tools/user_interaction.py +36 -0
  25. shotgun/agents/tools/web_search.py +69 -0
  26. shotgun/cli/__init__.py +1 -0
  27. shotgun/cli/codebase/__init__.py +5 -0
  28. shotgun/cli/codebase/commands.py +202 -0
  29. shotgun/cli/codebase/models.py +21 -0
  30. shotgun/cli/config.py +261 -0
  31. shotgun/cli/models.py +10 -0
  32. shotgun/cli/plan.py +65 -0
  33. shotgun/cli/research.py +78 -0
  34. shotgun/cli/tasks.py +71 -0
  35. shotgun/cli/utils.py +25 -0
  36. shotgun/codebase/__init__.py +12 -0
  37. shotgun/codebase/core/__init__.py +46 -0
  38. shotgun/codebase/core/change_detector.py +358 -0
  39. shotgun/codebase/core/code_retrieval.py +243 -0
  40. shotgun/codebase/core/ingestor.py +1497 -0
  41. shotgun/codebase/core/language_config.py +297 -0
  42. shotgun/codebase/core/manager.py +1554 -0
  43. shotgun/codebase/core/nl_query.py +327 -0
  44. shotgun/codebase/core/parser_loader.py +152 -0
  45. shotgun/codebase/models.py +107 -0
  46. shotgun/codebase/service.py +148 -0
  47. shotgun/logging_config.py +172 -0
  48. shotgun/main.py +73 -0
  49. shotgun/prompts/__init__.py +5 -0
  50. shotgun/prompts/agents/__init__.py +1 -0
  51. shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
  52. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
  53. shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
  54. shotgun/prompts/agents/plan.j2 +57 -0
  55. shotgun/prompts/agents/research.j2 +38 -0
  56. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
  57. shotgun/prompts/agents/state/system_state.j2 +1 -0
  58. shotgun/prompts/agents/tasks.j2 +67 -0
  59. shotgun/prompts/codebase/__init__.py +1 -0
  60. shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
  61. shotgun/prompts/codebase/cypher_system.j2 +28 -0
  62. shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
  63. shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
  64. shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
  65. shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
  66. shotgun/prompts/history/__init__.py +1 -0
  67. shotgun/prompts/history/summarization.j2 +46 -0
  68. shotgun/prompts/loader.py +140 -0
  69. shotgun/prompts/user/research.j2 +5 -0
  70. shotgun/py.typed +0 -0
  71. shotgun/sdk/__init__.py +13 -0
  72. shotgun/sdk/codebase.py +195 -0
  73. shotgun/sdk/exceptions.py +17 -0
  74. shotgun/sdk/models.py +189 -0
  75. shotgun/sdk/services.py +23 -0
  76. shotgun/telemetry.py +68 -0
  77. shotgun/tui/__init__.py +0 -0
  78. shotgun/tui/app.py +49 -0
  79. shotgun/tui/components/prompt_input.py +69 -0
  80. shotgun/tui/components/spinner.py +86 -0
  81. shotgun/tui/components/splash.py +25 -0
  82. shotgun/tui/components/vertical_tail.py +28 -0
  83. shotgun/tui/screens/chat.py +415 -0
  84. shotgun/tui/screens/chat.tcss +28 -0
  85. shotgun/tui/screens/provider_config.py +221 -0
  86. shotgun/tui/screens/splash.py +31 -0
  87. shotgun/tui/styles.tcss +10 -0
  88. shotgun/utils/__init__.py +5 -0
  89. shotgun/utils/file_system_utils.py +31 -0
  90. shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
  91. shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
  92. shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
  93. shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
  94. shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,202 @@
1
+ """Codebase management CLI commands."""
2
+
3
+ import asyncio
4
+ import traceback
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from shotgun.codebase.models import CodebaseGraph, QueryType
11
+ from shotgun.logging_config import get_logger
12
+ from shotgun.sdk.codebase import CodebaseSDK
13
+ from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
14
+
15
+ from ..models import OutputFormat
16
+ from ..utils import output_result
17
+ from .models import ErrorResult
18
+
19
+ app = typer.Typer(
20
+ name="codebase",
21
+ help="Manage and query code knowledge graphs",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+ # Set up logger but it will be suppressed by default
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ @app.command(name="list")
30
+ def list_codebases(
31
+ format_type: Annotated[
32
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
33
+ ] = OutputFormat.TEXT,
34
+ ) -> None:
35
+ """List all indexed codebases."""
36
+ sdk = CodebaseSDK()
37
+
38
+ try:
39
+ result = asyncio.run(sdk.list_codebases())
40
+ output_result(result, format_type)
41
+ except Exception as e:
42
+ error_result = ErrorResult(
43
+ error_message=f"Error listing codebases: {e}",
44
+ details=f"Full traceback:\n{traceback.format_exc()}",
45
+ )
46
+ output_result(error_result, format_type)
47
+ raise typer.Exit(1) from e
48
+
49
+
50
+ @app.command()
51
+ def index(
52
+ path: Annotated[str, typer.Argument(help="Path to repository to index")],
53
+ name: Annotated[
54
+ str, typer.Option("--name", "-n", help="Human-readable name for the codebase")
55
+ ],
56
+ format_type: Annotated[
57
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
58
+ ] = OutputFormat.TEXT,
59
+ ) -> None:
60
+ """Index a new codebase."""
61
+ sdk = CodebaseSDK()
62
+
63
+ try:
64
+ repo_path = Path(path)
65
+ result = asyncio.run(sdk.index_codebase(repo_path, name))
66
+ output_result(result, format_type)
67
+ except InvalidPathError as e:
68
+ error_result = ErrorResult(error_message=str(e))
69
+ output_result(error_result, format_type)
70
+ raise typer.Exit(1) from e
71
+ except Exception as e:
72
+ error_result = ErrorResult(
73
+ error_message=f"Error indexing codebase: {e}",
74
+ details=f"Full traceback:\n{traceback.format_exc()}",
75
+ )
76
+ output_result(error_result, format_type)
77
+ raise typer.Exit(1) from e
78
+
79
+
80
+ @app.command()
81
+ def delete(
82
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to delete")],
83
+ format_type: Annotated[
84
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
85
+ ] = OutputFormat.TEXT,
86
+ ) -> None:
87
+ """Delete an indexed codebase."""
88
+ sdk = CodebaseSDK()
89
+
90
+ # CLI-specific confirmation callback
91
+ def cli_confirm(graph: CodebaseGraph) -> bool:
92
+ return typer.confirm(
93
+ f"Are you sure you want to delete codebase '{graph.name}' ({graph_id})?"
94
+ )
95
+
96
+ try:
97
+ result = asyncio.run(sdk.delete_codebase(graph_id, cli_confirm))
98
+ output_result(result, format_type)
99
+ if not result.deleted and not result.cancelled:
100
+ raise typer.Exit(1)
101
+ except CodebaseNotFoundError as e:
102
+ error_result = ErrorResult(error_message=str(e))
103
+ output_result(error_result, format_type)
104
+ raise typer.Exit(1) from e
105
+ except Exception as e:
106
+ error_result = ErrorResult(
107
+ error_message=f"Error deleting codebase: {e}",
108
+ details=f"Full traceback:\n{traceback.format_exc()}",
109
+ )
110
+ output_result(error_result, format_type)
111
+ raise typer.Exit(1) from e
112
+
113
+
114
+ @app.command()
115
+ def info(
116
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to show info for")],
117
+ format_type: Annotated[
118
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
119
+ ] = OutputFormat.TEXT,
120
+ ) -> None:
121
+ """Show detailed information about a codebase."""
122
+ sdk = CodebaseSDK()
123
+
124
+ try:
125
+ result = asyncio.run(sdk.get_info(graph_id))
126
+ output_result(result, format_type)
127
+ except CodebaseNotFoundError as e:
128
+ error_result = ErrorResult(error_message=str(e))
129
+ output_result(error_result, format_type)
130
+ raise typer.Exit(1) from e
131
+ except Exception as e:
132
+ error_result = ErrorResult(
133
+ error_message=f"Error getting codebase info: {e}",
134
+ details=f"Full traceback:\n{traceback.format_exc()}",
135
+ )
136
+ output_result(error_result, format_type)
137
+ raise typer.Exit(1) from e
138
+
139
+
140
+ @app.command()
141
+ def query(
142
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to query")],
143
+ query_text: Annotated[
144
+ str, typer.Argument(help="Query text (natural language or Cypher)")
145
+ ],
146
+ cypher: Annotated[
147
+ bool,
148
+ typer.Option(
149
+ "--cypher", help="Treat query as Cypher instead of natural language"
150
+ ),
151
+ ] = False,
152
+ format_type: Annotated[
153
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
154
+ ] = OutputFormat.TEXT,
155
+ ) -> None:
156
+ """Query a codebase using natural language or Cypher."""
157
+
158
+ try:
159
+ sdk = CodebaseSDK()
160
+ query_type = QueryType.CYPHER if cypher else QueryType.NATURAL_LANGUAGE
161
+ result = asyncio.run(sdk.query_codebase(graph_id, query_text, query_type))
162
+ output_result(result, format_type)
163
+ except CodebaseNotFoundError as e:
164
+ error_result = ErrorResult(error_message=str(e))
165
+ output_result(error_result, format_type)
166
+ raise typer.Exit(1) from e
167
+
168
+ except Exception as e:
169
+ error_result = ErrorResult(
170
+ error_message=f"Error executing query: {e}",
171
+ details=f"Full traceback:\n{traceback.format_exc()}",
172
+ )
173
+ output_result(error_result, format_type)
174
+ raise typer.Exit(1) from e
175
+
176
+
177
+ @app.command()
178
+ def reindex(
179
+ graph_id: Annotated[str, typer.Argument(help="Graph ID to reindex")],
180
+ format_type: Annotated[
181
+ OutputFormat, typer.Option("--format", "-f", help="Output format")
182
+ ] = OutputFormat.TEXT,
183
+ ) -> None:
184
+ """Reindex an existing codebase."""
185
+
186
+ try:
187
+ sdk = CodebaseSDK()
188
+ result = asyncio.run(sdk.reindex_codebase(graph_id))
189
+ # Stats are always shown now that verbose is controlled by env var
190
+ output_result(result, format_type)
191
+ except CodebaseNotFoundError as e:
192
+ error_result = ErrorResult(error_message=str(e))
193
+ output_result(error_result, format_type)
194
+ raise typer.Exit(1) from e
195
+
196
+ except Exception as e:
197
+ error_result = ErrorResult(
198
+ error_message=f"Error reindexing codebase: {e}",
199
+ details=f"Full traceback:\n{traceback.format_exc()}",
200
+ )
201
+ output_result(error_result, format_type)
202
+ raise typer.Exit(1) from e
@@ -0,0 +1,21 @@
1
+ """Re-export SDK models for backward compatibility."""
2
+
3
+ from shotgun.sdk.models import (
4
+ DeleteResult,
5
+ ErrorResult,
6
+ IndexResult,
7
+ InfoResult,
8
+ ListResult,
9
+ QueryCommandResult,
10
+ ReindexResult,
11
+ )
12
+
13
+ __all__ = [
14
+ "ListResult",
15
+ "IndexResult",
16
+ "DeleteResult",
17
+ "InfoResult",
18
+ "QueryCommandResult",
19
+ "ReindexResult",
20
+ "ErrorResult",
21
+ ]
shotgun/cli/config.py ADDED
@@ -0,0 +1,261 @@
1
+ """Configuration management CLI commands."""
2
+
3
+ import json
4
+ from typing import Annotated, Any
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from shotgun.agents.config import ProviderType, get_config_manager
11
+ from shotgun.logging_config import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+ console = Console()
15
+
16
+ app = typer.Typer(
17
+ name="config",
18
+ help="Manage Shotgun configuration",
19
+ no_args_is_help=True,
20
+ )
21
+
22
+
23
+ @app.command()
24
+ def init(
25
+ interactive: Annotated[
26
+ bool,
27
+ typer.Option("--interactive", "-i", help="Run interactive setup wizard"),
28
+ ] = True,
29
+ ) -> None:
30
+ """Initialize Shotgun configuration."""
31
+ config_manager = get_config_manager()
32
+
33
+ if config_manager.config_path.exists() and not typer.confirm(
34
+ f"Configuration already exists at {config_manager.config_path}. Overwrite?"
35
+ ):
36
+ console.print("❌ Configuration initialization cancelled.", style="red")
37
+ raise typer.Exit(1)
38
+
39
+ if interactive:
40
+ console.print(
41
+ "🚀 [bold blue]Welcome to Shotgun Configuration Setup![/bold blue]"
42
+ )
43
+ console.print()
44
+
45
+ # Initialize with defaults
46
+ config = config_manager.initialize()
47
+
48
+ # Ask for default provider
49
+ provider_choices = ["openai", "anthropic", "google"]
50
+ console.print("Choose your default AI provider:")
51
+ for i, provider in enumerate(provider_choices, 1):
52
+ console.print(f" {i}. {provider}")
53
+
54
+ while True:
55
+ try:
56
+ choice = typer.prompt("Enter choice (1-3)", type=int)
57
+ if 1 <= choice <= 3:
58
+ config.default_provider = ProviderType(provider_choices[choice - 1])
59
+ break
60
+ else:
61
+ console.print(
62
+ "❌ Invalid choice. Please enter 1, 2, or 3.", style="red"
63
+ )
64
+ except ValueError:
65
+ console.print("❌ Please enter a valid number.", style="red")
66
+
67
+ # Ask for API key for the selected provider
68
+ provider = config.default_provider
69
+ console.print(f"\n🔑 Setting up {provider.upper()} API key...")
70
+
71
+ api_key = typer.prompt(
72
+ f"Enter your {provider.upper()} API key",
73
+ hide_input=True,
74
+ default="",
75
+ )
76
+
77
+ if api_key:
78
+ config_manager.update_provider(provider, api_key=api_key)
79
+
80
+ config_manager.save()
81
+ console.print(
82
+ f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
83
+ )
84
+ console.print("🎯 You can now use Shotgun with your configured provider!")
85
+
86
+ else:
87
+ config_manager.initialize()
88
+ console.print(f"✅ Configuration initialized at {config_manager.config_path}")
89
+
90
+
91
+ @app.command()
92
+ def set(
93
+ provider: Annotated[
94
+ ProviderType,
95
+ typer.Argument(help="AI provider to configure (openai, anthropic, google)"),
96
+ ],
97
+ api_key: Annotated[
98
+ str | None,
99
+ typer.Option("--api-key", "-k", help="API key for the provider"),
100
+ ] = None,
101
+ default: Annotated[
102
+ bool,
103
+ typer.Option("--default", "-d", help="Set this provider as default"),
104
+ ] = False,
105
+ ) -> None:
106
+ """Set configuration for a specific provider."""
107
+ config_manager = get_config_manager()
108
+
109
+ # If no API key provided via option and not just setting default, prompt for it
110
+ if api_key is None and not default:
111
+ api_key = typer.prompt(
112
+ f"Enter your {provider.upper()} API key",
113
+ hide_input=True,
114
+ default="",
115
+ )
116
+
117
+ try:
118
+ if api_key:
119
+ config_manager.update_provider(provider, api_key=api_key)
120
+
121
+ if default:
122
+ config = config_manager.load()
123
+ config.default_provider = provider
124
+ config_manager.save(config)
125
+
126
+ console.print(f"✅ Configuration updated for {provider}")
127
+
128
+ except Exception as e:
129
+ console.print(f"❌ Failed to update configuration: {e}", style="red")
130
+ raise typer.Exit(1) from e
131
+
132
+
133
+ @app.command()
134
+ def set_default(
135
+ provider: Annotated[
136
+ ProviderType,
137
+ typer.Argument(
138
+ help="AI provider to set as default (openai, anthropic, google)"
139
+ ),
140
+ ],
141
+ ) -> None:
142
+ """Set the default AI provider without modifying API keys."""
143
+ config_manager = get_config_manager()
144
+
145
+ try:
146
+ config = config_manager.load()
147
+
148
+ # Check if the provider has an API key configured
149
+ provider_config = getattr(config, provider.value)
150
+ if not provider_config.api_key:
151
+ console.print(
152
+ f"⚠️ Warning: {provider.upper()} does not have an API key configured.",
153
+ style="yellow",
154
+ )
155
+ console.print(f"Use 'shotgun config set {provider}' to configure it.")
156
+
157
+ # Set as default
158
+ config.default_provider = provider
159
+ config_manager.save(config)
160
+
161
+ console.print(f"✅ Default provider set to: {provider}")
162
+
163
+ except Exception as e:
164
+ console.print(f"❌ Failed to set default provider: {e}", style="red")
165
+ raise typer.Exit(1) from e
166
+
167
+
168
+ @app.command()
169
+ def get(
170
+ provider: Annotated[
171
+ ProviderType | None,
172
+ typer.Option("--provider", "-p", help="Show config for specific provider"),
173
+ ] = None,
174
+ json_output: Annotated[
175
+ bool,
176
+ typer.Option("--json", "-j", help="Output as JSON"),
177
+ ] = False,
178
+ ) -> None:
179
+ """Display current configuration."""
180
+ config_manager = get_config_manager()
181
+ config = config_manager.load()
182
+
183
+ if json_output:
184
+ # Convert to dict and mask secrets
185
+ data = config.model_dump()
186
+ _mask_secrets(data)
187
+ console.print(json.dumps(data, indent=2))
188
+ return
189
+
190
+ if provider:
191
+ # Show specific provider configuration
192
+ _show_provider_config(provider, config)
193
+ else:
194
+ # Show all configuration
195
+ _show_full_config(config)
196
+
197
+
198
+ def _show_full_config(config: Any) -> None:
199
+ """Display full configuration in a table."""
200
+ table = Table(title="Shotgun Configuration", show_header=True)
201
+ table.add_column("Setting", style="cyan")
202
+ table.add_column("Value", style="white")
203
+
204
+ # Default provider
205
+ table.add_row("Default Provider", f"[bold]{config.default_provider}[/bold]")
206
+ table.add_row("", "") # Separator
207
+
208
+ # Provider configurations
209
+ for provider_name, provider_config in [
210
+ ("OpenAI", config.openai),
211
+ ("Anthropic", config.anthropic),
212
+ ("Google", config.google),
213
+ ]:
214
+ table.add_row(f"[bold]{provider_name}[/bold]", "")
215
+
216
+ # API Key
217
+ api_key_status = "✅ Set" if provider_config.api_key else "❌ Not set"
218
+ table.add_row(" API Key", api_key_status)
219
+ table.add_row("", "") # Separator
220
+
221
+ console.print(table)
222
+
223
+
224
+ def _show_provider_config(provider: ProviderType, config: Any) -> None:
225
+ """Display configuration for a specific provider."""
226
+ provider_str = provider.value if isinstance(provider, ProviderType) else provider
227
+
228
+ if provider_str == "openai":
229
+ provider_config = config.openai
230
+ elif provider_str == "anthropic":
231
+ provider_config = config.anthropic
232
+ elif provider_str == "google":
233
+ provider_config = config.google
234
+ else:
235
+ console.print(f"❌ Unknown provider: {provider}", style="red")
236
+ return
237
+
238
+ table = Table(title=f"{provider.upper()} Configuration")
239
+ table.add_column("Setting", style="cyan")
240
+ table.add_column("Value", style="white")
241
+
242
+ # API Key
243
+ api_key_status = "✅ Set" if provider_config.api_key else "❌ Not set"
244
+ table.add_row("API Key", api_key_status)
245
+
246
+ console.print(table)
247
+
248
+
249
+ def _mask_secrets(data: dict[str, Any]) -> None:
250
+ """Mask secrets in configuration data."""
251
+ for provider in ["openai", "anthropic", "google"]:
252
+ if provider in data and isinstance(data[provider], dict):
253
+ if "api_key" in data[provider] and data[provider]["api_key"]:
254
+ data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
255
+
256
+
257
+ def _mask_value(value: str) -> str:
258
+ """Mask a secret value."""
259
+ if len(value) <= 8:
260
+ return "••••••••"
261
+ return f"{value[:4]}{'•' * (len(value) - 8)}{value[-4:]}"
shotgun/cli/models.py ADDED
@@ -0,0 +1,10 @@
1
+ """Common models for CLI commands."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class OutputFormat(str, Enum):
7
+ """Output format options for CLI commands."""
8
+
9
+ TEXT = "text"
10
+ JSON = "json"
shotgun/cli/plan.py ADDED
@@ -0,0 +1,65 @@
1
+ """Plan command for shotgun CLI."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from shotgun.agents.config import ProviderType
9
+ from shotgun.agents.models import AgentRuntimeOptions
10
+ from shotgun.agents.plan import create_plan_agent, get_plan_history, run_plan_agent
11
+ from shotgun.logging_config import get_logger
12
+
13
+ app = typer.Typer(name="plan", help="Generate structured plans", no_args_is_help=True)
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ @app.callback(invoke_without_command=True)
18
+ def plan(
19
+ goal: Annotated[str, typer.Argument(help="Goal or objective to plan for")],
20
+ non_interactive: Annotated[
21
+ bool,
22
+ typer.Option(
23
+ "--non-interactive", "-n", help="Disable user interaction (for CI/CD)"
24
+ ),
25
+ ] = False,
26
+ provider: Annotated[
27
+ ProviderType | None,
28
+ typer.Option("--provider", "-p", help="AI provider to use (overrides default)"),
29
+ ] = None,
30
+ ) -> None:
31
+ """Generate a structured plan for achieving the given goal.
32
+
33
+ This command will create detailed, actionable plans broken down into steps
34
+ and milestones to help achieve your specified objective. It can also update
35
+ existing plans based on new requirements or refinements.
36
+ """
37
+
38
+ logger.info("📋 Planning Goal: %s", goal)
39
+
40
+ try:
41
+ # Create agent dependencies
42
+ agent_runtime_options = AgentRuntimeOptions(
43
+ interactive_mode=not non_interactive
44
+ )
45
+
46
+ # Create the plan agent with deps and provider
47
+ agent, deps = create_plan_agent(agent_runtime_options, provider)
48
+
49
+ # Start planning process
50
+ logger.info("🎯 Starting planning...")
51
+ result = asyncio.run(run_plan_agent(agent, goal, deps))
52
+
53
+ # Display results
54
+ logger.info("✅ Planning Complete!")
55
+ logger.info("📋 Results:")
56
+ logger.info("%s", result.output)
57
+ logger.info("📄 Plan saved to: .shotgun/plan.md")
58
+ logger.debug("📚 Current plan:")
59
+ logger.debug("%s", get_plan_history())
60
+
61
+ except Exception as e:
62
+ logger.error("❌ Error during planning: %s", str(e))
63
+ import traceback
64
+
65
+ logger.debug("Full traceback:\n%s", traceback.format_exc())
@@ -0,0 +1,78 @@
1
+ """Research command for shotgun CLI."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from shotgun.agents.config import ProviderType
9
+ from shotgun.agents.models import AgentRuntimeOptions
10
+ from shotgun.agents.research import (
11
+ create_research_agent,
12
+ get_research_history,
13
+ run_research_agent,
14
+ )
15
+ from shotgun.logging_config import get_logger
16
+
17
+ app = typer.Typer(
18
+ name="research", help="Perform research with agentic loops", no_args_is_help=True
19
+ )
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ @app.callback(invoke_without_command=True)
24
+ def research(
25
+ query: Annotated[str, typer.Argument(help="Research query or topic")],
26
+ non_interactive: Annotated[
27
+ bool,
28
+ typer.Option(
29
+ "--non-interactive", "-n", help="Disable user interaction (for CI/CD)"
30
+ ),
31
+ ] = False,
32
+ provider: Annotated[
33
+ ProviderType | None,
34
+ typer.Option("--provider", "-p", help="AI provider to use (overrides default)"),
35
+ ] = None,
36
+ ) -> None:
37
+ """Perform research on a given query using agentic loops.
38
+
39
+ This command will use AI agents to iteratively research the provided topic,
40
+ gathering information from multiple sources and refining the search process.
41
+ """
42
+
43
+ logger.info("🔍 Research Query: %s", query)
44
+
45
+ try:
46
+ # Run everything in the same event loop
47
+ asyncio.run(async_research(query, non_interactive, provider))
48
+
49
+ except Exception as e:
50
+ logger.error("❌ Error during research: %s", str(e))
51
+ import traceback
52
+
53
+ logger.debug("Full traceback:\n%s", traceback.format_exc())
54
+
55
+
56
+ async def async_research(
57
+ query: str,
58
+ non_interactive: bool,
59
+ provider: ProviderType | None = None,
60
+ ) -> None:
61
+ """Async wrapper for research process."""
62
+ # Create agent dependencies
63
+ agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
64
+
65
+ # Create the research agent with deps and provider
66
+ agent, deps = create_research_agent(agent_runtime_options, provider)
67
+
68
+ # Start research process
69
+ logger.info("🔬 Starting research...")
70
+ result = await run_research_agent(agent, query, deps)
71
+
72
+ # Display results
73
+ logger.info("✅ Research Complete!")
74
+ logger.info("📋 Findings:")
75
+ logger.info("%s", result.output)
76
+ logger.info("📄 Full research saved to: .shotgun/research.md")
77
+ logger.debug("📚 Research history:")
78
+ logger.debug("%s", get_research_history())