shotgun-sh 0.1.0__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/__init__.py +5 -0
- shotgun/agents/__init__.py +1 -0
- shotgun/agents/agent_manager.py +651 -0
- shotgun/agents/common.py +549 -0
- shotgun/agents/config/__init__.py +13 -0
- shotgun/agents/config/constants.py +17 -0
- shotgun/agents/config/manager.py +294 -0
- shotgun/agents/config/models.py +185 -0
- shotgun/agents/config/provider.py +206 -0
- shotgun/agents/conversation_history.py +106 -0
- shotgun/agents/conversation_manager.py +105 -0
- shotgun/agents/export.py +96 -0
- shotgun/agents/history/__init__.py +5 -0
- shotgun/agents/history/compaction.py +85 -0
- shotgun/agents/history/constants.py +19 -0
- shotgun/agents/history/context_extraction.py +108 -0
- shotgun/agents/history/history_building.py +104 -0
- shotgun/agents/history/history_processors.py +426 -0
- shotgun/agents/history/message_utils.py +84 -0
- shotgun/agents/history/token_counting.py +429 -0
- shotgun/agents/history/token_estimation.py +138 -0
- shotgun/agents/messages.py +35 -0
- shotgun/agents/models.py +275 -0
- shotgun/agents/plan.py +98 -0
- shotgun/agents/research.py +108 -0
- shotgun/agents/specify.py +98 -0
- shotgun/agents/tasks.py +96 -0
- shotgun/agents/tools/__init__.py +34 -0
- shotgun/agents/tools/codebase/__init__.py +28 -0
- shotgun/agents/tools/codebase/codebase_shell.py +256 -0
- shotgun/agents/tools/codebase/directory_lister.py +141 -0
- shotgun/agents/tools/codebase/file_read.py +144 -0
- shotgun/agents/tools/codebase/models.py +252 -0
- shotgun/agents/tools/codebase/query_graph.py +67 -0
- shotgun/agents/tools/codebase/retrieve_code.py +81 -0
- shotgun/agents/tools/file_management.py +218 -0
- shotgun/agents/tools/user_interaction.py +37 -0
- shotgun/agents/tools/web_search/__init__.py +60 -0
- shotgun/agents/tools/web_search/anthropic.py +144 -0
- shotgun/agents/tools/web_search/gemini.py +85 -0
- shotgun/agents/tools/web_search/openai.py +98 -0
- shotgun/agents/tools/web_search/utils.py +20 -0
- shotgun/build_constants.py +20 -0
- shotgun/cli/__init__.py +1 -0
- shotgun/cli/codebase/__init__.py +5 -0
- shotgun/cli/codebase/commands.py +202 -0
- shotgun/cli/codebase/models.py +21 -0
- shotgun/cli/config.py +275 -0
- shotgun/cli/export.py +81 -0
- shotgun/cli/models.py +10 -0
- shotgun/cli/plan.py +73 -0
- shotgun/cli/research.py +85 -0
- shotgun/cli/specify.py +69 -0
- shotgun/cli/tasks.py +78 -0
- shotgun/cli/update.py +152 -0
- shotgun/cli/utils.py +25 -0
- shotgun/codebase/__init__.py +12 -0
- shotgun/codebase/core/__init__.py +46 -0
- shotgun/codebase/core/change_detector.py +358 -0
- shotgun/codebase/core/code_retrieval.py +243 -0
- shotgun/codebase/core/ingestor.py +1497 -0
- shotgun/codebase/core/language_config.py +297 -0
- shotgun/codebase/core/manager.py +1662 -0
- shotgun/codebase/core/nl_query.py +331 -0
- shotgun/codebase/core/parser_loader.py +128 -0
- shotgun/codebase/models.py +111 -0
- shotgun/codebase/service.py +206 -0
- shotgun/logging_config.py +227 -0
- shotgun/main.py +167 -0
- shotgun/posthog_telemetry.py +158 -0
- shotgun/prompts/__init__.py +5 -0
- shotgun/prompts/agents/__init__.py +1 -0
- shotgun/prompts/agents/export.j2 +350 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +87 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +37 -0
- shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +26 -0
- shotgun/prompts/agents/plan.j2 +144 -0
- shotgun/prompts/agents/research.j2 +69 -0
- shotgun/prompts/agents/specify.j2 +51 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +19 -0
- shotgun/prompts/agents/state/system_state.j2 +31 -0
- shotgun/prompts/agents/tasks.j2 +143 -0
- shotgun/prompts/codebase/__init__.py +1 -0
- shotgun/prompts/codebase/cypher_query_patterns.j2 +223 -0
- shotgun/prompts/codebase/cypher_system.j2 +28 -0
- shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
- shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
- shotgun/prompts/codebase/partials/graph_schema.j2 +30 -0
- shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
- shotgun/prompts/history/__init__.py +1 -0
- shotgun/prompts/history/incremental_summarization.j2 +53 -0
- shotgun/prompts/history/summarization.j2 +46 -0
- shotgun/prompts/loader.py +140 -0
- shotgun/py.typed +0 -0
- shotgun/sdk/__init__.py +13 -0
- shotgun/sdk/codebase.py +219 -0
- shotgun/sdk/exceptions.py +17 -0
- shotgun/sdk/models.py +189 -0
- shotgun/sdk/services.py +23 -0
- shotgun/sentry_telemetry.py +87 -0
- shotgun/telemetry.py +93 -0
- shotgun/tui/__init__.py +0 -0
- shotgun/tui/app.py +116 -0
- shotgun/tui/commands/__init__.py +76 -0
- shotgun/tui/components/prompt_input.py +69 -0
- shotgun/tui/components/spinner.py +86 -0
- shotgun/tui/components/splash.py +25 -0
- shotgun/tui/components/vertical_tail.py +13 -0
- shotgun/tui/screens/chat.py +782 -0
- shotgun/tui/screens/chat.tcss +43 -0
- shotgun/tui/screens/chat_screen/__init__.py +0 -0
- shotgun/tui/screens/chat_screen/command_providers.py +219 -0
- shotgun/tui/screens/chat_screen/hint_message.py +40 -0
- shotgun/tui/screens/chat_screen/history.py +221 -0
- shotgun/tui/screens/directory_setup.py +113 -0
- shotgun/tui/screens/provider_config.py +221 -0
- shotgun/tui/screens/splash.py +31 -0
- shotgun/tui/styles.tcss +10 -0
- shotgun/tui/utils/__init__.py +5 -0
- shotgun/tui/utils/mode_progress.py +257 -0
- shotgun/utils/__init__.py +5 -0
- shotgun/utils/env_utils.py +35 -0
- shotgun/utils/file_system_utils.py +36 -0
- shotgun/utils/update_checker.py +375 -0
- shotgun_sh-0.1.0.dist-info/METADATA +466 -0
- shotgun_sh-0.1.0.dist-info/RECORD +130 -0
- shotgun_sh-0.1.0.dist-info/WHEEL +4 -0
- shotgun_sh-0.1.0.dist-info/entry_points.txt +2 -0
- shotgun_sh-0.1.0.dist-info/licenses/LICENSE +21 -0
shotgun/sdk/codebase.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Codebase SDK for framework-agnostic business logic."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from shotgun.codebase.models import CodebaseGraph, QueryType
|
|
8
|
+
|
|
9
|
+
from .exceptions import CodebaseNotFoundError, InvalidPathError
|
|
10
|
+
from .models import (
|
|
11
|
+
DeleteResult,
|
|
12
|
+
IndexResult,
|
|
13
|
+
InfoResult,
|
|
14
|
+
ListResult,
|
|
15
|
+
QueryCommandResult,
|
|
16
|
+
ReindexResult,
|
|
17
|
+
)
|
|
18
|
+
from .services import get_codebase_service
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CodebaseSDK:
|
|
22
|
+
"""Framework-agnostic SDK for codebase operations.
|
|
23
|
+
|
|
24
|
+
This SDK provides business logic for codebase management that can be
|
|
25
|
+
used by both CLI and TUI implementations without framework dependencies.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, storage_dir: Path | None = None):
|
|
29
|
+
"""Initialize SDK with optional storage directory.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
storage_dir: Optional custom storage directory.
|
|
33
|
+
Defaults to ~/.shotgun-sh/codebases/
|
|
34
|
+
"""
|
|
35
|
+
self.service = get_codebase_service(storage_dir)
|
|
36
|
+
|
|
37
|
+
async def list_codebases(self) -> ListResult:
|
|
38
|
+
"""List all indexed codebases.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
ListResult containing list of codebases
|
|
42
|
+
"""
|
|
43
|
+
graphs = await self.service.list_graphs()
|
|
44
|
+
return ListResult(graphs=graphs)
|
|
45
|
+
|
|
46
|
+
async def list_codebases_for_directory(
|
|
47
|
+
self, directory: Path | None = None
|
|
48
|
+
) -> ListResult:
|
|
49
|
+
"""List codebases accessible from a specific directory.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
directory: Directory to filter by. If None, uses current working directory.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
ListResult containing filtered list of codebases
|
|
56
|
+
"""
|
|
57
|
+
graphs = await self.service.list_graphs_for_directory(directory)
|
|
58
|
+
return ListResult(graphs=graphs)
|
|
59
|
+
|
|
60
|
+
async def index_codebase(
|
|
61
|
+
self, path: Path, name: str, indexed_from_cwd: str | None = None
|
|
62
|
+
) -> IndexResult:
|
|
63
|
+
"""Index a new codebase.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
path: Path to the repository to index
|
|
67
|
+
name: Human-readable name for the codebase
|
|
68
|
+
indexed_from_cwd: Working directory from which indexing was initiated.
|
|
69
|
+
If None, uses current working directory.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
IndexResult with indexing details
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
InvalidPathError: If the path does not exist
|
|
76
|
+
"""
|
|
77
|
+
resolved_path = path.resolve()
|
|
78
|
+
if not resolved_path.exists():
|
|
79
|
+
raise InvalidPathError(f"Path does not exist: {resolved_path}")
|
|
80
|
+
|
|
81
|
+
# Default to current working directory if not specified
|
|
82
|
+
if indexed_from_cwd is None:
|
|
83
|
+
indexed_from_cwd = str(Path.cwd().resolve())
|
|
84
|
+
|
|
85
|
+
graph = await self.service.create_graph(
|
|
86
|
+
resolved_path, name, indexed_from_cwd=indexed_from_cwd
|
|
87
|
+
)
|
|
88
|
+
file_count = sum(graph.language_stats.values()) if graph.language_stats else 0
|
|
89
|
+
|
|
90
|
+
return IndexResult(
|
|
91
|
+
graph_id=graph.graph_id,
|
|
92
|
+
name=name,
|
|
93
|
+
repo_path=str(resolved_path),
|
|
94
|
+
file_count=file_count,
|
|
95
|
+
node_count=graph.node_count,
|
|
96
|
+
relationship_count=graph.relationship_count,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def delete_codebase(
|
|
100
|
+
self,
|
|
101
|
+
graph_id: str,
|
|
102
|
+
confirm_callback: Callable[[CodebaseGraph], bool]
|
|
103
|
+
| Callable[[CodebaseGraph], Awaitable[bool]]
|
|
104
|
+
| None = None,
|
|
105
|
+
) -> DeleteResult:
|
|
106
|
+
"""Delete a codebase with optional confirmation.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
graph_id: ID of the graph to delete
|
|
110
|
+
confirm_callback: Optional callback for confirmation.
|
|
111
|
+
Can be sync or async function that receives
|
|
112
|
+
the CodebaseGraph object and returns boolean.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
DeleteResult indicating success, failure, or cancellation
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
CodebaseNotFoundError: If the graph is not found
|
|
119
|
+
"""
|
|
120
|
+
graph = await self.service.get_graph(graph_id)
|
|
121
|
+
if not graph:
|
|
122
|
+
raise CodebaseNotFoundError(f"Graph not found: {graph_id}")
|
|
123
|
+
|
|
124
|
+
# Handle confirmation callback if provided
|
|
125
|
+
if confirm_callback:
|
|
126
|
+
if asyncio.iscoroutinefunction(confirm_callback):
|
|
127
|
+
confirmed = await confirm_callback(graph)
|
|
128
|
+
else:
|
|
129
|
+
confirmed = confirm_callback(graph)
|
|
130
|
+
|
|
131
|
+
if not confirmed:
|
|
132
|
+
return DeleteResult(
|
|
133
|
+
graph_id=graph_id,
|
|
134
|
+
name=graph.name,
|
|
135
|
+
deleted=False,
|
|
136
|
+
cancelled=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
await self.service.delete_graph(graph_id)
|
|
140
|
+
return DeleteResult(
|
|
141
|
+
graph_id=graph_id,
|
|
142
|
+
name=graph.name,
|
|
143
|
+
deleted=True,
|
|
144
|
+
cancelled=False,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def get_info(self, graph_id: str) -> InfoResult:
|
|
148
|
+
"""Get detailed information about a codebase.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
graph_id: ID of the graph to get info for
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
InfoResult with detailed graph information
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
CodebaseNotFoundError: If the graph is not found
|
|
158
|
+
"""
|
|
159
|
+
graph = await self.service.get_graph(graph_id)
|
|
160
|
+
if not graph:
|
|
161
|
+
raise CodebaseNotFoundError(f"Graph not found: {graph_id}")
|
|
162
|
+
|
|
163
|
+
return InfoResult(graph=graph)
|
|
164
|
+
|
|
165
|
+
async def query_codebase(
|
|
166
|
+
self, graph_id: str, query_text: str, query_type: QueryType
|
|
167
|
+
) -> QueryCommandResult:
|
|
168
|
+
"""Query a codebase using natural language or Cypher.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
graph_id: ID of the graph to query
|
|
172
|
+
query_text: Query text (natural language or Cypher)
|
|
173
|
+
query_type: Type of query (NATURAL_LANGUAGE or CYPHER)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
QueryCommandResult with query results
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
CodebaseNotFoundError: If the graph is not found
|
|
180
|
+
"""
|
|
181
|
+
graph = await self.service.get_graph(graph_id)
|
|
182
|
+
if not graph:
|
|
183
|
+
raise CodebaseNotFoundError(f"Graph not found: {graph_id}")
|
|
184
|
+
|
|
185
|
+
query_result = await self.service.execute_query(
|
|
186
|
+
graph_id, query_text, query_type
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return QueryCommandResult(
|
|
190
|
+
graph_name=graph.name,
|
|
191
|
+
query_type="Cypher"
|
|
192
|
+
if query_type == QueryType.CYPHER
|
|
193
|
+
else "natural language",
|
|
194
|
+
result=query_result,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def reindex_codebase(self, graph_id: str) -> ReindexResult:
|
|
198
|
+
"""Reindex an existing codebase.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
graph_id: ID of the graph to reindex
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
ReindexResult with reindexing details
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
CodebaseNotFoundError: If the graph is not found
|
|
208
|
+
"""
|
|
209
|
+
graph = await self.service.get_graph(graph_id)
|
|
210
|
+
if not graph:
|
|
211
|
+
raise CodebaseNotFoundError(f"Graph not found: {graph_id}")
|
|
212
|
+
|
|
213
|
+
stats = await self.service.reindex_graph(graph_id)
|
|
214
|
+
|
|
215
|
+
return ReindexResult(
|
|
216
|
+
graph_id=graph_id,
|
|
217
|
+
name=graph.name,
|
|
218
|
+
stats=stats,
|
|
219
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""SDK-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ShotgunSDKError(Exception):
|
|
5
|
+
"""Base exception for all SDK operations."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CodebaseNotFoundError(ShotgunSDKError):
|
|
9
|
+
"""Raised when a codebase or graph is not found."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CodebaseOperationError(ShotgunSDKError):
|
|
13
|
+
"""Raised when a codebase operation fails."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidPathError(ShotgunSDKError):
|
|
17
|
+
"""Raised when a provided path is invalid."""
|
shotgun/sdk/models.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Result models for SDK operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from shotgun.codebase.models import CodebaseGraph, QueryResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ListResult(BaseModel):
|
|
11
|
+
"""Result for list command."""
|
|
12
|
+
|
|
13
|
+
graphs: list[CodebaseGraph]
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
"""Format list result as plain text table."""
|
|
17
|
+
if not self.graphs:
|
|
18
|
+
return "No indexed codebases found."
|
|
19
|
+
|
|
20
|
+
lines = [
|
|
21
|
+
f"{'ID':<12} {'Name':<30} {'Status':<10} {'Files':<8} {'Path'}",
|
|
22
|
+
"-" * 80,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
for graph in self.graphs:
|
|
26
|
+
file_count = (
|
|
27
|
+
sum(graph.language_stats.values()) if graph.language_stats else 0
|
|
28
|
+
)
|
|
29
|
+
lines.append(
|
|
30
|
+
f"{graph.graph_id[:12]:<12} {graph.name[:30]:<30} {graph.status.value:<10} {file_count:<8} {graph.repo_path}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return "\n".join(lines)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class IndexResult(BaseModel):
|
|
37
|
+
"""Result for index command."""
|
|
38
|
+
|
|
39
|
+
graph_id: str
|
|
40
|
+
name: str
|
|
41
|
+
repo_path: str
|
|
42
|
+
file_count: int
|
|
43
|
+
node_count: int
|
|
44
|
+
relationship_count: int
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
"""Format index result as success message."""
|
|
48
|
+
return (
|
|
49
|
+
"Successfully indexed codebase!\n"
|
|
50
|
+
f"Graph ID: {self.graph_id}\n"
|
|
51
|
+
f"Files processed: {self.file_count}\n"
|
|
52
|
+
f"Nodes: {self.node_count}\n"
|
|
53
|
+
f"Relationships: {self.relationship_count}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DeleteResult(BaseModel):
|
|
58
|
+
"""Result for delete command."""
|
|
59
|
+
|
|
60
|
+
graph_id: str
|
|
61
|
+
name: str
|
|
62
|
+
deleted: bool
|
|
63
|
+
cancelled: bool = False
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
"""Format delete result message."""
|
|
67
|
+
if self.cancelled:
|
|
68
|
+
return "Deletion cancelled."
|
|
69
|
+
elif self.deleted:
|
|
70
|
+
return f"Successfully deleted codebase: {self.graph_id}"
|
|
71
|
+
else:
|
|
72
|
+
return f"Failed to delete codebase: {self.graph_id}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class InfoResult(BaseModel):
|
|
76
|
+
"""Result for info command."""
|
|
77
|
+
|
|
78
|
+
graph: CodebaseGraph
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
"""Format detailed graph information."""
|
|
82
|
+
graph = self.graph
|
|
83
|
+
lines = [
|
|
84
|
+
f"Graph ID: {graph.graph_id}",
|
|
85
|
+
f"Name: {graph.name}",
|
|
86
|
+
f"Status: {graph.status.value}",
|
|
87
|
+
f"Repository Path: {graph.repo_path}",
|
|
88
|
+
f"Database Path: {graph.graph_path}",
|
|
89
|
+
f"Created: {graph.created_at}",
|
|
90
|
+
f"Updated: {graph.updated_at}",
|
|
91
|
+
f"Schema Version: {graph.schema_version}",
|
|
92
|
+
f"Total Nodes: {graph.node_count}",
|
|
93
|
+
f"Total Relationships: {graph.relationship_count}",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
if graph.language_stats:
|
|
97
|
+
lines.append("\nLanguage Statistics:")
|
|
98
|
+
for lang, count in graph.language_stats.items():
|
|
99
|
+
lines.append(f" {lang}: {count} files")
|
|
100
|
+
|
|
101
|
+
if graph.node_stats:
|
|
102
|
+
lines.append("\nNode Statistics:")
|
|
103
|
+
for node_type, count in graph.node_stats.items():
|
|
104
|
+
lines.append(f" {node_type}: {count}")
|
|
105
|
+
|
|
106
|
+
if graph.relationship_stats:
|
|
107
|
+
lines.append("\nRelationship Statistics:")
|
|
108
|
+
for rel_type, count in graph.relationship_stats.items():
|
|
109
|
+
lines.append(f" {rel_type}: {count}")
|
|
110
|
+
|
|
111
|
+
return "\n".join(lines)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class QueryCommandResult(BaseModel):
|
|
115
|
+
"""Result for query command."""
|
|
116
|
+
|
|
117
|
+
graph_name: str
|
|
118
|
+
query_type: str
|
|
119
|
+
result: QueryResult
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
"""Format query results table."""
|
|
123
|
+
query_result = self.result
|
|
124
|
+
|
|
125
|
+
if not query_result.success:
|
|
126
|
+
return f"Query failed: {query_result.error}"
|
|
127
|
+
|
|
128
|
+
if not query_result.results:
|
|
129
|
+
return "No results found."
|
|
130
|
+
|
|
131
|
+
lines = [
|
|
132
|
+
f"Query executed in {query_result.execution_time_ms:.2f}ms",
|
|
133
|
+
f"Results: {query_result.row_count} rows",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
if query_result.cypher_query:
|
|
137
|
+
lines.append(f"Generated Cypher: {query_result.cypher_query}")
|
|
138
|
+
|
|
139
|
+
lines.append("") # Empty line
|
|
140
|
+
|
|
141
|
+
# Format results table
|
|
142
|
+
if query_result.column_names:
|
|
143
|
+
header = " | ".join(f"{col:<20}" for col in query_result.column_names)
|
|
144
|
+
lines.append(header)
|
|
145
|
+
lines.append("-" * len(header))
|
|
146
|
+
|
|
147
|
+
for row in query_result.results:
|
|
148
|
+
row_data = " | ".join(
|
|
149
|
+
f"{str(row.get(col, '')):<20}" for col in query_result.column_names
|
|
150
|
+
)
|
|
151
|
+
lines.append(row_data)
|
|
152
|
+
else:
|
|
153
|
+
# Fallback for results without column names
|
|
154
|
+
for i, row in enumerate(query_result.results):
|
|
155
|
+
lines.append(f"Row {i + 1}:")
|
|
156
|
+
for key, value in row.items():
|
|
157
|
+
lines.append(f" {key}: {value}")
|
|
158
|
+
lines.append("")
|
|
159
|
+
|
|
160
|
+
return "\n".join(lines)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ReindexResult(BaseModel):
|
|
164
|
+
"""Result for reindex command."""
|
|
165
|
+
|
|
166
|
+
graph_id: str
|
|
167
|
+
name: str
|
|
168
|
+
stats: dict[str, Any] | None = None
|
|
169
|
+
|
|
170
|
+
def __str__(self) -> str:
|
|
171
|
+
"""Format reindex completion message."""
|
|
172
|
+
lines = ["Reindexing completed!"]
|
|
173
|
+
if self.stats:
|
|
174
|
+
lines.append(f"Stats: {self.stats}")
|
|
175
|
+
return "\n".join(lines)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ErrorResult(BaseModel):
|
|
179
|
+
"""Result for error cases."""
|
|
180
|
+
|
|
181
|
+
error_message: str
|
|
182
|
+
details: str | None = None
|
|
183
|
+
|
|
184
|
+
def __str__(self) -> str:
|
|
185
|
+
"""Format error message."""
|
|
186
|
+
output = f"Error: {self.error_message}"
|
|
187
|
+
if self.details:
|
|
188
|
+
output += f"\n{self.details}"
|
|
189
|
+
return output
|
shotgun/sdk/services.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Service factory functions for SDK."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from shotgun.codebase.service import CodebaseService
|
|
6
|
+
from shotgun.utils import get_shotgun_home
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_codebase_service(storage_dir: Path | str | None = None) -> CodebaseService:
|
|
10
|
+
"""Get CodebaseService instance with configurable storage.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
storage_dir: Optional custom storage directory.
|
|
14
|
+
Defaults to ~/.shotgun-sh/codebases/
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Configured CodebaseService instance
|
|
18
|
+
"""
|
|
19
|
+
if storage_dir is None:
|
|
20
|
+
storage_dir = get_shotgun_home() / "codebases"
|
|
21
|
+
elif isinstance(storage_dir, str):
|
|
22
|
+
storage_dir = Path(storage_dir)
|
|
23
|
+
return CodebaseService(storage_dir)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Sentry observability setup for Shotgun."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from shotgun.logging_config import get_early_logger
|
|
6
|
+
|
|
7
|
+
# Use early logger to prevent automatic StreamHandler creation
|
|
8
|
+
logger = get_early_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_sentry_observability() -> bool:
|
|
12
|
+
"""Set up Sentry observability for error tracking.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
True if Sentry was successfully set up, False otherwise
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
import sentry_sdk
|
|
19
|
+
|
|
20
|
+
# Check if Sentry is already initialized
|
|
21
|
+
if sentry_sdk.is_initialized():
|
|
22
|
+
logger.debug("Sentry is already initialized, skipping")
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
# Try to get DSN from build constants first (production builds)
|
|
26
|
+
dsn = None
|
|
27
|
+
try:
|
|
28
|
+
from shotgun import build_constants
|
|
29
|
+
|
|
30
|
+
dsn = build_constants.SENTRY_DSN
|
|
31
|
+
logger.debug("Using Sentry DSN from build constants")
|
|
32
|
+
except ImportError:
|
|
33
|
+
# Fallback to environment variable (development)
|
|
34
|
+
dsn = os.getenv("SENTRY_DSN", "")
|
|
35
|
+
if dsn:
|
|
36
|
+
logger.debug("Using Sentry DSN from environment variable")
|
|
37
|
+
|
|
38
|
+
if not dsn:
|
|
39
|
+
logger.debug("No Sentry DSN configured, skipping Sentry initialization")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
logger.debug("Found DSN, proceeding with Sentry setup")
|
|
43
|
+
|
|
44
|
+
# Get version for release tracking
|
|
45
|
+
from shotgun import __version__
|
|
46
|
+
|
|
47
|
+
# Determine environment based on version
|
|
48
|
+
# Dev versions contain "dev", "rc", "alpha", or "beta"
|
|
49
|
+
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
50
|
+
environment = "development"
|
|
51
|
+
else:
|
|
52
|
+
environment = "production"
|
|
53
|
+
|
|
54
|
+
# Initialize Sentry
|
|
55
|
+
sentry_sdk.init(
|
|
56
|
+
dsn=dsn,
|
|
57
|
+
release=f"shotgun-sh@{__version__}",
|
|
58
|
+
environment=environment,
|
|
59
|
+
send_default_pii=False, # Privacy-first: never send PII
|
|
60
|
+
traces_sample_rate=0.1 if environment == "production" else 1.0,
|
|
61
|
+
profiles_sample_rate=0.1 if environment == "production" else 1.0,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Set user context with anonymous user ID from config
|
|
65
|
+
try:
|
|
66
|
+
from shotgun.agents.config import get_config_manager
|
|
67
|
+
|
|
68
|
+
config_manager = get_config_manager()
|
|
69
|
+
user_id = config_manager.get_user_id()
|
|
70
|
+
sentry_sdk.set_user({"id": user_id})
|
|
71
|
+
logger.debug("Sentry user context set with anonymous ID")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.warning("Failed to set Sentry user context: %s", e)
|
|
74
|
+
|
|
75
|
+
logger.debug(
|
|
76
|
+
"Sentry observability configured successfully (environment: %s, version: %s)",
|
|
77
|
+
environment,
|
|
78
|
+
__version__,
|
|
79
|
+
)
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
except ImportError as e:
|
|
83
|
+
logger.error("Sentry SDK not available: %s", e)
|
|
84
|
+
return False
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.warning("Failed to setup Sentry observability: %s", e)
|
|
87
|
+
return False
|
shotgun/telemetry.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Observability setup for Logfire."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from shotgun.logging_config import get_early_logger
|
|
6
|
+
from shotgun.utils.env_utils import is_falsy, is_truthy
|
|
7
|
+
|
|
8
|
+
# Use early logger to prevent automatic StreamHandler creation
|
|
9
|
+
logger = get_early_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def setup_logfire_observability() -> bool:
|
|
13
|
+
"""Set up Logfire observability if enabled.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
True if Logfire was successfully set up, False otherwise
|
|
17
|
+
"""
|
|
18
|
+
# Try to get Logfire configuration from build constants first, fall back to env vars
|
|
19
|
+
logfire_enabled = None
|
|
20
|
+
logfire_token = None
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from shotgun.build_constants import LOGFIRE_ENABLED, LOGFIRE_TOKEN
|
|
24
|
+
|
|
25
|
+
# Use build constants if they're not empty
|
|
26
|
+
if LOGFIRE_ENABLED:
|
|
27
|
+
logfire_enabled = LOGFIRE_ENABLED
|
|
28
|
+
if LOGFIRE_TOKEN:
|
|
29
|
+
logfire_token = LOGFIRE_TOKEN
|
|
30
|
+
except ImportError:
|
|
31
|
+
# No build constants available
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# Fall back to environment variables if not set from build constants
|
|
35
|
+
if not logfire_enabled:
|
|
36
|
+
logfire_enabled = os.getenv("LOGFIRE_ENABLED", "false")
|
|
37
|
+
if not logfire_token:
|
|
38
|
+
logfire_token = os.getenv("LOGFIRE_TOKEN")
|
|
39
|
+
|
|
40
|
+
# Allow environment variable to override and disable Logfire
|
|
41
|
+
env_override = os.getenv("LOGFIRE_ENABLED")
|
|
42
|
+
if env_override and is_falsy(env_override):
|
|
43
|
+
logfire_enabled = env_override
|
|
44
|
+
|
|
45
|
+
# Check if Logfire observability is enabled
|
|
46
|
+
if not is_truthy(logfire_enabled):
|
|
47
|
+
logger.debug("Logfire observability disabled via LOGFIRE_ENABLED")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
import logfire
|
|
52
|
+
|
|
53
|
+
# Check for Logfire token
|
|
54
|
+
if not logfire_token:
|
|
55
|
+
logger.warning("LOGFIRE_TOKEN not set, Logfire observability disabled")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# Configure Logfire
|
|
59
|
+
# Always disable console output - we only want telemetry sent to the web service
|
|
60
|
+
logfire.configure(
|
|
61
|
+
token=logfire_token,
|
|
62
|
+
console=False, # Never output to console, only send to Logfire service
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Instrument Pydantic AI for better observability
|
|
66
|
+
logfire.instrument_pydantic_ai()
|
|
67
|
+
|
|
68
|
+
# Set user context using baggage for all logs and spans
|
|
69
|
+
try:
|
|
70
|
+
from opentelemetry import baggage, context
|
|
71
|
+
|
|
72
|
+
from shotgun.agents.config import get_config_manager
|
|
73
|
+
|
|
74
|
+
config_manager = get_config_manager()
|
|
75
|
+
user_id = config_manager.get_user_id()
|
|
76
|
+
|
|
77
|
+
# Set user_id as baggage in global context - this will be included in all logs/spans
|
|
78
|
+
ctx = baggage.set_baggage("user_id", user_id)
|
|
79
|
+
context.attach(ctx)
|
|
80
|
+
logger.debug("Logfire user context set with user_id: %s", user_id)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.warning("Failed to set Logfire user context: %s", e)
|
|
83
|
+
|
|
84
|
+
logger.debug("Logfire observability configured successfully")
|
|
85
|
+
logger.debug("Token configured: %s", "Yes" if logfire_token else "No")
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
except ImportError as e:
|
|
89
|
+
logger.warning("Logfire not available: %s", e)
|
|
90
|
+
return False
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning("Failed to setup Logfire observability: %s", e)
|
|
93
|
+
return False
|
shotgun/tui/__init__.py
ADDED
|
File without changes
|