shotgun-sh 0.2.6.dev5__py3-none-any.whl → 0.2.7.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.

@@ -1,10 +1,13 @@
1
1
  """Agent manager for coordinating multiple AI agents with shared message history."""
2
2
 
3
+ import json
3
4
  import logging
4
5
  from collections.abc import AsyncIterable, Sequence
5
6
  from dataclasses import dataclass, field, is_dataclass, replace
6
7
  from typing import TYPE_CHECKING, Any, cast
7
8
 
9
+ import logfire
10
+
8
11
  if TYPE_CHECKING:
9
12
  from shotgun.agents.conversation_history import ConversationState
10
13
 
@@ -401,6 +404,24 @@ class AgentManager(Widget):
401
404
  else None,
402
405
  **kwargs,
403
406
  )
407
+ except Exception as e:
408
+ # Log the error with full stack trace to shotgun.log and Logfire
409
+ logger.exception(
410
+ "Agent execution failed",
411
+ extra={
412
+ "agent_mode": self._current_agent_type.value,
413
+ "model_name": model_name,
414
+ "error_type": type(e).__name__,
415
+ },
416
+ )
417
+ logfire.exception(
418
+ "Agent execution failed",
419
+ agent_mode=self._current_agent_type.value,
420
+ model_name=model_name,
421
+ error_type=type(e).__name__,
422
+ )
423
+ # Re-raise to let TUI handle user messaging
424
+ raise
404
425
  finally:
405
426
  self._stream_state = None
406
427
 
@@ -468,7 +489,33 @@ class AgentManager(Widget):
468
489
 
469
490
  # Apply compaction to persistent message history to prevent cascading growth
470
491
  all_messages = result.all_messages()
471
- self.message_history = await apply_persistent_compaction(all_messages, deps)
492
+ try:
493
+ logger.debug(
494
+ "Starting message history compaction",
495
+ extra={"message_count": len(all_messages)},
496
+ )
497
+ self.message_history = await apply_persistent_compaction(all_messages, deps)
498
+ logger.debug(
499
+ "Completed message history compaction",
500
+ extra={
501
+ "original_count": len(all_messages),
502
+ "compacted_count": len(self.message_history),
503
+ },
504
+ )
505
+ except Exception as e:
506
+ # If compaction fails, log full error with stack trace and use uncompacted messages
507
+ logger.error(
508
+ "Failed to compact message history - using uncompacted messages",
509
+ exc_info=True,
510
+ extra={
511
+ "error": str(e),
512
+ "message_count": len(all_messages),
513
+ "agent_mode": self._current_agent_type.value,
514
+ },
515
+ )
516
+ # Fallback: use uncompacted messages to prevent data loss
517
+ self.message_history = all_messages
518
+
472
519
  usage = result.usage()
473
520
  deps.usage_manager.add_usage(
474
521
  usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
@@ -554,6 +601,39 @@ class AgentManager(Widget):
554
601
  # Detect source from call stack
555
602
  source = detect_source()
556
603
 
604
+ # Log if tool call has incomplete args (for debugging truncated JSON)
605
+ if isinstance(event.part.args, str):
606
+ try:
607
+ json.loads(event.part.args)
608
+ except (json.JSONDecodeError, ValueError):
609
+ args_preview = (
610
+ event.part.args[:100] + "..."
611
+ if len(event.part.args) > 100
612
+ else event.part.args
613
+ )
614
+ logger.warning(
615
+ "FunctionToolCallEvent received with incomplete JSON args",
616
+ extra={
617
+ "tool_name": event.part.tool_name,
618
+ "tool_call_id": event.part.tool_call_id,
619
+ "args_preview": args_preview,
620
+ "args_length": len(event.part.args)
621
+ if event.part.args
622
+ else 0,
623
+ "agent_mode": self._current_agent_type.value,
624
+ },
625
+ )
626
+ logfire.warn(
627
+ "FunctionToolCallEvent received with incomplete JSON args",
628
+ tool_name=event.part.tool_name,
629
+ tool_call_id=event.part.tool_call_id,
630
+ args_preview=args_preview,
631
+ args_length=len(event.part.args)
632
+ if event.part.args
633
+ else 0,
634
+ agent_mode=self._current_agent_type.value,
635
+ )
636
+
557
637
  track_event(
558
638
  "tool_called",
559
639
  {
@@ -1,5 +1,7 @@
1
1
  """Models and utilities for persisting TUI conversation history."""
2
2
 
3
+ import json
4
+ import logging
3
5
  from datetime import datetime
4
6
  from typing import Any, cast
5
7
 
@@ -7,14 +9,106 @@ from pydantic import BaseModel, ConfigDict, Field
7
9
  from pydantic_ai.messages import (
8
10
  ModelMessage,
9
11
  ModelMessagesTypeAdapter,
12
+ ModelResponse,
13
+ ToolCallPart,
10
14
  )
11
15
  from pydantic_core import to_jsonable_python
12
16
 
13
17
  from shotgun.tui.screens.chat_screen.hint_message import HintMessage
14
18
 
19
+ logger = logging.getLogger(__name__)
20
+
15
21
  SerializedMessage = dict[str, Any]
16
22
 
17
23
 
24
+ def is_tool_call_complete(tool_call: ToolCallPart) -> bool:
25
+ """Check if a tool call has valid, complete JSON arguments.
26
+
27
+ Args:
28
+ tool_call: The tool call part to validate
29
+
30
+ Returns:
31
+ True if the tool call args are valid JSON, False otherwise
32
+ """
33
+ if tool_call.args is None:
34
+ return True # No args is valid
35
+
36
+ if isinstance(tool_call.args, dict):
37
+ return True # Already parsed dict is valid
38
+
39
+ if not isinstance(tool_call.args, str):
40
+ return False
41
+
42
+ # Try to parse the JSON string
43
+ try:
44
+ json.loads(tool_call.args)
45
+ return True
46
+ except (json.JSONDecodeError, ValueError) as e:
47
+ # Log incomplete tool call detection
48
+ args_preview = (
49
+ tool_call.args[:100] + "..."
50
+ if len(tool_call.args) > 100
51
+ else tool_call.args
52
+ )
53
+ logger.info(
54
+ "Detected incomplete tool call in validation",
55
+ extra={
56
+ "tool_name": tool_call.tool_name,
57
+ "tool_call_id": tool_call.tool_call_id,
58
+ "args_preview": args_preview,
59
+ "error": str(e),
60
+ },
61
+ )
62
+ return False
63
+
64
+
65
+ def filter_incomplete_messages(messages: list[ModelMessage]) -> list[ModelMessage]:
66
+ """Filter out messages with incomplete tool calls.
67
+
68
+ Args:
69
+ messages: List of messages to filter
70
+
71
+ Returns:
72
+ List of messages with only complete tool calls
73
+ """
74
+ filtered: list[ModelMessage] = []
75
+ filtered_count = 0
76
+ filtered_tool_names: list[str] = []
77
+
78
+ for message in messages:
79
+ # Only check ModelResponse messages for tool calls
80
+ if not isinstance(message, ModelResponse):
81
+ filtered.append(message)
82
+ continue
83
+
84
+ # Check if any tool calls are incomplete
85
+ has_incomplete_tool_call = False
86
+ for part in message.parts:
87
+ if isinstance(part, ToolCallPart) and not is_tool_call_complete(part):
88
+ has_incomplete_tool_call = True
89
+ filtered_tool_names.append(part.tool_name)
90
+ break
91
+
92
+ # Only include messages without incomplete tool calls
93
+ if not has_incomplete_tool_call:
94
+ filtered.append(message)
95
+ else:
96
+ filtered_count += 1
97
+
98
+ # Log if any messages were filtered
99
+ if filtered_count > 0:
100
+ logger.info(
101
+ "Filtered incomplete messages before saving",
102
+ extra={
103
+ "filtered_count": filtered_count,
104
+ "total_messages": len(messages),
105
+ "filtered_tool_names": filtered_tool_names,
106
+ },
107
+ )
108
+
109
+ return filtered
110
+
111
+
18
112
  class ConversationState(BaseModel):
19
113
  """Represents the complete state of a conversation in memory."""
20
114
 
@@ -46,14 +140,41 @@ class ConversationHistory(BaseModel):
46
140
  Args:
47
141
  messages: List of ModelMessage objects to serialize and store
48
142
  """
143
+ # Filter out messages with incomplete tool calls to prevent corruption
144
+ filtered_messages = filter_incomplete_messages(messages)
145
+
49
146
  # Serialize ModelMessage list to JSON-serializable format
50
147
  self.agent_history = to_jsonable_python(
51
- messages, fallback=lambda x: str(x), exclude_none=True
148
+ filtered_messages, fallback=lambda x: str(x), exclude_none=True
52
149
  )
53
150
 
54
151
  def set_ui_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
55
152
  """Set ui_history from a list of UI messages."""
56
153
 
154
+ # Filter out ModelMessages with incomplete tool calls (keep all HintMessages)
155
+ # We need to maintain message order, so we'll check each message individually
156
+ filtered_messages: list[ModelMessage | HintMessage] = []
157
+
158
+ for msg in messages:
159
+ if isinstance(msg, HintMessage):
160
+ # Always keep hint messages
161
+ filtered_messages.append(msg)
162
+ elif isinstance(msg, ModelResponse):
163
+ # Check if this ModelResponse has incomplete tool calls
164
+ has_incomplete = False
165
+ for part in msg.parts:
166
+ if isinstance(part, ToolCallPart) and not is_tool_call_complete(
167
+ part
168
+ ):
169
+ has_incomplete = True
170
+ break
171
+
172
+ if not has_incomplete:
173
+ filtered_messages.append(msg)
174
+ else:
175
+ # Keep all other ModelMessage types (ModelRequest, etc.)
176
+ filtered_messages.append(msg)
177
+
57
178
  def _serialize_message(
58
179
  message: ModelMessage | HintMessage,
59
180
  ) -> Any:
@@ -68,7 +189,7 @@ class ConversationHistory(BaseModel):
68
189
  payload.setdefault("message_type", "model")
69
190
  return payload
70
191
 
71
- self.ui_history = [_serialize_message(msg) for msg in messages]
192
+ self.ui_history = [_serialize_message(msg) for msg in filtered_messages]
72
193
 
73
194
  def get_agent_messages(self) -> list[ModelMessage]:
74
195
  """Get agent_history as a list of ModelMessage objects.
@@ -1,6 +1,7 @@
1
1
  """Manager for handling conversation persistence operations."""
2
2
 
3
3
  import json
4
+ import shutil
4
5
  from pathlib import Path
5
6
 
6
7
  from shotgun.logging_config import get_logger
@@ -77,9 +78,30 @@ class ConversationManager:
77
78
  )
78
79
  return conversation
79
80
 
80
- except Exception as e:
81
+ except (json.JSONDecodeError, ValueError) as e:
82
+ # Handle corrupted JSON or validation errors
83
+ logger.error(
84
+ "Corrupted conversation file at %s: %s. Creating backup and starting fresh.",
85
+ self.conversation_path,
86
+ e,
87
+ )
88
+
89
+ # Create a backup of the corrupted file for debugging
90
+ backup_path = self.conversation_path.with_suffix(".json.backup")
91
+ try:
92
+ shutil.copy2(self.conversation_path, backup_path)
93
+ logger.info("Backed up corrupted conversation to %s", backup_path)
94
+ except Exception as backup_error: # pragma: no cover
95
+ logger.warning("Failed to backup corrupted file: %s", backup_error)
96
+
97
+ return None
98
+
99
+ except Exception as e: # pragma: no cover
100
+ # Catch-all for unexpected errors
81
101
  logger.error(
82
- "Failed to load conversation from %s: %s", self.conversation_path, e
102
+ "Unexpected error loading conversation from %s: %s",
103
+ self.conversation_path,
104
+ e,
83
105
  )
84
106
  return None
85
107
 
@@ -1,5 +1,9 @@
1
1
  """Context extraction utilities for history processing."""
2
2
 
3
+ import json
4
+ import logging
5
+ import traceback
6
+
3
7
  from pydantic_ai.messages import (
4
8
  BuiltinToolCallPart,
5
9
  BuiltinToolReturnPart,
@@ -16,6 +20,46 @@ from pydantic_ai.messages import (
16
20
  UserPromptPart,
17
21
  )
18
22
 
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _safely_parse_tool_args(args: dict[str, object] | str | None) -> dict[str, object]:
27
+ """Safely parse tool call arguments, handling incomplete/invalid JSON.
28
+
29
+ Args:
30
+ args: Tool call arguments (dict, JSON string, or None)
31
+
32
+ Returns:
33
+ Parsed args dict, or empty dict if parsing fails
34
+ """
35
+ if args is None:
36
+ return {}
37
+
38
+ if isinstance(args, dict):
39
+ return args
40
+
41
+ if not isinstance(args, str):
42
+ return {}
43
+
44
+ try:
45
+ parsed = json.loads(args)
46
+ return parsed if isinstance(parsed, dict) else {}
47
+ except (json.JSONDecodeError, ValueError) as e:
48
+ # Only log warning if it looks like JSON (starts with { or [) - incomplete JSON
49
+ # Plain strings are valid args and shouldn't trigger warnings
50
+ stripped_args = args.strip()
51
+ if stripped_args.startswith(("{", "[")):
52
+ args_preview = args[:100] + "..." if len(args) > 100 else args
53
+ logger.warning(
54
+ "Detected incomplete/invalid JSON in tool call args during parsing",
55
+ extra={
56
+ "args_preview": args_preview,
57
+ "error": str(e),
58
+ "args_length": len(args),
59
+ },
60
+ )
61
+ return {}
62
+
19
63
 
20
64
  def extract_context_from_messages(messages: list[ModelMessage]) -> str:
21
65
  """Extract context from a list of messages for summarization."""
@@ -87,12 +131,55 @@ def extract_context_from_part(
87
131
  return f"<ASSISTANT_TEXT>\n{message_part.content}\n</ASSISTANT_TEXT>"
88
132
 
89
133
  elif isinstance(message_part, ToolCallPart):
90
- if isinstance(message_part.args, dict):
91
- args_str = ", ".join(f"{k}={repr(v)}" for k, v in message_part.args.items())
92
- tool_call_str = f"{message_part.tool_name}({args_str})"
93
- else:
94
- tool_call_str = f"{message_part.tool_name}({message_part.args})"
95
- return f"<TOOL_CALL>\n{tool_call_str}\n</TOOL_CALL>"
134
+ # Safely parse args to avoid crashes from incomplete JSON during streaming
135
+ try:
136
+ parsed_args = _safely_parse_tool_args(message_part.args)
137
+ if parsed_args:
138
+ # Successfully parsed as dict - format nicely
139
+ args_str = ", ".join(f"{k}={repr(v)}" for k, v in parsed_args.items())
140
+ tool_call_str = f"{message_part.tool_name}({args_str})"
141
+ elif isinstance(message_part.args, str) and message_part.args:
142
+ # Non-empty string that didn't parse as JSON
143
+ # Check if it looks like JSON (starts with { or [) - if so, it's incomplete
144
+ stripped_args = message_part.args.strip()
145
+ if stripped_args.startswith(("{", "[")):
146
+ # Looks like incomplete JSON - log warning and show empty parens
147
+ args_preview = (
148
+ stripped_args[:100] + "..."
149
+ if len(stripped_args) > 100
150
+ else stripped_args
151
+ )
152
+ stack_trace = "".join(traceback.format_stack())
153
+ logger.warning(
154
+ "ToolCallPart with unparseable args encountered during context extraction",
155
+ extra={
156
+ "tool_name": message_part.tool_name,
157
+ "tool_call_id": message_part.tool_call_id,
158
+ "args_preview": args_preview,
159
+ "args_type": type(message_part.args).__name__,
160
+ "stack_trace": stack_trace,
161
+ },
162
+ )
163
+ tool_call_str = f"{message_part.tool_name}()"
164
+ else:
165
+ # Plain string arg - display as-is
166
+ tool_call_str = f"{message_part.tool_name}({message_part.args})"
167
+ else:
168
+ # No args
169
+ tool_call_str = f"{message_part.tool_name}()"
170
+ return f"<TOOL_CALL>\n{tool_call_str}\n</TOOL_CALL>"
171
+ except Exception as e: # pragma: no cover - defensive catch-all
172
+ # If anything goes wrong, log full exception with stack trace
173
+ logger.error(
174
+ "Unexpected error processing ToolCallPart",
175
+ exc_info=True,
176
+ extra={
177
+ "tool_name": message_part.tool_name,
178
+ "tool_call_id": message_part.tool_call_id,
179
+ "error": str(e),
180
+ },
181
+ )
182
+ return f"<TOOL_CALL>\n{message_part.tool_name}()\n</TOOL_CALL>"
96
183
 
97
184
  elif isinstance(message_part, BuiltinToolCallPart):
98
185
  return f"<BUILTIN_TOOL_CALL>\n{message_part.tool_name}\n</BUILTIN_TOOL_CALL>"
@@ -881,6 +881,30 @@ class ChatScreen(Screen[None]):
881
881
  except asyncio.CancelledError:
882
882
  # Handle cancellation gracefully - DO NOT re-raise
883
883
  self.mount_hint("⚠️ Operation cancelled by user")
884
+ except Exception as e:
885
+ # Log with full stack trace to shotgun.log
886
+ logger.exception(
887
+ "Agent run failed",
888
+ extra={
889
+ "agent_mode": self.mode.value,
890
+ "error_type": type(e).__name__,
891
+ },
892
+ )
893
+
894
+ # Determine user-friendly message based on error type
895
+ error_name = type(e).__name__
896
+ error_message = str(e)
897
+
898
+ if "APIStatusError" in error_name and "overload" in error_message.lower():
899
+ hint = "⚠️ The AI service is temporarily overloaded. Please wait a moment and try again."
900
+ elif "APIStatusError" in error_name and "rate" in error_message.lower():
901
+ hint = "⚠️ Rate limit reached. Please wait before trying again."
902
+ elif "APIStatusError" in error_name:
903
+ hint = f"⚠️ AI service error: {error_message}"
904
+ else:
905
+ hint = f"⚠️ An error occurred: {error_message}\n\nCheck logs at ~/.shotgun-sh/logs/shotgun.log"
906
+
907
+ self.mount_hint(hint)
884
908
  finally:
885
909
  self.working = False
886
910
  self._current_worker = None
@@ -910,24 +934,41 @@ class ChatScreen(Screen[None]):
910
934
  """Load conversation from persistent storage."""
911
935
  conversation = self.conversation_manager.load()
912
936
  if conversation is None:
937
+ # Check if file existed but was corrupted (backup was created)
938
+ backup_path = self.conversation_manager.conversation_path.with_suffix(
939
+ ".json.backup"
940
+ )
941
+ if backup_path.exists():
942
+ # File was corrupted - show friendly notification
943
+ self.mount_hint(
944
+ "⚠️ Previous session was corrupted and has been backed up. Starting fresh conversation."
945
+ )
913
946
  return
914
947
 
915
- # Restore agent state
916
- agent_messages = conversation.get_agent_messages()
917
- ui_messages = conversation.get_ui_messages()
948
+ try:
949
+ # Restore agent state
950
+ agent_messages = conversation.get_agent_messages()
951
+ ui_messages = conversation.get_ui_messages()
952
+
953
+ # Create ConversationState for restoration
954
+ state = ConversationState(
955
+ agent_messages=agent_messages,
956
+ ui_messages=ui_messages,
957
+ agent_type=conversation.last_agent_model,
958
+ )
918
959
 
919
- # Create ConversationState for restoration
920
- state = ConversationState(
921
- agent_messages=agent_messages,
922
- ui_messages=ui_messages,
923
- agent_type=conversation.last_agent_model,
924
- )
960
+ self.agent_manager.restore_conversation_state(state)
925
961
 
926
- self.agent_manager.restore_conversation_state(state)
962
+ # Update the current mode
963
+ self.mode = AgentType(conversation.last_agent_model)
964
+ self.deps.usage_manager.restore_usage_state()
927
965
 
928
- # Update the current mode
929
- self.mode = AgentType(conversation.last_agent_model)
930
- self.deps.usage_manager.restore_usage_state()
966
+ except Exception as e: # pragma: no cover
967
+ # If anything goes wrong during restoration, log it and continue
968
+ logger.error("Failed to restore conversation state: %s", e)
969
+ self.mount_hint(
970
+ "⚠️ Could not restore previous session. Starting fresh conversation."
971
+ )
931
972
 
932
973
 
933
974
  def help_text_with_codebase(already_indexed: bool = False) -> str:
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: shotgun-sh
3
+ Version: 0.2.7.dev1
4
+ Summary: AI-powered research, planning, and task management CLI tool
5
+ Project-URL: Homepage, https://shotgun.sh/
6
+ Project-URL: Repository, https://github.com/shotgun-sh/shotgun
7
+ Project-URL: Issues, https://github.com/shotgun-sh/shotgun-alpha/issues
8
+ Project-URL: Discord, https://discord.gg/5RmY6J2N7s
9
+ Author-email: "Proofs.io" <hello@proofs.io>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,ai,cli,llm,planning,productivity,pydantic-ai,research,task-management
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: anthropic>=0.39.0
25
+ Requires-Dist: genai-prices>=0.0.27
26
+ Requires-Dist: httpx>=0.27.0
27
+ Requires-Dist: jinja2>=3.1.0
28
+ Requires-Dist: kuzu>=0.7.0
29
+ Requires-Dist: logfire[pydantic-ai]>=2.0.0
30
+ Requires-Dist: openai>=1.0.0
31
+ Requires-Dist: packaging>=23.0
32
+ Requires-Dist: posthog>=3.0.0
33
+ Requires-Dist: pydantic-ai>=0.0.14
34
+ Requires-Dist: rich>=13.0.0
35
+ Requires-Dist: sentencepiece>=0.2.0
36
+ Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
37
+ Requires-Dist: textual-dev>=1.7.0
38
+ Requires-Dist: textual>=6.1.0
39
+ Requires-Dist: tiktoken>=0.7.0
40
+ Requires-Dist: tree-sitter-go>=0.23.0
41
+ Requires-Dist: tree-sitter-javascript>=0.23.0
42
+ Requires-Dist: tree-sitter-python>=0.23.0
43
+ Requires-Dist: tree-sitter-rust>=0.23.0
44
+ Requires-Dist: tree-sitter-typescript>=0.23.0
45
+ Requires-Dist: tree-sitter>=0.21.0
46
+ Requires-Dist: typer>=0.12.0
47
+ Requires-Dist: watchdog>=4.0.0
48
+ Provides-Extra: dev
49
+ Requires-Dist: commitizen>=3.13.0; extra == 'dev'
50
+ Requires-Dist: lefthook>=1.12.0; extra == 'dev'
51
+ Requires-Dist: mypy>=1.11.0; extra == 'dev'
52
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # Shotgun
56
+
57
+ **Spec-Driven Development for AI Code Generation**
58
+
59
+ Shotgun is a CLI tool that turns work with AI code-gen tools from "I want to build X" into: **research → specs → plans → tasks → implementation**. It reads your entire codebase, coordinates AI agents to do the heavy lifting, and exports clean artifacts in the agents.md format so your code-gen tools actually know what they're building.
60
+
61
+ 🌐 **Learn more at [shotgun.sh](https://shotgun.sh/)**
62
+
63
+ ## Features
64
+
65
+ ### 📊 Complete Codebase Understanding
66
+
67
+ Before writing a single line, Shotgun reads all of it. Your patterns. Your dependencies. Your technical debt. Whether you're adding features, onboarding devs, planning migrations, or refactoring - Shotgun knows what you're working with.
68
+
69
+ ### 🔄 Five Modes. One Journey. Zero Gaps.
70
+
71
+ **Research** (what exists) → **Specify** (what to build) → **Plan** (how to build) → **Tasks** (break it down) → **Export** (to any tool)
72
+
73
+ Not another chatbot. A complete workflow where each mode feeds the next.
74
+
75
+ ### ➡️ Export to agents.md
76
+
77
+ Outputs plug into many code-generation tools including Codex, Cursor, Warp, Devin, opencode, Jules, and more.
78
+
79
+ ### 📝 Specs That Don't Die in Slack
80
+
81
+ Every research finding, every architectural decision, every "here's why we didn't use that library" - captured as markdown in your repo. Version controlled. Searchable.
82
+
83
+ ## Installation
84
+
85
+ ### Using pipx (Recommended)
86
+
87
+ ```bash
88
+ pipx install shotgun-sh
89
+ ```
90
+
91
+ **Why pipx?** It installs Shotgun in an isolated environment, preventing dependency conflicts with your other Python projects.
92
+
93
+ ### Using pip
94
+
95
+ ```bash
96
+ pip install shotgun-sh
97
+ ```
98
+
99
+ ## Quick Start
100
+
101
+ ```bash
102
+ # Research your codebase or a topic
103
+ shotgun research "What is our authentication flow?"
104
+
105
+ # Generate specifications
106
+ shotgun spec "Add OAuth2 authentication"
107
+
108
+ # Create an implementation plan
109
+ shotgun plan "Build user dashboard"
110
+
111
+ # Break down into tasks
112
+ shotgun tasks "Implement payment system"
113
+
114
+ # Export to agents.md format for your code-gen tools
115
+ shotgun export
116
+ ```
117
+
118
+ ## Support
119
+
120
+ Have questions? Join our community on **[Discord](https://discord.gg/5RmY6J2N7s)**
121
+
122
+ ---
123
+
124
+ **License:** MIT
125
+ **Python:** 3.11+
@@ -8,10 +8,10 @@ shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  shotgun/sentry_telemetry.py,sha256=VD8es-tREfgtRKhDsEVvqpo0_kM_ab6iVm2lkOEmTlI,2950
9
9
  shotgun/telemetry.py,sha256=C8dM7Feo1OxJMwDvgAMaA2RyRDO2rYPvC_8kLBuRUS8,3683
10
10
  shotgun/agents/__init__.py,sha256=8Jzv1YsDuLyNPFJyckSr_qI4ehTVeDyIMDW4omsfPGc,25
11
- shotgun/agents/agent_manager.py,sha256=6f86XooirrBRxyfTWRwsJk6M_kKaaoAP4Yfi6Lu7zGc,28995
11
+ shotgun/agents/agent_manager.py,sha256=Fjm_7fDqmaqWgjAgGiiIdMNnK0ahanLLZ4bxJIbrMlE,32611
12
12
  shotgun/agents/common.py,sha256=g8QW782XZfZHjAJPC6k2uXqoX4UZufvbGaejzWgya8E,17768
13
- shotgun/agents/conversation_history.py,sha256=5J8_1yxdZiiWTq22aDio88DkBDZ4_Lh_p5Iy5_ENszc,3898
14
- shotgun/agents/conversation_manager.py,sha256=fxAvXbEl3Cl2ugJ4N9aWXaqZtkrnfj3QzwjWC4LFXwI,3514
13
+ shotgun/agents/conversation_history.py,sha256=c-1PBaG9FXPfSmekWtrD6s6YVT0sd98DdDhzS4a1Hyo,7859
14
+ shotgun/agents/conversation_manager.py,sha256=X3DWZZIqrv0hS1SUasyoxexnLPBUrZMBg4ZmRAipkBE,4429
15
15
  shotgun/agents/export.py,sha256=7ZNw7781WJ4peLSeoUc7zxKeaZVxFpow2y4nU4dCfbo,2919
16
16
  shotgun/agents/llm.py,sha256=hs8j1wwTczGtehzahL1Z_5D4qus5QUx4-h9-m5ZPzm4,2209
17
17
  shotgun/agents/messages.py,sha256=wNn0qC5AqASM8LMaSGFOerZEJPn5FsIOmaJs1bdosuU,1036
@@ -29,7 +29,7 @@ shotgun/agents/config/provider.py,sha256=K2uW7DkdAhZfoDJcWV7NxOrSoEq1rQzArtZnxIg
29
29
  shotgun/agents/history/__init__.py,sha256=XFQj2a6fxDqVg0Q3juvN9RjV_RJbgvFZtQOCOjVJyp4,147
30
30
  shotgun/agents/history/compaction.py,sha256=9RMpG0aY_7L4TecbgwHSOkGtbd9W5XZTg-MbzZmNl00,3515
31
31
  shotgun/agents/history/constants.py,sha256=yWY8rrTZarLA3flCCMB_hS2NMvUDRDTwP4D4j7MIh1w,446
32
- shotgun/agents/history/context_extraction.py,sha256=yVka1U6TqNVsORR4JlxpWi9yBt3Quip8g_u3x2Vi9Gs,3564
32
+ shotgun/agents/history/context_extraction.py,sha256=yPF3oYpv5GFsFQT5y53ORKdADtrkGH4u8LwPdO0YVzU,7157
33
33
  shotgun/agents/history/history_building.py,sha256=6LFDZ60MTPDoGAcmu_mjlnjVYu8YYWdIi-cGbF3jm7A,3532
34
34
  shotgun/agents/history/history_processors.py,sha256=D3z-hzrXHxE7OAZaVX4_YAKN_nyxSF5iYMIYO24V_CI,17943
35
35
  shotgun/agents/history/message_utils.py,sha256=aPusAl2RYKbjc7lBxPaNprRHmZEG6fe97q7DQUlhlzU,2918
@@ -127,7 +127,7 @@ shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4Ugad
127
127
  shotgun/tui/components/spinner.py,sha256=ovTDeaJ6FD6chZx_Aepia6R3UkPOVJ77EKHfRmn39MY,2427
128
128
  shotgun/tui/components/splash.py,sha256=vppy9vEIEvywuUKRXn2y11HwXSRkQZHLYoVjhDVdJeU,1267
129
129
  shotgun/tui/components/vertical_tail.py,sha256=kROwTaRjUwVB7H35dtmNcUVPQqNYvvfq7K2tXBKEb6c,638
130
- shotgun/tui/screens/chat.py,sha256=78GaZMKN2iE7bQQwiKBWFTsRoraWnuWl91qPjo5q1Nw,35446
130
+ shotgun/tui/screens/chat.py,sha256=VAsgX7YiXTXAH7pAwcWaWr2hGu_Ft0i8VpoUETTPsTM,37424
131
131
  shotgun/tui/screens/chat.tcss,sha256=2Yq3E23jxsySYsgZf4G1AYrYVcpX0UDW6kNNI0tDmtM,437
132
132
  shotgun/tui/screens/directory_setup.py,sha256=lIZ1J4A6g5Q2ZBX8epW7BhR96Dmdcg22CyiM5S-I5WU,3237
133
133
  shotgun/tui/screens/feedback.py,sha256=VxpW0PVxMp22ZvSfQkTtgixNrpEOlfWtekjqlVfYEjA,5708
@@ -148,8 +148,8 @@ shotgun/utils/env_utils.py,sha256=ulM3BRi9ZhS7uC-zorGeDQm4SHvsyFuuU9BtVPqdrHY,14
148
148
  shotgun/utils/file_system_utils.py,sha256=l-0p1bEHF34OU19MahnRFdClHufThfGAjQ431teAIp0,1004
149
149
  shotgun/utils/source_detection.py,sha256=Co6Q03R3fT771TF3RzB-70stfjNP2S4F_ArZKibwzm8,454
150
150
  shotgun/utils/update_checker.py,sha256=IgzPHRhS1ETH7PnJR_dIx6lxgr1qHpCkMTgzUxvGjhI,7586
151
- shotgun_sh-0.2.6.dev5.dist-info/METADATA,sha256=XyTZmnYtmdUKWPMWrXAzzUv9HZeIZuGjXf2GvpzHY2k,11226
152
- shotgun_sh-0.2.6.dev5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
153
- shotgun_sh-0.2.6.dev5.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
154
- shotgun_sh-0.2.6.dev5.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
155
- shotgun_sh-0.2.6.dev5.dist-info/RECORD,,
151
+ shotgun_sh-0.2.7.dev1.dist-info/METADATA,sha256=MEaQlu9HJSSTi9_WuCgVgC8nX5J34O8PzyP8b8mCj_4,4268
152
+ shotgun_sh-0.2.7.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
153
+ shotgun_sh-0.2.7.dev1.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
154
+ shotgun_sh-0.2.7.dev1.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
155
+ shotgun_sh-0.2.7.dev1.dist-info/RECORD,,
@@ -1,467 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: shotgun-sh
3
- Version: 0.2.6.dev5
4
- Summary: AI-powered research, planning, and task management CLI tool
5
- Project-URL: Homepage, https://shotgun.sh/
6
- Project-URL: Repository, https://github.com/shotgun-sh/shotgun
7
- Project-URL: Issues, https://github.com/shotgun-sh/shotgun-alpha/issues
8
- Project-URL: Discord, https://discord.gg/5RmY6J2N7s
9
- Author-email: "Proofs.io" <hello@proofs.io>
10
- License: MIT
11
- License-File: LICENSE
12
- Keywords: agent,ai,cli,llm,planning,productivity,pydantic-ai,research,task-management
13
- Classifier: Development Status :: 3 - Alpha
14
- Classifier: Environment :: Console
15
- Classifier: Intended Audience :: Developers
16
- Classifier: License :: OSI Approved :: MIT License
17
- Classifier: Operating System :: OS Independent
18
- Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
- Classifier: Topic :: Utilities
23
- Requires-Python: >=3.11
24
- Requires-Dist: anthropic>=0.39.0
25
- Requires-Dist: genai-prices>=0.0.27
26
- Requires-Dist: httpx>=0.27.0
27
- Requires-Dist: jinja2>=3.1.0
28
- Requires-Dist: kuzu>=0.7.0
29
- Requires-Dist: logfire[pydantic-ai]>=2.0.0
30
- Requires-Dist: openai>=1.0.0
31
- Requires-Dist: packaging>=23.0
32
- Requires-Dist: posthog>=3.0.0
33
- Requires-Dist: pydantic-ai>=0.0.14
34
- Requires-Dist: rich>=13.0.0
35
- Requires-Dist: sentencepiece>=0.2.0
36
- Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
37
- Requires-Dist: textual-dev>=1.7.0
38
- Requires-Dist: textual>=6.1.0
39
- Requires-Dist: tiktoken>=0.7.0
40
- Requires-Dist: tree-sitter-go>=0.23.0
41
- Requires-Dist: tree-sitter-javascript>=0.23.0
42
- Requires-Dist: tree-sitter-python>=0.23.0
43
- Requires-Dist: tree-sitter-rust>=0.23.0
44
- Requires-Dist: tree-sitter-typescript>=0.23.0
45
- Requires-Dist: tree-sitter>=0.21.0
46
- Requires-Dist: typer>=0.12.0
47
- Requires-Dist: watchdog>=4.0.0
48
- Provides-Extra: dev
49
- Requires-Dist: commitizen>=3.13.0; extra == 'dev'
50
- Requires-Dist: lefthook>=1.12.0; extra == 'dev'
51
- Requires-Dist: mypy>=1.11.0; extra == 'dev'
52
- Requires-Dist: ruff>=0.6.0; extra == 'dev'
53
- Description-Content-Type: text/markdown
54
-
55
- # Shotgun
56
-
57
- A Python CLI tool for research, planning, and task management powered by AI agents.
58
-
59
- ## Features
60
-
61
- - **Research**: Perform research with agentic loops
62
- - **Planning**: Generate structured plans for achieving goals
63
- - **Tasks**: Generate prioritized task lists with agentic approaches
64
-
65
- ## Installation
66
-
67
- ### From PyPI (Recommended)
68
-
69
- ```bash
70
- pip install shotgun-sh
71
- ```
72
-
73
- ### From Source
74
-
75
- ```bash
76
- git clone https://github.com/shotgun-sh/shotgun.git
77
- cd shotgun
78
- uv sync --all-extras
79
- ```
80
-
81
- After installation from source, you can use either method:
82
-
83
- **Method 1: Direct command (after uv sync)**
84
- ```bash
85
- shotgun --help
86
- ```
87
-
88
- **Method 2: Via uv run**
89
- ```bash
90
- uv run shotgun --help
91
- ```
92
-
93
- If installed from PyPI, simply use:
94
- ```bash
95
- shotgun --help
96
- ```
97
-
98
- ### Virtual Environment Setup (Optional)
99
-
100
- If you prefer using a local virtual environment:
101
-
102
- ```bash
103
- uv venv
104
- source .venv/bin/activate # On Windows: .venv\Scripts\activate
105
- uv sync --all-extras
106
- shotgun --help
107
- ```
108
-
109
- ## Usage
110
-
111
- ### Using Direct Commands (after uv sync)
112
-
113
- ```bash
114
- # Research a topic
115
- shotgun research "What is quantum computing?"
116
-
117
- # Generate a plan
118
- shotgun plan "Build a web application"
119
- shotgun plan "build me a house"
120
-
121
- # Generate tasks for a project
122
- shotgun tasks "Create a machine learning model"
123
- ```
124
-
125
- ### Using uv run
126
-
127
- ```bash
128
- # Research a topic
129
- uv run shotgun research "What is quantum computing?"
130
-
131
- # Generate a plan
132
- uv run shotgun plan "Build a web application"
133
-
134
- # Generate tasks for a project
135
- uv run shotgun tasks "Create a machine learning model"
136
- ```
137
-
138
- ## Auto-Updates
139
-
140
- Shotgun automatically checks for updates to keep you on the latest version.
141
-
142
- ### How it works
143
-
144
- - Checks for updates on startup (runs in background, non-blocking)
145
- - Caches results for 24 hours to minimize API calls
146
- - Shows notification after command execution if an update is available
147
- - Never auto-updates development versions
148
-
149
- ### Update Commands
150
-
151
- ```bash
152
- # Check for available updates
153
- shotgun update --check
154
-
155
- # Install available updates
156
- shotgun update
157
-
158
- # Force update (even for dev versions with confirmation)
159
- shotgun update --force
160
- ```
161
-
162
- ### Disable Update Checks
163
-
164
- ```bash
165
- # Disable for a single command
166
- shotgun --no-update-check research "topic"
167
- ```
168
-
169
- ### Installation Methods
170
-
171
- The update command automatically detects and uses the appropriate method:
172
- - **pipx**: `pipx upgrade shotgun-sh`
173
- - **pip**: `pip install --upgrade shotgun-sh`
174
- - **venv**: Updates within the virtual environment
175
-
176
- ## Development Setup
177
-
178
- ### Requirements
179
-
180
- - **Python 3.11+** (3.13 recommended)
181
- - **uv** - Fast Python package installer and resolver
182
- - **actionlint** (optional) - For GitHub Actions workflow validation
183
-
184
- ### Quick Start
185
-
186
- 1. **Clone and setup**:
187
- ```bash
188
- git clone https://github.com/shotgun-sh/shotgun.git
189
- cd shotgun
190
- ```
191
-
192
- 2. **Install uv** (if not already installed):
193
- ```bash
194
- # macOS/Linux
195
- curl -LsSf https://astral.sh/uv/install.sh | sh
196
-
197
- # Or via brew
198
- brew install uv
199
- ```
200
-
201
- 3. **Install dependencies**:
202
- ```bash
203
- uv sync --all-extras
204
- ```
205
-
206
- 4. **Install git hooks**:
207
- ```bash
208
- uv run lefthook install
209
- ```
210
-
211
- 5. **Verify setup**:
212
- ```bash
213
- uv run shotgun --version
214
- ```
215
-
216
- ### Development Commands
217
-
218
- ```bash
219
- # Run the CLI
220
- uv run shotgun --help
221
-
222
- # Run the TUI
223
- uv run tui
224
-
225
- # Run tests
226
- uv run pytest
227
-
228
- # Run tests with coverage
229
- uv run pytest --cov=src --cov-report=term-missing --cov-report=html
230
-
231
- # Run linting
232
- uv run ruff check .
233
-
234
- # Run formatting
235
- uv run ruff format .
236
-
237
- # Run type checking
238
- uv run mypy src/
239
-
240
- # Run all pre-commit hooks manually
241
- uv run lefthook run pre-commit
242
- ```
243
-
244
- ### Code Coverage
245
-
246
- To analyze test coverage and identify areas that need testing:
247
-
248
- ```bash
249
- # Run tests with coverage analysis
250
- uv run pytest --cov=src --cov-report=term-missing --cov-report=html
251
- ```
252
-
253
- This will:
254
- - Display coverage summary in the terminal
255
- - Generate a detailed HTML coverage report
256
-
257
- **Viewing the coverage report:**
258
- Open `htmlcov/index.html` in your browser to see:
259
- - Overall coverage percentage
260
- - File-by-file coverage breakdown
261
- - Line-by-line coverage highlighting
262
- - Missing coverage areas
263
-
264
- The coverage configuration is in `pyproject.toml` and will automatically run when you use `uv run pytest`.
265
-
266
- ### Git Hooks (Lefthook)
267
-
268
- This project uses [lefthook](https://github.com/evilmartians/lefthook) for git hooks. The hooks automatically run:
269
-
270
- - **ruff** - Python linting with auto-fix
271
- - **ruff-format** - Code formatting
272
- - **mypy** - Type checking
273
- - **commitizen** - Commit message validation
274
- - **actionlint** - GitHub Actions workflow validation (if installed)
275
-
276
- #### Installing actionlint (recommended)
277
-
278
- ```bash
279
- # macOS
280
- brew install actionlint
281
-
282
- # Linux/macOS (direct download)
283
- curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash
284
-
285
- # Go install
286
- go install github.com/rhysd/actionlint/cmd/actionlint@latest
287
- ```
288
-
289
-
290
- ### Python Version Management
291
-
292
- The project supports **Python 3.11+**. The `.python-version` file specifies Python 3.11 to ensure development against the minimum supported version.
293
-
294
- If using **pyenv**:
295
- ```bash
296
- pyenv install 3.11
297
- ```
298
-
299
- If using **uv** (recommended):
300
- ```bash
301
- uv python install 3.11
302
- uv sync --python 3.11
303
- ```
304
-
305
- ### Commit Message Convention
306
-
307
- This project enforces **Conventional Commits** specification. All commit messages must follow this format:
308
-
309
- ```
310
- <type>[optional scope]: <description>
311
- ```
312
-
313
- **Required commit types:**
314
- - `feat`: New feature
315
- - `fix`: Bug fix
316
- - `docs`: Documentation changes
317
- - `style`: Code formatting changes
318
- - `refactor`: Code restructuring without feature changes
319
- - `perf`: Performance improvements
320
- - `test`: Adding or updating tests
321
- - `build`: Build system changes
322
- - `ci`: CI/CD changes
323
- - `chore`: Maintenance tasks
324
- - `revert`: Reverting previous commits
325
-
326
- **Examples:**
327
- ```bash
328
- feat: add user authentication system
329
- fix: resolve memory leak in data processing
330
- docs: update API documentation
331
- refactor: simplify user validation logic
332
- ```
333
-
334
- **For interactive commit creation:**
335
- ```bash
336
- uv run cz commit
337
- ```
338
-
339
- ### Contributing
340
-
341
- 1. Fork the repository
342
- 2. Create a feature branch: `git checkout -b feat/feature-name`
343
- 3. Make your changes
344
- 4. Run the pre-commit hooks: `uv run lefthook run pre-commit`
345
- 5. Commit with conventional format: `git commit -m "feat: add new feature"`
346
- 6. Push to your fork: `git push origin feat/feature-name`
347
- 7. Create a Pull Request with conventional title format
348
-
349
- ### CI/CD
350
-
351
- GitHub Actions automatically:
352
- - Runs on pull requests and pushes to main
353
- - Tests with Python 3.11
354
- - Validates code with ruff, ruff-format, and mypy
355
- - Ensures all checks pass before merge
356
-
357
- ## Observability & Telemetry
358
-
359
- Shotgun includes built-in observability with Sentry for error tracking and Logfire for logging and tracing. Both services track users anonymously using a UUID generated on first run.
360
-
361
- ### Anonymous User Tracking
362
-
363
- Each user gets a unique anonymous ID stored in their config:
364
- ```bash
365
- # Get your anonymous user ID
366
- shotgun config get-user-id
367
- ```
368
-
369
- This ID is automatically included in:
370
- - **Sentry**: Error reports and exceptions
371
- - **Logfire**: All logs, traces, and spans
372
-
373
- ### Logfire Queries
374
-
375
- Logfire uses SQL for querying logs. Here are helpful queries for debugging and analysis:
376
-
377
- #### Find all logs for a specific user
378
- ```sql
379
- SELECT * FROM records
380
- WHERE attributes->>'user_id' = 'your-user-id-here'
381
- ORDER BY timestamp DESC;
382
- ```
383
-
384
- #### Track user actions
385
- ```sql
386
- SELECT
387
- timestamp,
388
- span_name,
389
- message,
390
- attributes
391
- FROM records
392
- WHERE attributes->>'user_id' = 'your-user-id-here'
393
- AND span_name LIKE '%research%'
394
- ORDER BY timestamp DESC;
395
- ```
396
-
397
- #### Find slow operations for a user
398
- ```sql
399
- SELECT
400
- span_name,
401
- duration_ms,
402
- attributes
403
- FROM records
404
- WHERE attributes->>'user_id' = 'your-user-id-here'
405
- AND duration_ms > 1000
406
- ORDER BY duration_ms DESC;
407
- ```
408
-
409
- #### Find errors for a user
410
- ```sql
411
- SELECT * FROM records
412
- WHERE attributes->>'user_id' = 'your-user-id-here'
413
- AND level = 'error'
414
- ORDER BY timestamp DESC;
415
- ```
416
-
417
- #### Analyze user's AI provider usage
418
- ```sql
419
- SELECT
420
- attributes->>'provider' as provider,
421
- COUNT(*) as usage_count,
422
- AVG(duration_ms) as avg_duration
423
- FROM records
424
- WHERE attributes->>'user_id' = 'your-user-id-here'
425
- AND attributes->>'provider' IS NOT NULL
426
- GROUP BY provider;
427
- ```
428
-
429
- #### Track feature usage by user
430
- ```sql
431
- SELECT
432
- span_name,
433
- COUNT(*) as usage_count
434
- FROM records
435
- WHERE attributes->>'user_id' = 'your-user-id-here'
436
- AND span_name IN ('research', 'plan', 'tasks')
437
- GROUP BY span_name
438
- ORDER BY usage_count DESC;
439
- ```
440
-
441
- ### Setting Up Observability (Optional)
442
-
443
- For local development with Logfire:
444
- ```bash
445
- # Set environment variables
446
- export LOGFIRE_ENABLED=true
447
- export LOGFIRE_TOKEN=your-logfire-token
448
-
449
- # Run shotgun - will now send logs to Logfire
450
- shotgun research "topic"
451
- ```
452
-
453
- For Sentry (automatically configured in production builds):
454
- ```bash
455
- # Set for local development
456
- export SENTRY_DSN=your-sentry-dsn
457
- ```
458
-
459
- ### Privacy
460
-
461
- - **No PII collected**: Only anonymous UUIDs are used for identification
462
- - **Opt-in for development**: Telemetry requires explicit environment variables
463
- - **Automatic in production**: Production builds include telemetry for error tracking
464
-
465
- ## Support
466
-
467
- Join our discord https://discord.gg/5RmY6J2N7s