uncaughtdev 0.1.0__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.
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ dist/
3
+ .turbo/
4
+ .uncaught/
5
+ *.tsbuildinfo
6
+ .env
7
+ .env.local
8
+ .next/
9
+ .DS_Store
10
+ .claude/
11
+ __pycache__/
12
+ *.pyc
13
+ target/
14
+ *.class
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: uncaughtdev
3
+ Version: 0.1.0
4
+ Summary: Local-first, AI-ready error monitoring for Python — catch bugs, get AI-powered fixes
5
+ Project-URL: Homepage, https://github.com/ajeeshworkspace/uncaught
6
+ Project-URL: Documentation, https://github.com/ajeeshworkspace/uncaught
7
+ Project-URL: Repository, https://github.com/ajeeshworkspace/uncaught
8
+ Author: Uncaught Dev
9
+ License-Expression: MIT
10
+ Keywords: ai,debugging,error-monitoring,mcp,vibe-coding
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Framework :: Flask
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Debuggers
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: all
26
+ Requires-Dist: django>=3.2; extra == 'all'
27
+ Requires-Dist: fastapi>=0.68.0; extra == 'all'
28
+ Requires-Dist: flask>=2.0.0; extra == 'all'
29
+ Requires-Dist: sqlalchemy>=1.4.0; extra == 'all'
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
32
+ Requires-Dist: pytest>=7.0; extra == 'dev'
33
+ Provides-Extra: django
34
+ Requires-Dist: django>=3.2; extra == 'django'
35
+ Provides-Extra: fastapi
36
+ Requires-Dist: fastapi>=0.68.0; extra == 'fastapi'
37
+ Provides-Extra: flask
38
+ Requires-Dist: flask>=2.0.0; extra == 'flask'
39
+ Provides-Extra: sqlalchemy
40
+ Requires-Dist: sqlalchemy>=1.4.0; extra == 'sqlalchemy'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # uncaught
44
+
45
+ Local-first, AI-ready error monitoring for Python. Zero-config error capture with automatic fix prompts.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install uncaught
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```python
56
+ from uncaught import init_uncaught
57
+
58
+ client = init_uncaught({
59
+ "environment": "development",
60
+ })
61
+
62
+ # Errors are automatically captured via sys.excepthook
63
+ # Or capture manually:
64
+ try:
65
+ risky_operation()
66
+ except Exception as e:
67
+ client.capture_error(e)
68
+ ```
69
+
70
+ ## Framework Integrations
71
+
72
+ ### FastAPI
73
+
74
+ ```python
75
+ from fastapi import FastAPI
76
+ from uncaught.integrations.fastapi import UncaughtMiddleware
77
+
78
+ app = FastAPI()
79
+ app.add_middleware(UncaughtMiddleware)
80
+ ```
81
+
82
+ ### Flask
83
+
84
+ ```python
85
+ from flask import Flask
86
+ from uncaught.integrations.flask import init_app
87
+
88
+ app = Flask(__name__)
89
+ init_app(app)
90
+ ```
91
+
92
+ ### Django
93
+
94
+ Add to `MIDDLEWARE` in `settings.py`:
95
+
96
+ ```python
97
+ MIDDLEWARE = [
98
+ "uncaught.integrations.django.UncaughtMiddleware",
99
+ # ...
100
+ ]
101
+ ```
102
+
103
+ ### SQLAlchemy
104
+
105
+ ```python
106
+ from uncaught.integrations.sqlalchemy import setup_sqlalchemy
107
+
108
+ setup_sqlalchemy(engine)
109
+ ```
110
+
111
+ ## How It Works
112
+
113
+ Errors are written to a local `.uncaught/` directory with AI-ready fix prompts. Use the MCP server to query errors from Claude, Cursor, or any MCP-compatible AI tool.
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,75 @@
1
+ # uncaught
2
+
3
+ Local-first, AI-ready error monitoring for Python. Zero-config error capture with automatic fix prompts.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install uncaught
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from uncaught import init_uncaught
15
+
16
+ client = init_uncaught({
17
+ "environment": "development",
18
+ })
19
+
20
+ # Errors are automatically captured via sys.excepthook
21
+ # Or capture manually:
22
+ try:
23
+ risky_operation()
24
+ except Exception as e:
25
+ client.capture_error(e)
26
+ ```
27
+
28
+ ## Framework Integrations
29
+
30
+ ### FastAPI
31
+
32
+ ```python
33
+ from fastapi import FastAPI
34
+ from uncaught.integrations.fastapi import UncaughtMiddleware
35
+
36
+ app = FastAPI()
37
+ app.add_middleware(UncaughtMiddleware)
38
+ ```
39
+
40
+ ### Flask
41
+
42
+ ```python
43
+ from flask import Flask
44
+ from uncaught.integrations.flask import init_app
45
+
46
+ app = Flask(__name__)
47
+ init_app(app)
48
+ ```
49
+
50
+ ### Django
51
+
52
+ Add to `MIDDLEWARE` in `settings.py`:
53
+
54
+ ```python
55
+ MIDDLEWARE = [
56
+ "uncaught.integrations.django.UncaughtMiddleware",
57
+ # ...
58
+ ]
59
+ ```
60
+
61
+ ### SQLAlchemy
62
+
63
+ ```python
64
+ from uncaught.integrations.sqlalchemy import setup_sqlalchemy
65
+
66
+ setup_sqlalchemy(engine)
67
+ ```
68
+
69
+ ## How It Works
70
+
71
+ Errors are written to a local `.uncaught/` directory with AI-ready fix prompts. Use the MCP server to query errors from Claude, Cursor, or any MCP-compatible AI tool.
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "uncaughtdev"
7
+ version = "0.1.0"
8
+ description = "Local-first, AI-ready error monitoring for Python — catch bugs, get AI-powered fixes"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Uncaught Dev" }]
13
+ keywords = ["error-monitoring", "debugging", "ai", "mcp", "vibe-coding"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Framework :: FastAPI",
17
+ "Framework :: Flask",
18
+ "Framework :: Django",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Debuggers",
28
+ ]
29
+ dependencies = []
30
+
31
+ [project.optional-dependencies]
32
+ fastapi = ["fastapi>=0.68.0"]
33
+ flask = ["flask>=2.0.0"]
34
+ django = ["django>=3.2"]
35
+ sqlalchemy = ["sqlalchemy>=1.4.0"]
36
+ all = ["fastapi>=0.68.0", "flask>=2.0.0", "django>=3.2", "sqlalchemy>=1.4.0"]
37
+ dev = ["pytest>=7.0", "pytest-asyncio>=0.21.0"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/ajeeshworkspace/uncaught"
41
+ Documentation = "https://github.com/ajeeshworkspace/uncaught"
42
+ Repository = "https://github.com/ajeeshworkspace/uncaught"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/uncaught"]
@@ -0,0 +1,14 @@
1
+ """Uncaught — local-first, AI-ready error monitoring for Python."""
2
+
3
+ from uncaught.client import UncaughtClient, init_uncaught, get_client
4
+ from uncaught.types import UncaughtConfig, SeverityLevel
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "UncaughtClient",
9
+ "init_uncaught",
10
+ "get_client",
11
+ "UncaughtConfig",
12
+ "SeverityLevel",
13
+ "__version__",
14
+ ]
@@ -0,0 +1,43 @@
1
+ """Ring-buffer breadcrumb store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from collections import deque
7
+
8
+ from uncaught.types import Breadcrumb
9
+ from uncaught.utils import iso_timestamp
10
+
11
+
12
+ class BreadcrumbStore:
13
+ """Thread-safe ring buffer for breadcrumbs."""
14
+
15
+ def __init__(self, max_breadcrumbs: int = 20) -> None:
16
+ self._buffer: deque[Breadcrumb] = deque(maxlen=max_breadcrumbs)
17
+
18
+ def add(self, crumb: dict) -> None:
19
+ """Append a breadcrumb, auto-adding a timestamp if missing."""
20
+ entry: Breadcrumb = {
21
+ "type": crumb.get("type", "custom"),
22
+ "category": crumb.get("category", ""),
23
+ "message": crumb.get("message", ""),
24
+ "timestamp": crumb.get("timestamp", iso_timestamp()),
25
+ }
26
+ if crumb.get("data"):
27
+ entry["data"] = crumb["data"]
28
+ if crumb.get("level"):
29
+ entry["level"] = crumb["level"]
30
+ self._buffer.append(entry)
31
+
32
+ def get_all(self) -> list[Breadcrumb]:
33
+ """Return all stored breadcrumbs in chronological order (copies)."""
34
+ return [copy.deepcopy(b) for b in self._buffer]
35
+
36
+ def get_last(self, n: int) -> list[Breadcrumb]:
37
+ """Return the most recent n breadcrumbs (copies)."""
38
+ items = list(self._buffer)[-n:]
39
+ return [copy.deepcopy(b) for b in items]
40
+
41
+ def clear(self) -> None:
42
+ """Empty the buffer."""
43
+ self._buffer.clear()
@@ -0,0 +1,245 @@
1
+ """Core Uncaught client — the main error capture pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sys
7
+ import traceback
8
+ from typing import Any
9
+
10
+ from uncaught.breadcrumbs import BreadcrumbStore
11
+ from uncaught.env_detector import detect_environment
12
+ from uncaught.fingerprint import generate_fingerprint
13
+ from uncaught.prompt_builder import build_fix_prompt
14
+ from uncaught.rate_limiter import RateLimiter
15
+ from uncaught.sanitizer import sanitize
16
+ from uncaught.transport import ConsoleTransport, LocalFileTransport
17
+ from uncaught.types import Breadcrumb, UncaughtConfig, UncaughtEvent
18
+ from uncaught.utils import generate_uuid, iso_timestamp
19
+
20
+ # Global singleton
21
+ _client: UncaughtClient | None = None
22
+
23
+ SDK_NAME = "uncaught-python"
24
+ SDK_VERSION = "0.1.0"
25
+
26
+
27
+ class UncaughtClient:
28
+ """The main Uncaught client that captures, processes, and stores errors."""
29
+
30
+ def __init__(self, config: UncaughtConfig | None = None) -> None:
31
+ config = config or {}
32
+ self._config = config
33
+ self._enabled = config.get("enabled", True)
34
+ self._debug = config.get("debug", False)
35
+ self._environment = config.get("environment")
36
+ self._release = config.get("release")
37
+ self._before_send = config.get("before_send")
38
+ self._sanitize_keys = config.get("sanitize_keys", [])
39
+ self._ignore_errors = config.get("ignore_errors", [])
40
+ self._webhook_url = config.get("webhook_url")
41
+
42
+ self._breadcrumbs = BreadcrumbStore(config.get("max_breadcrumbs", 20))
43
+ self._rate_limiter = RateLimiter(config.get("max_events_per_minute", 30))
44
+ self._seen_fingerprints: set[str] = set()
45
+
46
+ # Setup transport
47
+ transport_mode = config.get("transport", "local")
48
+ if transport_mode == "console":
49
+ self._transport = ConsoleTransport()
50
+ else:
51
+ self._transport = LocalFileTransport(config.get("local_output_dir"))
52
+
53
+ # Set user context
54
+ self._user: dict[str, Any] = {}
55
+
56
+ def capture_error(
57
+ self,
58
+ error: BaseException | str | dict | Any,
59
+ *,
60
+ level: str = "error",
61
+ request: dict | None = None,
62
+ operation: dict | None = None,
63
+ user: dict | None = None,
64
+ ) -> str | None:
65
+ """Capture an error through the full processing pipeline.
66
+
67
+ Returns the event ID if sent, None if dropped.
68
+ """
69
+ if not self._enabled:
70
+ return None
71
+
72
+ try:
73
+ # Normalise the error
74
+ error_info = self._normalise_error(error)
75
+
76
+ # Check ignore list
77
+ if self._should_ignore(error_info.get("message", "")):
78
+ return None
79
+
80
+ # Generate fingerprint
81
+ fingerprint = generate_fingerprint(error_info)
82
+
83
+ # Rate limit
84
+ if not self._rate_limiter.should_allow(fingerprint):
85
+ if self._debug:
86
+ print(f"[uncaught] Rate limited: {fingerprint}")
87
+ return None
88
+
89
+ # Build event
90
+ event: UncaughtEvent = {
91
+ "eventId": generate_uuid(),
92
+ "timestamp": iso_timestamp(),
93
+ "level": level,
94
+ "fingerprint": fingerprint,
95
+ "error": error_info,
96
+ "breadcrumbs": self._breadcrumbs.get_all(),
97
+ "environment": detect_environment(),
98
+ "sdk": {"name": SDK_NAME, "version": SDK_VERSION},
99
+ }
100
+
101
+ if self._release:
102
+ event["release"] = self._release
103
+ if self._environment:
104
+ event["environment"]["deploy"] = self._environment
105
+ if request:
106
+ event["request"] = request
107
+ if operation:
108
+ event["operation"] = operation
109
+ if user or self._user:
110
+ event["user"] = {**self._user, **(user or {})}
111
+ if self._config.get("project_key"):
112
+ event["projectKey"] = self._config["project_key"]
113
+
114
+ # Build fix prompt
115
+ event["fixPrompt"] = build_fix_prompt(event)
116
+
117
+ # Sanitize
118
+ event = sanitize(event, self._sanitize_keys)
119
+
120
+ # Before send hook
121
+ if self._before_send:
122
+ result = self._before_send(event)
123
+ if result is None:
124
+ return None
125
+ event = result
126
+
127
+ # Send
128
+ self._transport.send(event)
129
+
130
+ # Webhook on first occurrence
131
+ if self._webhook_url and fingerprint not in self._seen_fingerprints:
132
+ self._seen_fingerprints.add(fingerprint)
133
+ self._send_webhook(event)
134
+
135
+ if self._debug:
136
+ print(f"[uncaught] Captured: {error_info.get('type')}: {error_info.get('message')} ({fingerprint})")
137
+
138
+ return event["eventId"]
139
+
140
+ except Exception as e:
141
+ if self._debug:
142
+ print(f"[uncaught] Internal error: {e}")
143
+ return None
144
+
145
+ def capture_exception(self, **kwargs: Any) -> str | None:
146
+ """Capture the current exception from sys.exc_info()."""
147
+ exc_info = sys.exc_info()
148
+ if exc_info[1] is not None:
149
+ return self.capture_error(exc_info[1], **kwargs)
150
+ return None
151
+
152
+ def add_breadcrumb(self, crumb: dict) -> None:
153
+ """Add a breadcrumb to the ring buffer."""
154
+ self._breadcrumbs.add(crumb)
155
+
156
+ def set_user(self, user: dict[str, Any]) -> None:
157
+ """Set user context for all subsequent events."""
158
+ self._user = user
159
+
160
+ def flush(self) -> None:
161
+ """Flush pending events."""
162
+ self._transport.flush()
163
+
164
+ def _normalise_error(self, error: BaseException | str | dict | Any) -> dict:
165
+ """Normalise any error type into a structured ErrorInfo dict."""
166
+ if isinstance(error, BaseException):
167
+ tb = "".join(traceback.format_exception(type(error), error, error.__traceback__))
168
+ return {
169
+ "message": str(error),
170
+ "type": type(error).__name__,
171
+ "stack": tb,
172
+ }
173
+ if isinstance(error, str):
174
+ return {"message": error, "type": "Error"}
175
+ if isinstance(error, dict):
176
+ return {
177
+ "message": error.get("message", str(error)),
178
+ "type": error.get("type", "Error"),
179
+ "stack": error.get("stack", ""),
180
+ }
181
+ return {"message": str(error), "type": type(error).__name__}
182
+
183
+ def _should_ignore(self, message: str) -> bool:
184
+ """Check if the error message matches any ignore pattern."""
185
+ for pattern in self._ignore_errors:
186
+ if isinstance(pattern, str):
187
+ if pattern in message:
188
+ return True
189
+ else:
190
+ try:
191
+ if re.search(pattern, message):
192
+ return True
193
+ except Exception:
194
+ pass
195
+ return False
196
+
197
+ def _send_webhook(self, event: UncaughtEvent) -> None:
198
+ """Fire-and-forget webhook notification."""
199
+ try:
200
+ import urllib.request
201
+ import json
202
+ error = event.get("error", {})
203
+ payload = json.dumps({
204
+ "title": error.get("message", ""),
205
+ "errorType": error.get("type", ""),
206
+ "fingerprint": event.get("fingerprint", ""),
207
+ "level": event.get("level", ""),
208
+ "timestamp": event.get("timestamp", ""),
209
+ "release": event.get("release"),
210
+ "environment": event.get("environment", {}).get("deploy"),
211
+ "fixPrompt": event.get("fixPrompt", ""),
212
+ }).encode("utf-8")
213
+ req = urllib.request.Request(
214
+ self._webhook_url,
215
+ data=payload,
216
+ headers={"Content-Type": "application/json"},
217
+ )
218
+ urllib.request.urlopen(req, timeout=5)
219
+ except Exception:
220
+ pass # Fire and forget
221
+
222
+ def _setup_global_handlers(self) -> None:
223
+ """Install global exception hooks."""
224
+ original_excepthook = sys.excepthook
225
+
226
+ def uncaught_excepthook(exc_type: type, exc_value: BaseException, exc_tb: Any) -> None:
227
+ self.capture_error(exc_value, level="fatal")
228
+ original_excepthook(exc_type, exc_value, exc_tb)
229
+
230
+ sys.excepthook = uncaught_excepthook
231
+
232
+
233
+ def init_uncaught(config: UncaughtConfig | None = None) -> UncaughtClient:
234
+ """Initialize the global Uncaught client singleton."""
235
+ global _client
236
+ _client = UncaughtClient(config)
237
+ _client._setup_global_handlers()
238
+ return _client
239
+
240
+
241
+ def get_client() -> UncaughtClient:
242
+ """Get the global Uncaught client. Raises if not initialized."""
243
+ if _client is None:
244
+ raise RuntimeError("Uncaught not initialized. Call init_uncaught() first.")
245
+ return _client
@@ -0,0 +1,50 @@
1
+ """Environment detection for Python runtimes and frameworks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import sys
7
+
8
+ from uncaught.types import EnvironmentInfo
9
+
10
+
11
+ def detect_environment() -> EnvironmentInfo:
12
+ """Detect the current Python runtime, framework, and platform."""
13
+ info: EnvironmentInfo = {
14
+ "runtime": "Python",
15
+ "runtimeVersion": platform.python_version(),
16
+ "platform": sys.platform,
17
+ "os": platform.system(),
18
+ }
19
+
20
+ # Detect framework
21
+ if "fastapi" in sys.modules:
22
+ try:
23
+ import fastapi
24
+ info["framework"] = "FastAPI"
25
+ info["frameworkVersion"] = fastapi.__version__
26
+ except (ImportError, AttributeError):
27
+ pass
28
+ elif "flask" in sys.modules:
29
+ try:
30
+ import flask
31
+ info["framework"] = "Flask"
32
+ info["frameworkVersion"] = flask.__version__
33
+ except (ImportError, AttributeError):
34
+ pass
35
+ elif "django" in sys.modules:
36
+ try:
37
+ import django
38
+ info["framework"] = "Django"
39
+ info["frameworkVersion"] = django.get_version()
40
+ except (ImportError, AttributeError):
41
+ pass
42
+ elif "starlette" in sys.modules:
43
+ try:
44
+ import starlette
45
+ info["framework"] = "Starlette"
46
+ info["frameworkVersion"] = starlette.__version__
47
+ except (ImportError, AttributeError):
48
+ pass
49
+
50
+ return info