shotgun-sh 0.2.17__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.
Files changed (112) hide show
  1. shotgun/agents/agent_manager.py +28 -14
  2. shotgun/agents/common.py +1 -1
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +323 -53
  6. shotgun/agents/config/models.py +85 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/runner.py +230 -0
  23. shotgun/agents/tools/web_search/openai.py +1 -1
  24. shotgun/build_constants.py +2 -2
  25. shotgun/cli/clear.py +1 -1
  26. shotgun/cli/compact.py +5 -3
  27. shotgun/cli/context.py +44 -1
  28. shotgun/cli/error_handler.py +24 -0
  29. shotgun/cli/export.py +34 -34
  30. shotgun/cli/plan.py +34 -34
  31. shotgun/cli/research.py +17 -9
  32. shotgun/cli/spec/__init__.py +5 -0
  33. shotgun/cli/spec/backup.py +81 -0
  34. shotgun/cli/spec/commands.py +132 -0
  35. shotgun/cli/spec/models.py +48 -0
  36. shotgun/cli/spec/pull_service.py +219 -0
  37. shotgun/cli/specify.py +20 -19
  38. shotgun/cli/tasks.py +34 -34
  39. shotgun/codebase/core/ingestor.py +153 -7
  40. shotgun/codebase/models.py +2 -0
  41. shotgun/exceptions.py +325 -0
  42. shotgun/llm_proxy/__init__.py +17 -0
  43. shotgun/llm_proxy/client.py +215 -0
  44. shotgun/llm_proxy/models.py +137 -0
  45. shotgun/logging_config.py +42 -0
  46. shotgun/main.py +4 -0
  47. shotgun/posthog_telemetry.py +1 -1
  48. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -3
  49. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  50. shotgun/prompts/agents/plan.j2 +16 -0
  51. shotgun/prompts/agents/research.j2 +16 -3
  52. shotgun/prompts/agents/specify.j2 +54 -1
  53. shotgun/prompts/agents/state/system_state.j2 +0 -2
  54. shotgun/prompts/agents/tasks.j2 +16 -0
  55. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  56. shotgun/prompts/history/combine_summaries.j2 +53 -0
  57. shotgun/sdk/codebase.py +14 -3
  58. shotgun/settings.py +5 -0
  59. shotgun/shotgun_web/__init__.py +67 -1
  60. shotgun/shotgun_web/client.py +42 -1
  61. shotgun/shotgun_web/constants.py +46 -0
  62. shotgun/shotgun_web/exceptions.py +29 -0
  63. shotgun/shotgun_web/models.py +390 -0
  64. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  65. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  66. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  67. shotgun/shotgun_web/shared_specs/models.py +71 -0
  68. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  69. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  70. shotgun/shotgun_web/specs_client.py +703 -0
  71. shotgun/shotgun_web/supabase_client.py +31 -0
  72. shotgun/tui/app.py +73 -9
  73. shotgun/tui/containers.py +1 -1
  74. shotgun/tui/layout.py +5 -0
  75. shotgun/tui/screens/chat/chat_screen.py +372 -95
  76. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  77. shotgun/tui/screens/chat_screen/command_providers.py +13 -2
  78. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  79. shotgun/tui/screens/confirmation_dialog.py +40 -0
  80. shotgun/tui/screens/directory_setup.py +45 -41
  81. shotgun/tui/screens/feedback.py +10 -3
  82. shotgun/tui/screens/github_issue.py +11 -2
  83. shotgun/tui/screens/model_picker.py +28 -8
  84. shotgun/tui/screens/onboarding.py +149 -0
  85. shotgun/tui/screens/pipx_migration.py +58 -6
  86. shotgun/tui/screens/provider_config.py +66 -8
  87. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  88. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  89. shotgun/tui/screens/shared_specs/models.py +56 -0
  90. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  91. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  92. shotgun/tui/screens/shotgun_auth.py +110 -16
  93. shotgun/tui/screens/spec_pull.py +288 -0
  94. shotgun/tui/screens/welcome.py +123 -0
  95. shotgun/tui/services/conversation_service.py +5 -2
  96. shotgun/tui/widgets/widget_coordinator.py +1 -1
  97. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
  98. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
  99. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  100. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  101. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  102. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  103. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  104. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  105. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  106. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  107. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  108. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  109. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  110. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  111. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +0 -0
  112. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,71 @@
1
+ """Pydantic models for the shared specs upload pipeline."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from shotgun.shotgun_web.models import FileMetadata
6
+ from shotgun.shotgun_web.shared_specs.utils import UploadPhase
7
+
8
+
9
+ class UploadProgress(BaseModel):
10
+ """Progress information for the upload pipeline.
11
+
12
+ Attributes:
13
+ phase: Current phase of the pipeline
14
+ current: Current item number in the phase
15
+ total: Total items in the phase
16
+ current_file: Name of the file currently being processed
17
+ bytes_uploaded: Total bytes uploaded so far
18
+ total_bytes: Total bytes to upload
19
+ message: Human-readable status message
20
+ """
21
+
22
+ phase: UploadPhase
23
+ current: int = 0
24
+ total: int = 0
25
+ current_file: str | None = None
26
+ bytes_uploaded: int = 0
27
+ total_bytes: int = 0
28
+ message: str = ""
29
+
30
+
31
+ class UploadResult(BaseModel):
32
+ """Result of the upload pipeline.
33
+
34
+ Attributes:
35
+ success: Whether the upload completed successfully
36
+ web_url: URL to view the spec version (on success)
37
+ error: Error message (on failure)
38
+ files_uploaded: Number of files uploaded
39
+ total_bytes: Total bytes uploaded
40
+ """
41
+
42
+ success: bool
43
+ web_url: str | None = None
44
+ error: str | None = None
45
+ files_uploaded: int = 0
46
+ total_bytes: int = 0
47
+
48
+
49
+ class FileWithHash(BaseModel):
50
+ """File metadata with computed hash."""
51
+
52
+ metadata: FileMetadata
53
+ content_hash: str = ""
54
+
55
+
56
+ class UploadState(BaseModel):
57
+ """Internal state for upload progress tracking."""
58
+
59
+ files_uploaded: int = 0
60
+ bytes_uploaded: int = 0
61
+ total_bytes: int = 0
62
+ current_file: str | None = None
63
+ hashes_completed: int = 0
64
+ total_files: int = 0
65
+
66
+
67
+ class ScanResult(BaseModel):
68
+ """Result of scanning .shotgun/ directory."""
69
+
70
+ files: list[FileMetadata]
71
+ total_files_before_filter: int
@@ -0,0 +1,329 @@
1
+ """Upload pipeline for .shotgun/ directory to Specs API."""
2
+
3
+ import asyncio
4
+ import time
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ from shotgun.logging_config import get_logger
9
+ from shotgun.posthog_telemetry import track_event
10
+ from shotgun.shotgun_web.models import FileMetadata
11
+ from shotgun.shotgun_web.shared_specs.file_scanner import (
12
+ scan_shotgun_directory_with_counts,
13
+ )
14
+ from shotgun.shotgun_web.shared_specs.hasher import calculate_sha256
15
+ from shotgun.shotgun_web.shared_specs.models import (
16
+ FileWithHash,
17
+ UploadProgress,
18
+ UploadResult,
19
+ UploadState,
20
+ )
21
+ from shotgun.shotgun_web.shared_specs.utils import UploadPhase, format_bytes
22
+ from shotgun.shotgun_web.specs_client import SpecsClient
23
+
24
+ logger = get_logger(__name__)
25
+
26
+ # Maximum concurrent hash calculations
27
+ MAX_CONCURRENT_HASHES = 10
28
+
29
+ # Maximum concurrent file uploads
30
+ MAX_CONCURRENT_UPLOADS = 3
31
+
32
+
33
+ async def run_upload_pipeline(
34
+ workspace_id: str,
35
+ spec_id: str,
36
+ version_id: str,
37
+ project_root: Path | None = None,
38
+ on_progress: Callable[[UploadProgress], None] | None = None,
39
+ ) -> UploadResult:
40
+ """Run the complete upload pipeline for a spec version.
41
+
42
+ Scans the .shotgun/ directory, calculates hashes for all files,
43
+ uploads them to the API, and closes the version.
44
+
45
+ Args:
46
+ workspace_id: Workspace UUID
47
+ spec_id: Spec UUID
48
+ version_id: Version UUID
49
+ project_root: Project root containing .shotgun/ directory (defaults to cwd)
50
+ on_progress: Optional callback for progress updates
51
+
52
+ Returns:
53
+ UploadResult with success status and web URL or error message
54
+ """
55
+ if project_root is None:
56
+ project_root = Path.cwd()
57
+
58
+ state = UploadState()
59
+ start_time = time.time()
60
+ current_phase: UploadPhase = UploadPhase.CREATING
61
+ track_event("spec_upload_started")
62
+
63
+ def report_progress(progress: UploadProgress) -> None:
64
+ """Report progress to callback if provided."""
65
+ if on_progress:
66
+ on_progress(progress)
67
+
68
+ try:
69
+ # Phase 1: Scan files
70
+ current_phase = UploadPhase.SCANNING
71
+ report_progress(
72
+ UploadProgress(
73
+ phase=UploadPhase.SCANNING,
74
+ message="Scanning .shotgun/ directory...",
75
+ )
76
+ )
77
+
78
+ scan_result = await scan_shotgun_directory_with_counts(project_root)
79
+ files = scan_result.files
80
+ state.total_files = len(files)
81
+
82
+ if not files:
83
+ # Distinguish between empty directory and all files filtered
84
+ if scan_result.total_files_before_filter > 0:
85
+ error_message = (
86
+ "No shareable files found. All files matched ignore patterns."
87
+ )
88
+ else:
89
+ error_message = (
90
+ "No files to share. Add specifications to .shotgun/ first."
91
+ )
92
+
93
+ track_event(
94
+ "spec_upload_failed",
95
+ {
96
+ "error_type": "EmptyDirectory",
97
+ "phase": current_phase.value,
98
+ "files_uploaded": 0,
99
+ "bytes_uploaded": 0,
100
+ },
101
+ )
102
+ report_progress(
103
+ UploadProgress(
104
+ phase=UploadPhase.ERROR,
105
+ message=error_message,
106
+ )
107
+ )
108
+ return UploadResult(
109
+ success=False,
110
+ files_uploaded=0,
111
+ total_bytes=0,
112
+ error=error_message,
113
+ )
114
+
115
+ # Calculate total size
116
+ state.total_bytes = sum(f.size_bytes for f in files)
117
+
118
+ report_progress(
119
+ UploadProgress(
120
+ phase=UploadPhase.SCANNING,
121
+ total=state.total_files,
122
+ total_bytes=state.total_bytes,
123
+ message=f"Found {state.total_files} files ({format_bytes(state.total_bytes)})",
124
+ )
125
+ )
126
+
127
+ # Phase 2: Calculate hashes
128
+ current_phase = UploadPhase.HASHING
129
+ report_progress(
130
+ UploadProgress(
131
+ phase=UploadPhase.HASHING,
132
+ current=0,
133
+ total=state.total_files,
134
+ message="Calculating file hashes...",
135
+ )
136
+ )
137
+
138
+ files_with_hashes = await _calculate_hashes(files, state, report_progress)
139
+
140
+ # Phase 3: Upload files
141
+ current_phase = UploadPhase.UPLOADING
142
+ report_progress(
143
+ UploadProgress(
144
+ phase=UploadPhase.UPLOADING,
145
+ current=0,
146
+ total=state.total_files,
147
+ total_bytes=state.total_bytes,
148
+ message="Uploading files...",
149
+ )
150
+ )
151
+
152
+ client = SpecsClient()
153
+ await _upload_files(
154
+ client,
155
+ workspace_id,
156
+ spec_id,
157
+ version_id,
158
+ files_with_hashes,
159
+ state,
160
+ report_progress,
161
+ )
162
+
163
+ # Phase 4: Close version
164
+ current_phase = UploadPhase.CLOSING
165
+ report_progress(
166
+ UploadProgress(
167
+ phase=UploadPhase.CLOSING,
168
+ current=state.files_uploaded,
169
+ total=state.total_files,
170
+ bytes_uploaded=state.bytes_uploaded,
171
+ total_bytes=state.total_bytes,
172
+ message="Finalizing version...",
173
+ )
174
+ )
175
+
176
+ close_response = await client.close_version(workspace_id, spec_id, version_id)
177
+
178
+ # Complete
179
+ report_progress(
180
+ UploadProgress(
181
+ phase=UploadPhase.COMPLETE,
182
+ current=state.files_uploaded,
183
+ total=state.total_files,
184
+ bytes_uploaded=state.bytes_uploaded,
185
+ total_bytes=state.total_bytes,
186
+ message="Upload complete!",
187
+ )
188
+ )
189
+
190
+ # Track successful completion
191
+ duration = time.time() - start_time
192
+ track_event(
193
+ "spec_upload_completed",
194
+ {
195
+ "file_count": state.files_uploaded,
196
+ "total_bytes": state.bytes_uploaded,
197
+ "duration_seconds": round(duration, 2),
198
+ },
199
+ )
200
+
201
+ return UploadResult(
202
+ success=True,
203
+ web_url=close_response.web_url,
204
+ files_uploaded=state.files_uploaded,
205
+ total_bytes=state.bytes_uploaded,
206
+ )
207
+
208
+ except Exception as e:
209
+ logger.error(f"Upload pipeline failed: {e}", exc_info=True)
210
+ track_event(
211
+ "spec_upload_failed",
212
+ {
213
+ "error_type": type(e).__name__,
214
+ "phase": current_phase.value,
215
+ "files_uploaded": state.files_uploaded,
216
+ "bytes_uploaded": state.bytes_uploaded,
217
+ },
218
+ )
219
+ report_progress(
220
+ UploadProgress(
221
+ phase=UploadPhase.ERROR,
222
+ current=state.files_uploaded,
223
+ total=state.total_files,
224
+ bytes_uploaded=state.bytes_uploaded,
225
+ total_bytes=state.total_bytes,
226
+ message=f"Upload failed: {e}",
227
+ )
228
+ )
229
+ return UploadResult(
230
+ success=False,
231
+ error=str(e),
232
+ files_uploaded=state.files_uploaded,
233
+ total_bytes=state.bytes_uploaded,
234
+ )
235
+
236
+
237
+ async def _calculate_hashes(
238
+ files: list[FileMetadata],
239
+ state: UploadState,
240
+ report_progress: Callable[[UploadProgress], None],
241
+ ) -> list[FileWithHash]:
242
+ """Calculate hashes for all files with progress reporting.
243
+
244
+ Uses semaphore to limit concurrent hash operations.
245
+ """
246
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_HASHES)
247
+ files_with_hashes: list[FileWithHash] = []
248
+ lock = asyncio.Lock()
249
+
250
+ async def hash_file(file_meta: FileMetadata) -> FileWithHash:
251
+ async with semaphore:
252
+ content_hash = await calculate_sha256(file_meta.absolute_path)
253
+
254
+ # Update progress
255
+ async with lock:
256
+ state.hashes_completed += 1
257
+ report_progress(
258
+ UploadProgress(
259
+ phase=UploadPhase.HASHING,
260
+ current=state.hashes_completed,
261
+ total=state.total_files,
262
+ current_file=file_meta.relative_path,
263
+ message=f"Hashing {file_meta.relative_path}",
264
+ )
265
+ )
266
+
267
+ return FileWithHash(metadata=file_meta, content_hash=content_hash)
268
+
269
+ # Run hash calculations concurrently
270
+ results = await asyncio.gather(*[hash_file(f) for f in files])
271
+ files_with_hashes = list(results)
272
+
273
+ return files_with_hashes
274
+
275
+
276
+ async def _upload_files(
277
+ client: SpecsClient,
278
+ workspace_id: str,
279
+ spec_id: str,
280
+ version_id: str,
281
+ files: list[FileWithHash],
282
+ state: UploadState,
283
+ report_progress: Callable[[UploadProgress], None],
284
+ ) -> None:
285
+ """Upload all files with progress reporting.
286
+
287
+ Uses semaphore to limit concurrent uploads.
288
+ """
289
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_UPLOADS)
290
+ lock = asyncio.Lock()
291
+
292
+ async def upload_file(file: FileWithHash) -> None:
293
+ async with semaphore:
294
+ # Initiate upload to get presigned URL
295
+ response = await client.initiate_file_upload(
296
+ workspace_id,
297
+ spec_id,
298
+ version_id,
299
+ file.metadata.relative_path,
300
+ file.metadata.size_bytes,
301
+ file.content_hash,
302
+ )
303
+
304
+ # Upload to presigned URL
305
+ await client.upload_file_to_presigned_url(
306
+ response.upload_url,
307
+ file.metadata.absolute_path,
308
+ )
309
+
310
+ # Update progress
311
+ async with lock:
312
+ state.files_uploaded += 1
313
+ state.bytes_uploaded += file.metadata.size_bytes
314
+ state.current_file = file.metadata.relative_path
315
+
316
+ report_progress(
317
+ UploadProgress(
318
+ phase=UploadPhase.UPLOADING,
319
+ current=state.files_uploaded,
320
+ total=state.total_files,
321
+ current_file=file.metadata.relative_path,
322
+ bytes_uploaded=state.bytes_uploaded,
323
+ total_bytes=state.total_bytes,
324
+ message=f"Uploaded {file.metadata.relative_path}",
325
+ )
326
+ )
327
+
328
+ # Run uploads concurrently
329
+ await asyncio.gather(*[upload_file(f) for f in files])
@@ -0,0 +1,34 @@
1
+ """Utility functions for shared specs module."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class UploadPhase(StrEnum):
7
+ """Upload pipeline phases."""
8
+
9
+ CREATING = "creating" # Creating spec/version via API
10
+ SCANNING = "scanning"
11
+ HASHING = "hashing"
12
+ UPLOADING = "uploading"
13
+ CLOSING = "closing"
14
+ COMPLETE = "complete"
15
+ ERROR = "error"
16
+
17
+
18
+ def format_bytes(size: int) -> str:
19
+ """Format bytes as human-readable string.
20
+
21
+ Args:
22
+ size: Size in bytes
23
+
24
+ Returns:
25
+ Human-readable string like "1.5 KB" or "2.3 MB"
26
+ """
27
+ if size < 1024:
28
+ return f"{size} B"
29
+ elif size < 1024 * 1024:
30
+ return f"{size / 1024:.1f} KB"
31
+ elif size < 1024 * 1024 * 1024:
32
+ return f"{size / (1024 * 1024):.1f} MB"
33
+ else:
34
+ return f"{size / (1024 * 1024 * 1024):.1f} GB"