shotgun-sh 0.1.16.dev2__tar.gz → 0.2.1__tar.gz

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 (160) hide show
  1. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/.gitignore +3 -0
  2. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/PKG-INFO +2 -2
  3. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/pyproject.toml +2 -2
  4. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/common.py +4 -5
  5. shotgun_sh-0.2.1/src/shotgun/agents/config/constants.py +34 -0
  6. shotgun_sh-0.2.1/src/shotgun/agents/config/manager.py +457 -0
  7. shotgun_sh-0.2.1/src/shotgun/agents/config/models.py +175 -0
  8. shotgun_sh-0.2.1/src/shotgun/agents/config/provider.py +295 -0
  9. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/compaction.py +1 -1
  10. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/history_processors.py +18 -9
  11. shotgun_sh-0.2.1/src/shotgun/agents/history/token_counting/__init__.py +31 -0
  12. shotgun_sh-0.2.1/src/shotgun/agents/history/token_counting/anthropic.py +89 -0
  13. shotgun_sh-0.2.1/src/shotgun/agents/history/token_counting/base.py +67 -0
  14. shotgun_sh-0.2.1/src/shotgun/agents/history/token_counting/openai.py +80 -0
  15. shotgun_sh-0.2.1/src/shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
  16. shotgun_sh-0.2.1/src/shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
  17. shotgun_sh-0.2.1/src/shotgun/agents/history/token_counting/utils.py +147 -0
  18. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/token_estimation.py +12 -12
  19. shotgun_sh-0.2.1/src/shotgun/agents/llm.py +62 -0
  20. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/models.py +2 -2
  21. shotgun_sh-0.2.1/src/shotgun/agents/tools/web_search/__init__.py +87 -0
  22. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/web_search/anthropic.py +54 -50
  23. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/web_search/gemini.py +31 -20
  24. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/web_search/openai.py +4 -4
  25. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/build_constants.py +2 -2
  26. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/config.py +34 -63
  27. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/feedback.py +4 -2
  28. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/models.py +2 -2
  29. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/ingestor.py +47 -8
  30. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/manager.py +7 -3
  31. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/models.py +4 -4
  32. shotgun_sh-0.2.1/src/shotgun/llm_proxy/__init__.py +16 -0
  33. shotgun_sh-0.2.1/src/shotgun/llm_proxy/clients.py +39 -0
  34. shotgun_sh-0.2.1/src/shotgun/llm_proxy/constants.py +8 -0
  35. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/main.py +6 -0
  36. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/posthog_telemetry.py +15 -11
  37. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/sentry_telemetry.py +3 -3
  38. shotgun_sh-0.2.1/src/shotgun/shotgun_web/__init__.py +19 -0
  39. shotgun_sh-0.2.1/src/shotgun/shotgun_web/client.py +138 -0
  40. shotgun_sh-0.2.1/src/shotgun/shotgun_web/constants.py +17 -0
  41. shotgun_sh-0.2.1/src/shotgun/shotgun_web/models.py +47 -0
  42. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/telemetry.py +7 -4
  43. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/app.py +26 -8
  44. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/chat.py +2 -8
  45. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/chat_screen/command_providers.py +118 -11
  46. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/chat_screen/history.py +3 -1
  47. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/feedback.py +2 -2
  48. shotgun_sh-0.2.1/src/shotgun/tui/screens/model_picker.py +327 -0
  49. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/provider_config.py +118 -28
  50. shotgun_sh-0.2.1/src/shotgun/tui/screens/shotgun_auth.py +295 -0
  51. shotgun_sh-0.2.1/src/shotgun/tui/screens/welcome.py +176 -0
  52. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/utils/env_utils.py +12 -0
  53. shotgun_sh-0.1.16.dev2/src/shotgun/agents/config/constants.py +0 -17
  54. shotgun_sh-0.1.16.dev2/src/shotgun/agents/config/manager.py +0 -294
  55. shotgun_sh-0.1.16.dev2/src/shotgun/agents/config/models.py +0 -185
  56. shotgun_sh-0.1.16.dev2/src/shotgun/agents/config/provider.py +0 -206
  57. shotgun_sh-0.1.16.dev2/src/shotgun/agents/history/token_counting.py +0 -429
  58. shotgun_sh-0.1.16.dev2/src/shotgun/agents/tools/web_search/__init__.py +0 -60
  59. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/LICENSE +0 -0
  60. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/README.md +0 -0
  61. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/hatch_build.py +0 -0
  62. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/__init__.py +0 -0
  63. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/__init__.py +0 -0
  64. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/agent_manager.py +0 -0
  65. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/config/__init__.py +0 -0
  66. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/conversation_history.py +0 -0
  67. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/conversation_manager.py +0 -0
  68. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/export.py +0 -0
  69. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/__init__.py +0 -0
  70. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/constants.py +0 -0
  71. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/context_extraction.py +0 -0
  72. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/history_building.py +0 -0
  73. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/history/message_utils.py +0 -0
  74. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/messages.py +0 -0
  75. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/plan.py +0 -0
  76. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/research.py +0 -0
  77. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/specify.py +0 -0
  78. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tasks.py +0 -0
  79. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/__init__.py +0 -0
  80. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  81. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  82. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  83. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  84. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/codebase/models.py +0 -0
  85. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  86. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  87. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/file_management.py +0 -0
  88. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/user_interaction.py +0 -0
  89. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  90. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/agents/usage_manager.py +0 -0
  91. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/__init__.py +0 -0
  92. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/codebase/__init__.py +0 -0
  93. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/codebase/commands.py +0 -0
  94. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/codebase/models.py +0 -0
  95. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/export.py +0 -0
  96. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/plan.py +0 -0
  97. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/research.py +0 -0
  98. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/specify.py +0 -0
  99. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/tasks.py +0 -0
  100. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/update.py +0 -0
  101. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/cli/utils.py +0 -0
  102. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/__init__.py +0 -0
  103. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/__init__.py +0 -0
  104. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/change_detector.py +0 -0
  105. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  106. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/cypher_models.py +0 -0
  107. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/language_config.py +0 -0
  108. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/nl_query.py +0 -0
  109. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/core/parser_loader.py +0 -0
  110. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/codebase/service.py +0 -0
  111. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/logging_config.py +0 -0
  112. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/__init__.py +0 -0
  113. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/__init__.py +0 -0
  114. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/export.j2 +0 -0
  115. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  116. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  117. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  118. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  119. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/plan.j2 +0 -0
  120. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/research.j2 +0 -0
  121. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/specify.j2 +0 -0
  122. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  123. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  124. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  125. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/codebase/__init__.py +0 -0
  126. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  127. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
  128. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  129. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  130. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  131. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  132. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/history/__init__.py +0 -0
  133. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  134. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/history/summarization.j2 +0 -0
  135. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/prompts/loader.py +0 -0
  136. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/py.typed +0 -0
  137. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/sdk/__init__.py +0 -0
  138. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/sdk/codebase.py +0 -0
  139. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/sdk/exceptions.py +0 -0
  140. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/sdk/models.py +0 -0
  141. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/sdk/services.py +0 -0
  142. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/__init__.py +0 -0
  143. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/commands/__init__.py +0 -0
  144. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/components/prompt_input.py +0 -0
  145. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/components/spinner.py +0 -0
  146. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/components/splash.py +0 -0
  147. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/components/vertical_tail.py +0 -0
  148. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/filtered_codebase_service.py +0 -0
  149. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/chat.tcss +0 -0
  150. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  151. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  152. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/directory_setup.py +0 -0
  153. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/screens/splash.py +0 -0
  154. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/styles.tcss +0 -0
  155. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/utils/__init__.py +0 -0
  156. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/tui/utils/mode_progress.py +0 -0
  157. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/utils/__init__.py +0 -0
  158. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/utils/file_system_utils.py +0 -0
  159. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/utils/source_detection.py +0 -0
  160. {shotgun_sh-0.1.16.dev2 → shotgun_sh-0.2.1}/src/shotgun/utils/update_checker.py +0 -0
@@ -15,6 +15,9 @@ src/shotgun/build_constants.py
15
15
 
16
16
  .DS_Store
17
17
 
18
+ # Tokenizer model files (downloaded on first use)
19
+ *.model
20
+
18
21
  # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
19
22
  # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode
20
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.16.dev2
3
+ Version: 0.2.1
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -23,7 +23,6 @@ Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.11
24
24
  Requires-Dist: anthropic>=0.39.0
25
25
  Requires-Dist: genai-prices>=0.0.27
26
- Requires-Dist: google-generativeai>=0.8.5
27
26
  Requires-Dist: httpx>=0.27.0
28
27
  Requires-Dist: jinja2>=3.1.0
29
28
  Requires-Dist: kuzu>=0.7.0
@@ -33,6 +32,7 @@ Requires-Dist: packaging>=23.0
33
32
  Requires-Dist: posthog>=3.0.0
34
33
  Requires-Dist: pydantic-ai>=0.0.14
35
34
  Requires-Dist: rich>=13.0.0
35
+ Requires-Dist: sentencepiece>=0.2.0
36
36
  Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
37
37
  Requires-Dist: textual-dev>=1.7.0
38
38
  Requires-Dist: textual>=6.1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.1.16.dev2"
3
+ version = "0.2.1"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -42,8 +42,8 @@ dependencies = [
42
42
  "watchdog>=4.0.0",
43
43
  "openai>=1.0.0",
44
44
  "anthropic>=0.39.0",
45
- "google-generativeai>=0.8.5",
46
45
  "tiktoken>=0.7.0",
46
+ "sentencepiece>=0.2.0",
47
47
  "packaging>=23.0",
48
48
  "genai-prices>=0.0.27",
49
49
  ]
@@ -18,7 +18,7 @@ from pydantic_ai.messages import (
18
18
  ModelRequest,
19
19
  )
20
20
 
21
- from shotgun.agents.config import ProviderType, get_config_manager, get_provider_model
21
+ from shotgun.agents.config import ProviderType, get_provider_model
22
22
  from shotgun.agents.models import AgentType
23
23
  from shotgun.logging_config import get_logger
24
24
  from shotgun.prompts import PromptLoader
@@ -115,14 +115,13 @@ def create_base_agent(
115
115
  """
116
116
  ensure_shotgun_directory_exists()
117
117
 
118
- # Get configured model or fall back to hardcoded default
118
+ # Get configured model or fall back to first available provider
119
119
  try:
120
120
  model_config = get_provider_model(provider)
121
- config_manager = get_config_manager()
122
- provider_name = provider or config_manager.load().default_provider
121
+ provider_name = model_config.provider
123
122
  logger.debug(
124
123
  "🤖 Creating agent with configured %s model: %s",
125
- provider_name.upper(),
124
+ provider_name.value.upper(),
126
125
  model_config.name,
127
126
  )
128
127
  # Use the Model instance directly (has API key baked in)
@@ -0,0 +1,34 @@
1
+ """Configuration constants for Shotgun agents."""
2
+
3
+ from enum import StrEnum, auto
4
+
5
+ # Field names
6
+ API_KEY_FIELD = "api_key"
7
+ SUPABASE_JWT_FIELD = "supabase_jwt"
8
+ SHOTGUN_INSTANCE_ID_FIELD = "shotgun_instance_id"
9
+ CONFIG_VERSION_FIELD = "config_version"
10
+
11
+
12
+ class ConfigSection(StrEnum):
13
+ """Configuration file section names (JSON keys)."""
14
+
15
+ OPENAI = auto()
16
+ ANTHROPIC = auto()
17
+ GOOGLE = auto()
18
+ SHOTGUN = auto()
19
+
20
+
21
+ # Backwards compatibility - deprecated
22
+ OPENAI_PROVIDER = ConfigSection.OPENAI.value
23
+ ANTHROPIC_PROVIDER = ConfigSection.ANTHROPIC.value
24
+ GOOGLE_PROVIDER = ConfigSection.GOOGLE.value
25
+ SHOTGUN_PROVIDER = ConfigSection.SHOTGUN.value
26
+
27
+ # Environment variable names
28
+ OPENAI_API_KEY_ENV = "OPENAI_API_KEY"
29
+ ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"
30
+ GEMINI_API_KEY_ENV = "GEMINI_API_KEY"
31
+ SHOTGUN_API_KEY_ENV = "SHOTGUN_API_KEY"
32
+
33
+ # Token limits
34
+ MEDIUM_TEXT_8K_TOKENS = 8192 # Default max_tokens for web search requests
@@ -0,0 +1,457 @@
1
+ """Configuration manager for Shotgun CLI."""
2
+
3
+ import json
4
+ import uuid
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from pydantic import SecretStr
9
+
10
+ from shotgun.logging_config import get_logger
11
+ from shotgun.utils import get_shotgun_home
12
+
13
+ from .constants import (
14
+ API_KEY_FIELD,
15
+ SHOTGUN_INSTANCE_ID_FIELD,
16
+ SUPABASE_JWT_FIELD,
17
+ ConfigSection,
18
+ )
19
+ from .models import (
20
+ AnthropicConfig,
21
+ GoogleConfig,
22
+ ModelName,
23
+ OpenAIConfig,
24
+ ProviderType,
25
+ ShotgunAccountConfig,
26
+ ShotgunConfig,
27
+ )
28
+
29
+ logger = get_logger(__name__)
30
+
31
+ # Type alias for provider configuration objects
32
+ ProviderConfig = OpenAIConfig | AnthropicConfig | GoogleConfig | ShotgunAccountConfig
33
+
34
+
35
+ class ConfigManager:
36
+ """Manager for Shotgun configuration."""
37
+
38
+ def __init__(self, config_path: Path | None = None):
39
+ """Initialize ConfigManager.
40
+
41
+ Args:
42
+ config_path: Path to config file. If None, uses default ~/.shotgun-sh/config.json
43
+ """
44
+ if config_path is None:
45
+ self.config_path = get_shotgun_home() / "config.json"
46
+ else:
47
+ self.config_path = config_path
48
+
49
+ self._config: ShotgunConfig | None = None
50
+
51
+ def load(self, force_reload: bool = True) -> ShotgunConfig:
52
+ """Load configuration from file.
53
+
54
+ Args:
55
+ force_reload: If True, reload from disk even if cached (default: True)
56
+
57
+ Returns:
58
+ ShotgunConfig: Loaded configuration or default config if file doesn't exist
59
+ """
60
+ if self._config is not None and not force_reload:
61
+ return self._config
62
+
63
+ if not self.config_path.exists():
64
+ logger.info(
65
+ "Configuration file not found, creating new config at: %s",
66
+ self.config_path,
67
+ )
68
+ # Create new config with generated shotgun_instance_id
69
+ self._config = self.initialize()
70
+ return self._config
71
+
72
+ try:
73
+ with open(self.config_path, encoding="utf-8") as f:
74
+ data = json.load(f)
75
+
76
+ # Migration: Rename user_id to shotgun_instance_id (config v2 -> v3)
77
+ if "user_id" in data and SHOTGUN_INSTANCE_ID_FIELD not in data:
78
+ data[SHOTGUN_INSTANCE_ID_FIELD] = data.pop("user_id")
79
+ data["config_version"] = 3
80
+ logger.info(
81
+ "Migrated config v2->v3: renamed user_id to shotgun_instance_id"
82
+ )
83
+
84
+ # Convert plain text secrets to SecretStr objects
85
+ self._convert_secrets_to_secretstr(data)
86
+
87
+ self._config = ShotgunConfig.model_validate(data)
88
+ logger.debug("Configuration loaded successfully from %s", self.config_path)
89
+
90
+ # Validate selected_model if in BYOK mode (no Shotgun key)
91
+ if not self._provider_has_api_key(self._config.shotgun):
92
+ should_save = False
93
+
94
+ # If selected_model is set, verify its provider has a key
95
+ if self._config.selected_model:
96
+ from .models import MODEL_SPECS
97
+
98
+ if self._config.selected_model in MODEL_SPECS:
99
+ spec = MODEL_SPECS[self._config.selected_model]
100
+ if not self.has_provider_key(spec.provider):
101
+ logger.info(
102
+ "Selected model %s provider has no API key, finding available model",
103
+ self._config.selected_model.value,
104
+ )
105
+ self._config.selected_model = None
106
+ should_save = True
107
+ else:
108
+ logger.info(
109
+ "Selected model %s not found in MODEL_SPECS, resetting",
110
+ self._config.selected_model.value,
111
+ )
112
+ self._config.selected_model = None
113
+ should_save = True
114
+
115
+ # If no selected_model or it was invalid, find first available model
116
+ if not self._config.selected_model:
117
+ for provider in ProviderType:
118
+ if self.has_provider_key(provider):
119
+ # Set to that provider's default model
120
+ from .models import MODEL_SPECS, ModelName
121
+
122
+ # Find default model for this provider
123
+ provider_models = {
124
+ ProviderType.OPENAI: ModelName.GPT_5,
125
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
126
+ ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
127
+ }
128
+
129
+ if provider in provider_models:
130
+ self._config.selected_model = provider_models[provider]
131
+ logger.info(
132
+ "Set selected_model to %s (first available provider)",
133
+ self._config.selected_model.value,
134
+ )
135
+ should_save = True
136
+ break
137
+
138
+ if should_save:
139
+ self.save(self._config)
140
+
141
+ return self._config
142
+
143
+ except Exception as e:
144
+ logger.error(
145
+ "Failed to load configuration from %s: %s", self.config_path, e
146
+ )
147
+ logger.info("Creating new configuration with generated shotgun_instance_id")
148
+ self._config = self.initialize()
149
+ return self._config
150
+
151
+ def save(self, config: ShotgunConfig | None = None) -> None:
152
+ """Save configuration to file.
153
+
154
+ Args:
155
+ config: Configuration to save. If None, saves current loaded config
156
+ """
157
+ if config is None:
158
+ if self._config:
159
+ config = self._config
160
+ else:
161
+ # Create a new config with generated shotgun_instance_id
162
+ config = ShotgunConfig(
163
+ shotgun_instance_id=str(uuid.uuid4()),
164
+ )
165
+
166
+ # Ensure directory exists
167
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
168
+
169
+ try:
170
+ # Convert SecretStr to plain text for JSON serialization
171
+ data = config.model_dump()
172
+ self._convert_secretstr_to_plain(data)
173
+
174
+ with open(self.config_path, "w", encoding="utf-8") as f:
175
+ json.dump(data, f, indent=2, ensure_ascii=False)
176
+
177
+ logger.debug("Configuration saved to %s", self.config_path)
178
+ self._config = config
179
+
180
+ except Exception as e:
181
+ logger.error("Failed to save configuration to %s: %s", self.config_path, e)
182
+ raise
183
+
184
+ def update_provider(self, provider: ProviderType | str, **kwargs: Any) -> None:
185
+ """Update provider configuration.
186
+
187
+ Args:
188
+ provider: Provider to update
189
+ **kwargs: Configuration fields to update (only api_key supported)
190
+ """
191
+ config = self.load()
192
+
193
+ # Get provider config and check if it's shotgun
194
+ provider_config, is_shotgun = self._get_provider_config_and_type(
195
+ config, provider
196
+ )
197
+ # For non-shotgun providers, we need the enum for default provider logic
198
+ provider_enum = None if is_shotgun else self._ensure_provider_enum(provider)
199
+
200
+ # Only support api_key updates
201
+ if API_KEY_FIELD in kwargs:
202
+ api_key_value = kwargs[API_KEY_FIELD]
203
+ provider_config.api_key = (
204
+ SecretStr(api_key_value) if api_key_value is not None else None
205
+ )
206
+
207
+ # Reject other fields
208
+ unsupported_fields = set(kwargs.keys()) - {API_KEY_FIELD}
209
+ if unsupported_fields:
210
+ raise ValueError(f"Unsupported configuration fields: {unsupported_fields}")
211
+
212
+ # If no other providers have keys configured and we just added one,
213
+ # set selected_model to that provider's default model (only for LLM providers, not shotgun)
214
+ if not is_shotgun and API_KEY_FIELD in kwargs and api_key_value is not None:
215
+ # provider_enum is guaranteed to be non-None here since is_shotgun is False
216
+ if provider_enum is None:
217
+ raise RuntimeError("Provider enum should not be None for LLM providers")
218
+ other_providers = [p for p in ProviderType if p != provider_enum]
219
+ has_other_keys = any(self.has_provider_key(p) for p in other_providers)
220
+ if not has_other_keys:
221
+ # Set selected_model to this provider's default model
222
+ from .models import ModelName
223
+
224
+ provider_models = {
225
+ ProviderType.OPENAI: ModelName.GPT_5,
226
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
227
+ ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
228
+ }
229
+ if provider_enum in provider_models:
230
+ config.selected_model = provider_models[provider_enum]
231
+
232
+ self.save(config)
233
+
234
+ def clear_provider_key(self, provider: ProviderType | str) -> None:
235
+ """Remove the API key for the given provider (LLM provider or shotgun)."""
236
+ config = self.load()
237
+
238
+ # Get provider config (shotgun or LLM provider)
239
+ provider_config, _ = self._get_provider_config_and_type(config, provider)
240
+
241
+ provider_config.api_key = None
242
+ self.save(config)
243
+
244
+ def update_selected_model(self, model_name: "ModelName") -> None:
245
+ """Update the selected model.
246
+
247
+ Args:
248
+ model_name: Model to select
249
+ """
250
+ config = self.load()
251
+ config.selected_model = model_name
252
+ self.save(config)
253
+
254
+ def has_provider_key(self, provider: ProviderType | str) -> bool:
255
+ """Check if the given provider has a non-empty API key configured.
256
+
257
+ This checks only the configuration file.
258
+ """
259
+ # Use force_reload=False to avoid infinite loop when called from load()
260
+ config = self.load(force_reload=False)
261
+ provider_enum = self._ensure_provider_enum(provider)
262
+ provider_config = self._get_provider_config(config, provider_enum)
263
+
264
+ return self._provider_has_api_key(provider_config)
265
+
266
+ def has_any_provider_key(self) -> bool:
267
+ """Determine whether any provider has a configured API key."""
268
+ # Use force_reload=False to avoid infinite loop when called from load()
269
+ config = self.load(force_reload=False)
270
+ # Check LLM provider keys (BYOK)
271
+ has_llm_key = any(
272
+ self._provider_has_api_key(self._get_provider_config(config, provider))
273
+ for provider in (
274
+ ProviderType.OPENAI,
275
+ ProviderType.ANTHROPIC,
276
+ ProviderType.GOOGLE,
277
+ )
278
+ )
279
+ # Also check Shotgun Account key
280
+ has_shotgun_key = self._provider_has_api_key(config.shotgun)
281
+ return has_llm_key or has_shotgun_key
282
+
283
+ def initialize(self) -> ShotgunConfig:
284
+ """Initialize configuration with defaults and save to file.
285
+
286
+ Returns:
287
+ Default ShotgunConfig
288
+ """
289
+ # Generate unique shotgun instance ID for new config
290
+ config = ShotgunConfig(
291
+ shotgun_instance_id=str(uuid.uuid4()),
292
+ )
293
+ self.save(config)
294
+ logger.info(
295
+ "Configuration initialized at %s with shotgun_instance_id: %s",
296
+ self.config_path,
297
+ config.shotgun_instance_id,
298
+ )
299
+ return config
300
+
301
+ def _convert_secrets_to_secretstr(self, data: dict[str, Any]) -> None:
302
+ """Convert plain text secrets in data to SecretStr objects."""
303
+ for section in ConfigSection:
304
+ if section.value in data and isinstance(data[section.value], dict):
305
+ # Convert API key
306
+ if (
307
+ API_KEY_FIELD in data[section.value]
308
+ and data[section.value][API_KEY_FIELD] is not None
309
+ ):
310
+ data[section.value][API_KEY_FIELD] = SecretStr(
311
+ data[section.value][API_KEY_FIELD]
312
+ )
313
+ # Convert supabase JWT (shotgun section only)
314
+ if (
315
+ section == ConfigSection.SHOTGUN
316
+ and SUPABASE_JWT_FIELD in data[section.value]
317
+ and data[section.value][SUPABASE_JWT_FIELD] is not None
318
+ ):
319
+ data[section.value][SUPABASE_JWT_FIELD] = SecretStr(
320
+ data[section.value][SUPABASE_JWT_FIELD]
321
+ )
322
+
323
+ def _convert_secretstr_to_plain(self, data: dict[str, Any]) -> None:
324
+ """Convert SecretStr objects in data to plain text for JSON serialization."""
325
+ for section in ConfigSection:
326
+ if section.value in data and isinstance(data[section.value], dict):
327
+ # Convert API key
328
+ if (
329
+ API_KEY_FIELD in data[section.value]
330
+ and data[section.value][API_KEY_FIELD] is not None
331
+ ):
332
+ if hasattr(data[section.value][API_KEY_FIELD], "get_secret_value"):
333
+ data[section.value][API_KEY_FIELD] = data[section.value][
334
+ API_KEY_FIELD
335
+ ].get_secret_value()
336
+ # Convert supabase JWT (shotgun section only)
337
+ if (
338
+ section == ConfigSection.SHOTGUN
339
+ and SUPABASE_JWT_FIELD in data[section.value]
340
+ and data[section.value][SUPABASE_JWT_FIELD] is not None
341
+ ):
342
+ if hasattr(
343
+ data[section.value][SUPABASE_JWT_FIELD], "get_secret_value"
344
+ ):
345
+ data[section.value][SUPABASE_JWT_FIELD] = data[section.value][
346
+ SUPABASE_JWT_FIELD
347
+ ].get_secret_value()
348
+
349
+ def _ensure_provider_enum(self, provider: ProviderType | str) -> ProviderType:
350
+ """Normalize provider values to ProviderType enum."""
351
+ return (
352
+ provider if isinstance(provider, ProviderType) else ProviderType(provider)
353
+ )
354
+
355
+ def _get_provider_config(
356
+ self, config: ShotgunConfig, provider: ProviderType
357
+ ) -> Any:
358
+ """Retrieve the provider-specific configuration section."""
359
+ if provider == ProviderType.OPENAI:
360
+ return config.openai
361
+ if provider == ProviderType.ANTHROPIC:
362
+ return config.anthropic
363
+ if provider == ProviderType.GOOGLE:
364
+ return config.google
365
+ raise ValueError(f"Unsupported provider: {provider}")
366
+
367
+ def _provider_has_api_key(self, provider_config: Any) -> bool:
368
+ """Return True if the provider config contains a usable API key."""
369
+ api_key = getattr(provider_config, API_KEY_FIELD, None)
370
+ if api_key is None:
371
+ return False
372
+
373
+ if isinstance(api_key, SecretStr):
374
+ value = api_key.get_secret_value()
375
+ else:
376
+ value = str(api_key)
377
+
378
+ return bool(value.strip())
379
+
380
+ def _is_shotgun_provider(self, provider: ProviderType | str) -> bool:
381
+ """Check if provider string represents Shotgun Account.
382
+
383
+ Args:
384
+ provider: Provider type or string
385
+
386
+ Returns:
387
+ True if provider is shotgun account
388
+ """
389
+ return (
390
+ isinstance(provider, str)
391
+ and provider.lower() == ConfigSection.SHOTGUN.value
392
+ )
393
+
394
+ def _get_provider_config_and_type(
395
+ self, config: ShotgunConfig, provider: ProviderType | str
396
+ ) -> tuple[ProviderConfig, bool]:
397
+ """Get provider config, handling shotgun as special case.
398
+
399
+ Args:
400
+ config: Shotgun configuration
401
+ provider: Provider type or string
402
+
403
+ Returns:
404
+ Tuple of (provider_config, is_shotgun)
405
+ """
406
+ if self._is_shotgun_provider(provider):
407
+ return (config.shotgun, True)
408
+
409
+ provider_enum = self._ensure_provider_enum(provider)
410
+ return (self._get_provider_config(config, provider_enum), False)
411
+
412
+ def get_shotgun_instance_id(self) -> str:
413
+ """Get the shotgun instance ID from configuration.
414
+
415
+ Returns:
416
+ The unique shotgun instance ID string
417
+ """
418
+ config = self.load()
419
+ return config.shotgun_instance_id
420
+
421
+ def update_shotgun_account(
422
+ self, api_key: str | None = None, supabase_jwt: str | None = None
423
+ ) -> None:
424
+ """Update Shotgun Account configuration.
425
+
426
+ Args:
427
+ api_key: LiteLLM proxy API key (optional)
428
+ supabase_jwt: Supabase authentication JWT (optional)
429
+ """
430
+ config = self.load()
431
+
432
+ if api_key is not None:
433
+ config.shotgun.api_key = SecretStr(api_key) if api_key else None
434
+
435
+ if supabase_jwt is not None:
436
+ config.shotgun.supabase_jwt = (
437
+ SecretStr(supabase_jwt) if supabase_jwt else None
438
+ )
439
+
440
+ self.save(config)
441
+ logger.info("Updated Shotgun Account configuration")
442
+
443
+
444
+ # Global singleton instance
445
+ _config_manager_instance: ConfigManager | None = None
446
+
447
+
448
+ def get_config_manager() -> ConfigManager:
449
+ """Get the global singleton ConfigManager instance.
450
+
451
+ Returns:
452
+ The singleton ConfigManager instance
453
+ """
454
+ global _config_manager_instance
455
+ if _config_manager_instance is None:
456
+ _config_manager_instance = ConfigManager()
457
+ return _config_manager_instance