ErrorAI 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
errorai/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .compat import catch_errors, global_activate, watch
2
+ from .runtime import configure, get_runtime
3
+
4
+ __version__ = "2.0.0"
5
+ __all__ = ["get_runtime", "configure", "watch", "catch_errors", "global_activate"]
6
+
7
+ # Import-time auto-start for zero-decorator usage.
8
+ runtime = get_runtime()
9
+ runtime.initialize()
errorai/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
errorai/bootstrap.py ADDED
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import urllib.request
6
+
7
+ from .config import ModelConfig
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class BootstrapStatus:
12
+ ready: bool
13
+ mode: str
14
+ detail: str
15
+ model_path: Path | None = None
16
+
17
+
18
+ def model_path(config: ModelConfig) -> Path:
19
+ return config.cache_dir / "models" / f"{config.name}.gguf"
20
+
21
+
22
+ def _download_model(url: str, destination: Path) -> None:
23
+ destination.parent.mkdir(parents=True, exist_ok=True)
24
+ with urllib.request.urlopen(url, timeout=5) as response, destination.open("wb") as handle:
25
+ handle.write(response.read())
26
+
27
+
28
+ def ensure_model(config: ModelConfig, explicit: bool = False) -> BootstrapStatus:
29
+ path = model_path(config)
30
+ if path.exists():
31
+ return BootstrapStatus(True, "model-ready", "Local model available.", path)
32
+
33
+ if not explicit and not config.auto_bootstrap:
34
+ return BootstrapStatus(False, "rules-only", "Auto-bootstrap is disabled.", None)
35
+
36
+ try:
37
+ _download_model(config.model_url, path)
38
+ except Exception as exc:
39
+ return BootstrapStatus(
40
+ False,
41
+ "rules-only",
42
+ f"Model bootstrap failed ({exc.__class__.__name__}); continuing without model.",
43
+ None,
44
+ )
45
+ return BootstrapStatus(True, "model-ready", "Model bootstrap completed.", path)
46
+
47
+
48
+ def model_status(config: ModelConfig) -> BootstrapStatus:
49
+ path = model_path(config)
50
+ if path.exists():
51
+ return BootstrapStatus(True, "model-ready", "Model file exists.", path)
52
+ return BootstrapStatus(False, "rules-only", "Model file is not installed.", None)
errorai/cli.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from .bootstrap import ensure_model
7
+ from .config import config_template, load_config
8
+ from .runtime import get_runtime
9
+
10
+
11
+ def cmd_init(args) -> int:
12
+ root = Path.cwd()
13
+ config_path = root / ".errorai.toml"
14
+ if not config_path.exists() or args.force:
15
+ config_path.write_text(config_template(), encoding="utf-8")
16
+ print(f"[errorai] Wrote {config_path}")
17
+ else:
18
+ print(f"[errorai] Config already exists: {config_path}")
19
+ return 0
20
+
21
+
22
+ def cmd_doctor(_args) -> int:
23
+ runtime = get_runtime().initialize()
24
+ status = runtime.status_report()
25
+ print("ErrorAI Doctor")
26
+ for key in (
27
+ "environment",
28
+ "can_watch_fs",
29
+ "can_apply_patches",
30
+ "mode",
31
+ "model_ready",
32
+ "model_mode",
33
+ "model_detail",
34
+ "dry_run",
35
+ "project_root",
36
+ ):
37
+ print(f"- {key}: {status[key]}")
38
+ return 0
39
+
40
+
41
+ def cmd_install_model(_args) -> int:
42
+ cfg = load_config()
43
+ status = ensure_model(cfg.model, explicit=True)
44
+ print(status.detail)
45
+ return 0 if status.ready else 1
46
+
47
+
48
+ def build_parser() -> argparse.ArgumentParser:
49
+ parser = argparse.ArgumentParser(prog="errorai")
50
+ sub = parser.add_subparsers(dest="command", required=True)
51
+
52
+ init_parser = sub.add_parser("init", help="Create default .errorai.toml config.")
53
+ init_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
54
+ init_parser.set_defaults(func=cmd_init)
55
+
56
+ doctor_parser = sub.add_parser("doctor", help="Check runtime readiness and fallback state.")
57
+ doctor_parser.set_defaults(func=cmd_doctor)
58
+
59
+ install_parser = sub.add_parser("install-model", help="Install or retry local model bootstrap.")
60
+ install_parser.set_defaults(func=cmd_install_model)
61
+ return parser
62
+
63
+
64
+ def main(argv: list[str] | None = None) -> int:
65
+ parser = build_parser()
66
+ args = parser.parse_args(argv)
67
+ return args.func(args)
errorai/compat.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import sys
5
+
6
+ from .runtime import get_runtime
7
+
8
+
9
+ def watch(func):
10
+ @functools.wraps(func)
11
+ def wrapper(*args, **kwargs):
12
+ try:
13
+ return func(*args, **kwargs)
14
+ except Exception:
15
+ exc_type, exc_value, exc_tb = sys.exc_info()
16
+ get_runtime().process_exception(exc_type, exc_value, exc_tb)
17
+ raise
18
+
19
+ return wrapper
20
+
21
+
22
+ class catch_errors:
23
+ def __enter__(self):
24
+ return self
25
+
26
+ def __exit__(self, exc_type, exc_value, exc_tb):
27
+ if exc_type is None:
28
+ return False
29
+ get_runtime().process_exception(exc_type, exc_value, exc_tb)
30
+ return True
31
+
32
+
33
+ def global_activate():
34
+ runtime = get_runtime()
35
+ runtime.initialize()
36
+ return runtime
errorai/config.py ADDED
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field, replace
4
+ from pathlib import Path
5
+ import os
6
+ from typing import Any, Dict
7
+
8
+ try:
9
+ import tomllib
10
+ except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
11
+ import tomli as tomllib
12
+
13
+
14
+ def _default_cache_dir() -> Path:
15
+ try:
16
+ from platformdirs import user_cache_dir
17
+
18
+ return Path(user_cache_dir("errorai", "ErrorAI"))
19
+ except Exception:
20
+ return Path.home() / ".cache" / "errorai"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class RuntimeConfig:
25
+ auto_watch: bool = True
26
+ safe_mode: bool = True
27
+ dry_run: bool = True
28
+ project_root: Path = field(default_factory=lambda: Path.cwd())
29
+ ignore_patterns: tuple[str, ...] = (
30
+ ".git",
31
+ "__pycache__",
32
+ ".env",
33
+ ".venv",
34
+ "venv",
35
+ "node_modules",
36
+ "*.lock",
37
+ ".errorai",
38
+ )
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ModelConfig:
43
+ provider: str = "llama_cpp"
44
+ name: str = "qwen2.5-coder-1.5b-instruct-q4_k_m"
45
+ model_url: str = (
46
+ "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/"
47
+ "qwen2.5-coder-1.5b-instruct-q4_k_m.gguf"
48
+ )
49
+ context_window: int = 4096
50
+ temperature: float = 0.1
51
+ auto_bootstrap: bool = True
52
+ cache_dir: Path = field(default_factory=_default_cache_dir)
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class ErrorAIConfig:
57
+ runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
58
+ model: ModelConfig = field(default_factory=ModelConfig)
59
+
60
+
61
+ def _read_toml(path: Path) -> Dict[str, Any]:
62
+ if not path.exists():
63
+ return {}
64
+ with path.open("rb") as handle:
65
+ return tomllib.load(handle)
66
+
67
+
68
+ def _coerce_runtime(base: RuntimeConfig, values: Dict[str, Any]) -> RuntimeConfig:
69
+ data = {}
70
+ for key in ("auto_watch", "safe_mode", "dry_run"):
71
+ if key in values:
72
+ data[key] = bool(values[key])
73
+ if "ignore_patterns" in values:
74
+ data["ignore_patterns"] = tuple(str(v) for v in values["ignore_patterns"])
75
+ if "project_root" in values:
76
+ data["project_root"] = Path(values["project_root"]).resolve()
77
+ return replace(base, **data)
78
+
79
+
80
+ def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
81
+ data = {}
82
+ for key in ("provider", "name", "model_url"):
83
+ if key in values:
84
+ data[key] = str(values[key])
85
+ if "context_window" in values:
86
+ data["context_window"] = int(values["context_window"])
87
+ if "temperature" in values:
88
+ data["temperature"] = float(values["temperature"])
89
+ if "auto_bootstrap" in values:
90
+ data["auto_bootstrap"] = bool(values["auto_bootstrap"])
91
+ if "cache_dir" in values:
92
+ data["cache_dir"] = Path(values["cache_dir"]).expanduser().resolve()
93
+ return replace(base, **data)
94
+
95
+
96
+ def load_config(project_root: Path | None = None) -> ErrorAIConfig:
97
+ root = (project_root or Path.cwd()).resolve()
98
+ config = ErrorAIConfig(runtime=RuntimeConfig(project_root=root))
99
+ pyproject = _read_toml(root / "pyproject.toml")
100
+ pyproject_settings = pyproject.get("tool", {}).get("errorai", {})
101
+ local_settings = _read_toml(root / ".errorai.toml")
102
+
103
+ runtime_values = {}
104
+ model_values = {}
105
+ for source in (pyproject_settings, local_settings):
106
+ runtime_values.update(source.get("runtime", {}))
107
+ model_values.update(source.get("model", {}))
108
+
109
+ return ErrorAIConfig(
110
+ runtime=_coerce_runtime(config.runtime, runtime_values),
111
+ model=_coerce_model(config.model, model_values),
112
+ )
113
+
114
+
115
+ def config_template() -> str:
116
+ return """[runtime]
117
+ auto_watch = true
118
+ safe_mode = true
119
+ dry_run = true
120
+ ignore_patterns = [".git", "__pycache__", ".env", ".venv", "venv", "node_modules", "*.lock", ".errorai"]
121
+
122
+ [model]
123
+ provider = "llama_cpp"
124
+ name = "qwen2.5-coder-1.5b-instruct-q4_k_m"
125
+ context_window = 4096
126
+ temperature = 0.1
127
+ auto_bootstrap = true
128
+ """
129
+
130
+
131
+ def autostart_enabled() -> bool:
132
+ return os.environ.get("ERRORAI_AUTOSTART", "1") != "0"
errorai/environment.py ADDED
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import os
5
+ import sys
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Capabilities:
10
+ environment: str
11
+ can_watch_fs: bool
12
+ can_apply_patches: bool
13
+ can_prompt_user: bool
14
+
15
+
16
+ def detect_capabilities() -> Capabilities:
17
+ env = "generic"
18
+ interactive = bool(getattr(sys, "ps1", None) or sys.flags.interactive)
19
+ if "ipykernel" in sys.modules or "JPY_PARENT_PID" in os.environ:
20
+ env = "notebook"
21
+ elif "idlelib" in sys.modules:
22
+ env = "idle"
23
+ elif interactive:
24
+ env = "interactive"
25
+ elif sys.stdin and sys.stdin.isatty():
26
+ env = "terminal"
27
+ else:
28
+ env = "ide"
29
+
30
+ can_watch_fs = env not in {"notebook"}
31
+ can_apply_patches = env != "notebook"
32
+ can_prompt_user = env in {"terminal", "interactive", "idle", "ide"}
33
+ return Capabilities(
34
+ environment=env,
35
+ can_watch_fs=can_watch_fs,
36
+ can_apply_patches=can_apply_patches,
37
+ can_prompt_user=can_prompt_user,
38
+ )
errorai/pipeline.py ADDED
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ import fnmatch
7
+ import json
8
+ import threading
9
+ from typing import Any
10
+
11
+ from .config import RuntimeConfig
12
+ from .environment import Capabilities
13
+ from .providers import ModelProvider
14
+
15
+
16
+ class Reporter:
17
+ def __init__(self, project_root: Path):
18
+ self.log_path = project_root / ".errorai" / "logs" / "operations.log"
19
+ self.log_path.parent.mkdir(parents=True, exist_ok=True)
20
+ self._lock = threading.Lock()
21
+
22
+ def log(self, event: str, payload: dict[str, Any]) -> None:
23
+ record = {
24
+ "time": datetime.now(timezone.utc).isoformat(),
25
+ "event": event,
26
+ "payload": payload,
27
+ }
28
+ with self._lock, self.log_path.open("a", encoding="utf-8") as handle:
29
+ handle.write(json.dumps(record) + "\n")
30
+
31
+
32
+ class Analyzer:
33
+ def analyze_exception(self, exc_type, exc_value, exc_tb) -> dict[str, Any]:
34
+ return {
35
+ "type": getattr(exc_type, "__name__", str(exc_type)),
36
+ "message": str(exc_value),
37
+ }
38
+
39
+
40
+ class Planner:
41
+ def __init__(self, provider: ModelProvider):
42
+ self.provider = provider
43
+
44
+ def plan_line_fix(self, line: str, error_message: str) -> str | None:
45
+ return self.provider.suggest_patch(line, error_message)
46
+
47
+
48
+ @dataclass
49
+ class ApplyResult:
50
+ changed: bool
51
+ detail: str
52
+ preview: str | None = None
53
+
54
+
55
+ class Applier:
56
+ def __init__(self, config: RuntimeConfig):
57
+ self.config = config
58
+ self.project_root = config.project_root.resolve()
59
+
60
+ def _is_ignored(self, path: Path) -> bool:
61
+ rel = str(path)
62
+ for pattern in self.config.ignore_patterns:
63
+ if fnmatch.fnmatch(path.name, pattern) or fnmatch.fnmatch(rel, pattern):
64
+ return True
65
+ if pattern in path.parts:
66
+ return True
67
+ return False
68
+
69
+ def _is_within_root(self, path: Path) -> bool:
70
+ try:
71
+ path.resolve().relative_to(self.project_root)
72
+ except ValueError:
73
+ return False
74
+ return True
75
+
76
+ def can_edit(self, file_path: Path) -> bool:
77
+ path = file_path.resolve()
78
+ return self._is_within_root(path) and not self._is_ignored(path)
79
+
80
+ def apply_line_change(self, file_path: Path, lineno: int, new_line: str) -> ApplyResult:
81
+ path = file_path.resolve()
82
+ if not self.can_edit(path):
83
+ return ApplyResult(False, "Blocked by safe mode restrictions.")
84
+
85
+ lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
86
+ if lineno < 1 or lineno > len(lines):
87
+ return ApplyResult(False, "Line number out of range.")
88
+
89
+ old_line = lines[lineno - 1]
90
+ indentation = old_line[: len(old_line) - len(old_line.lstrip())]
91
+ candidate = f"{indentation}{new_line.strip()}\n"
92
+ preview = f"- {old_line.rstrip()}\n+ {candidate.rstrip()}"
93
+ if self.config.dry_run:
94
+ return ApplyResult(False, "Dry-run mode enabled; no write applied.", preview=preview)
95
+
96
+ lines[lineno - 1] = candidate
97
+ path.write_text("".join(lines), encoding="utf-8")
98
+ return ApplyResult(True, "Edit applied.", preview=preview)
99
+
100
+
101
+ class Watcher:
102
+ def __init__(self, capabilities: Capabilities, reporter: Reporter):
103
+ self.capabilities = capabilities
104
+ self.reporter = reporter
105
+ self.started = False
106
+
107
+ def start(self) -> bool:
108
+ if not self.capabilities.can_watch_fs:
109
+ self.reporter.log("watcher.skipped", {"reason": "environment unsupported"})
110
+ return False
111
+ try:
112
+ from watchdog.observers import Observer # type: ignore # noqa: F401
113
+ except Exception:
114
+ self.reporter.log("watcher.skipped", {"reason": "watchdog unavailable"})
115
+ return False
116
+ self.started = True
117
+ self.reporter.log("watcher.started", {})
118
+ return True
errorai/providers.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from .config import ModelConfig
8
+
9
+
10
+ class ModelProvider(ABC):
11
+ @abstractmethod
12
+ def suggest_patch(self, snippet: str, error_message: str) -> str | None:
13
+ raise NotImplementedError
14
+
15
+
16
+ class RulesOnlyProvider(ModelProvider):
17
+ def suggest_patch(self, snippet: str, error_message: str) -> str | None:
18
+ lowered = error_message.lower()
19
+ if "expected ':'" in lowered and not snippet.rstrip().endswith(":"):
20
+ return f"{snippet.rstrip()}:"
21
+ return None
22
+
23
+
24
+ @dataclass
25
+ class LlamaCppProvider(ModelProvider):
26
+ config: ModelConfig
27
+ model_path: Path
28
+ _llm: object | None = None
29
+
30
+ def _get_llm(self):
31
+ if self._llm is not None:
32
+ return self._llm
33
+ from llama_cpp import Llama # type: ignore
34
+
35
+ self._llm = Llama(
36
+ model_path=str(self.model_path),
37
+ n_ctx=self.config.context_window,
38
+ verbose=False,
39
+ )
40
+ return self._llm
41
+
42
+ def suggest_patch(self, snippet: str, error_message: str) -> str | None:
43
+ llm = self._get_llm()
44
+ prompt = (
45
+ "You are fixing Python code. Return only the fixed single line.\n"
46
+ f"Error: {error_message}\n"
47
+ f"Code: {snippet}\n"
48
+ "Fix:"
49
+ )
50
+ response = llm(
51
+ prompt=prompt,
52
+ temperature=self.config.temperature,
53
+ max_tokens=128,
54
+ stop=["\n\n"],
55
+ )
56
+ text = response["choices"][0]["text"].strip()
57
+ return text or None
errorai/runtime.py ADDED
@@ -0,0 +1,186 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ from pathlib import Path
5
+ import linecache
6
+ import sys
7
+ import threading
8
+ import traceback
9
+ from types import TracebackType
10
+ from typing import Any
11
+
12
+ from .bootstrap import BootstrapStatus, ensure_model, model_status
13
+ from .config import ErrorAIConfig, autostart_enabled, load_config
14
+ from .environment import Capabilities, detect_capabilities
15
+ from .pipeline import Analyzer, Applier, Planner, Reporter, Watcher
16
+ from .providers import LlamaCppProvider, ModelProvider, RulesOnlyProvider
17
+
18
+
19
+ class RuntimeManager:
20
+ _instance: "RuntimeManager | None" = None
21
+ _instance_lock = threading.Lock()
22
+
23
+ def __init__(self) -> None:
24
+ self._init_lock = threading.Lock()
25
+ self._initialized = False
26
+ self.config: ErrorAIConfig | None = None
27
+ self.capabilities: Capabilities | None = None
28
+ self.reporter: Reporter | None = None
29
+ self.analyzer: Analyzer | None = None
30
+ self.planner: Planner | None = None
31
+ self.applier: Applier | None = None
32
+ self.watcher: Watcher | None = None
33
+ self.provider: ModelProvider = RulesOnlyProvider()
34
+ self.bootstrap_status = BootstrapStatus(False, "rules-only", "Not initialized.", None)
35
+ self.mode = "rules-only"
36
+ self._orig_sys_hook = sys.excepthook
37
+ self._orig_thread_hook = getattr(threading, "excepthook", None)
38
+
39
+ @classmethod
40
+ def get_instance(cls) -> "RuntimeManager":
41
+ with cls._instance_lock:
42
+ if cls._instance is None:
43
+ cls._instance = cls()
44
+ return cls._instance
45
+
46
+ def initialize(self, config_override: ErrorAIConfig | None = None) -> "RuntimeManager":
47
+ if not autostart_enabled():
48
+ return self
49
+ with self._init_lock:
50
+ if self._initialized:
51
+ return self
52
+
53
+ self.config = config_override or load_config()
54
+ self.capabilities = detect_capabilities()
55
+ self.reporter = Reporter(self.config.runtime.project_root)
56
+ self.analyzer = Analyzer()
57
+ self.applier = Applier(self.config.runtime)
58
+ self.bootstrap_status = ensure_model(self.config.model, explicit=False)
59
+ self.provider = self._select_provider(self.bootstrap_status)
60
+ self.planner = Planner(self.provider)
61
+ self.watcher = Watcher(self.capabilities, self.reporter)
62
+ self._register_hooks()
63
+ watch_started = False
64
+ if self.config.runtime.auto_watch:
65
+ watch_started = self.watcher.start()
66
+ self.mode = "full" if self.bootstrap_status.ready and watch_started else "analyze-only"
67
+ if not self.bootstrap_status.ready:
68
+ self.mode = "rules-only"
69
+ self.reporter.log(
70
+ "runtime.initialized",
71
+ {
72
+ "environment": self.capabilities.environment,
73
+ "mode": self.mode,
74
+ "model_mode": self.bootstrap_status.mode,
75
+ },
76
+ )
77
+ self._initialized = True
78
+ return self
79
+
80
+ def _select_provider(self, status: BootstrapStatus) -> ModelProvider:
81
+ if not status.ready or status.model_path is None or self.config is None:
82
+ return RulesOnlyProvider()
83
+ if self.config.model.provider != "llama_cpp":
84
+ return RulesOnlyProvider()
85
+ try:
86
+ return LlamaCppProvider(self.config.model, status.model_path)
87
+ except Exception:
88
+ return RulesOnlyProvider()
89
+
90
+ def _register_hooks(self) -> None:
91
+ self._orig_sys_hook = sys.excepthook
92
+ sys.excepthook = self._sys_excepthook
93
+ if hasattr(threading, "excepthook"):
94
+ self._orig_thread_hook = threading.excepthook
95
+ threading.excepthook = self._thread_excepthook
96
+
97
+ def _sys_excepthook(self, exc_type, exc_value, exc_tb):
98
+ self.process_exception(exc_type, exc_value, exc_tb)
99
+ if self._orig_sys_hook:
100
+ self._orig_sys_hook(exc_type, exc_value, exc_tb)
101
+
102
+ def _thread_excepthook(self, args):
103
+ self.process_exception(args.exc_type, args.exc_value, args.exc_traceback)
104
+ if self._orig_thread_hook:
105
+ self._orig_thread_hook(args)
106
+
107
+ def process_exception(self, exc_type, exc_value, exc_tb: TracebackType | None) -> bool:
108
+ if not exc_tb or not self.analyzer or not self.planner or not self.applier or not self.reporter:
109
+ return False
110
+ tb_list = traceback.extract_tb(exc_tb)
111
+ if not tb_list:
112
+ return False
113
+ frame = tb_list[-1]
114
+ filename = frame.filename
115
+ line = linecache.getline(filename, frame.lineno).strip() if filename else ""
116
+ analysis = self.analyzer.analyze_exception(exc_type, exc_value, exc_tb)
117
+ plan = self.planner.plan_line_fix(line or frame.line or "", analysis["message"])
118
+ if not plan or not filename or "<" in filename:
119
+ self.reporter.log("exception.analyzed", {"analysis": analysis, "fixed": False})
120
+ return False
121
+ result = self.applier.apply_line_change(Path(filename), frame.lineno, plan)
122
+ self.reporter.log(
123
+ "exception.handled",
124
+ {
125
+ "analysis": analysis,
126
+ "changed": result.changed,
127
+ "detail": result.detail,
128
+ "preview": result.preview,
129
+ },
130
+ )
131
+ return result.changed
132
+
133
+ def status_report(self) -> dict[str, Any]:
134
+ cfg = self.config or load_config()
135
+ caps = self.capabilities or detect_capabilities()
136
+ model = model_status(cfg.model)
137
+ return {
138
+ "environment": caps.environment,
139
+ "can_watch_fs": caps.can_watch_fs,
140
+ "can_apply_patches": caps.can_apply_patches,
141
+ "mode": self.mode,
142
+ "model_ready": model.ready,
143
+ "model_mode": self.bootstrap_status.mode,
144
+ "model_detail": self.bootstrap_status.detail,
145
+ "dry_run": cfg.runtime.dry_run,
146
+ "project_root": str(cfg.runtime.project_root),
147
+ }
148
+
149
+ def install_model(self) -> BootstrapStatus:
150
+ if self.config is None:
151
+ self.config = load_config()
152
+ self.bootstrap_status = ensure_model(self.config.model, explicit=True)
153
+ return self.bootstrap_status
154
+
155
+ def configure(self, **overrides) -> None:
156
+ if self.config is None:
157
+ self.config = load_config()
158
+ runtime_values = overrides.get("runtime", {})
159
+ model_values = overrides.get("model", {})
160
+ self.config = ErrorAIConfig(
161
+ runtime=replace(self.config.runtime, **runtime_values),
162
+ model=replace(self.config.model, **model_values),
163
+ )
164
+
165
+ def shutdown(self) -> None:
166
+ sys.excepthook = self._orig_sys_hook
167
+ if hasattr(threading, "excepthook") and self._orig_thread_hook is not None:
168
+ threading.excepthook = self._orig_thread_hook
169
+ self._initialized = False
170
+
171
+
172
+ def get_runtime() -> RuntimeManager:
173
+ return RuntimeManager.get_instance()
174
+
175
+
176
+ def configure(**overrides) -> RuntimeManager:
177
+ runtime = get_runtime()
178
+ runtime.configure(**overrides)
179
+ return runtime
180
+
181
+
182
+ def _reset_for_tests() -> None:
183
+ runtime = RuntimeManager._instance
184
+ if runtime is not None:
185
+ runtime.shutdown()
186
+ RuntimeManager._instance = None
errorai/universal.py ADDED
@@ -0,0 +1,8 @@
1
+ from .compat import catch_errors, global_activate, watch
2
+ from .runtime import get_runtime
3
+
4
+
5
+ class ErrorAI:
6
+ @staticmethod
7
+ def inspect_and_fix(exc_type, exc_value, exc_tb):
8
+ return get_runtime().process_exception(exc_type, exc_value, exc_tb)
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErrorAI
3
+ Version: 2.0.0
4
+ Summary: Python-only autonomous runtime that watches/analyzes errors with safe local auto-fix fallbacks.
5
+ Author: Aswanth R
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/NodeX-AR/ErrorAI
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Software Development :: Quality Assurance
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: platformdirs>=3.0
15
+ Provides-Extra: local-model
16
+ Requires-Dist: llama-cpp-python>=0.2.90; extra == "local-model"
17
+ Provides-Extra: watch
18
+ Requires-Dist: watchdog>=4.0.0; extra == "watch"
19
+
20
+ # ErrorAI v2
21
+
22
+ `ErrorAI` is a Python-only autonomous runtime that starts on import, catches exceptions, and operates with safe-by-default watch/read/write behavior.
23
+
24
+ ## Quickstart
25
+
26
+ ```bash
27
+ pip install ErrorAI
28
+ ```
29
+
30
+ ```python
31
+ import errorai
32
+ ```
33
+
34
+ That import initializes a singleton runtime, registers exception hooks, and starts background watch services when supported.
35
+
36
+ ## Optional setup commands
37
+
38
+
39
+ ```bash
40
+ errorai init
41
+ errorai doctor
42
+ errorai install-model
43
+ ```
44
+
45
+ - `init`: writes default `.errorai.toml`
46
+ - `doctor`: reports environment/runtime/model readiness and fallback mode
47
+ - `install-model`: retries local model bootstrap
48
+
49
+ ## Model bootstrap strategy
50
+
51
+ - Core package does not bundle large model weights.
52
+ - On import/first use, ErrorAI attempts to bootstrap a lightweight local coding model into user cache.
53
+ - If bootstrap fails (offline/network/permissions), runtime continues in `rules-only` mode with clear status.
54
+
55
+ ## Safety defaults
56
+
57
+ - Safe mode is enabled by default.
58
+ - Writes are restricted to the project root.
59
+ - Sensitive/common ignore patterns are blocked by default.
60
+ - Dry-run mode is on by default to preview edits before writing.
61
+
62
+ ## Migration notes from v1 beta
63
+
64
+ - `import errorai` now auto-starts the runtime; explicit `global_activate()` is optional.
65
+ - `@watch` and `catch_errors` remain available for compatibility but are no longer required.
@@ -0,0 +1,16 @@
1
+ errorai/__init__.py,sha256=J67kAHQlrBPKaMsN-yC8u_bQLc-JVmW0QzouUjfEhbA,304
2
+ errorai/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ errorai/bootstrap.py,sha256=26IxxdYNnbOh-3CdPZ4qhYqqK8uOX4Chje3Yj3lbc8M,1649
4
+ errorai/cli.py,sha256=bPSrHHAr-z5f316neiGz5hdTZrJalAI63RxyCNx4PxI,1989
5
+ errorai/compat.py,sha256=UKnd-1DLbHUjUSskK2CLOIZkr4eRoYQ7O4-zYjyAu7E,787
6
+ errorai/config.py,sha256=jpDaQuChuT_FRjFx3Z-FDK58lavr6VMROEaZQ_ONiKM,3969
7
+ errorai/environment.py,sha256=rN4eLD0xkaLYb-Zu63HGk9mG4B3gL-3ZcW2g1LEIhyI,1008
8
+ errorai/pipeline.py,sha256=vglfYQQ0JGEqopMTf3ybmNACaB-rK_CW1ciYFRy6ilc,3960
9
+ errorai/providers.py,sha256=jVEb33MvFX8Xdwl3p7i2xaZVOOgAfg1p5ZuK6g0Qbho,1625
10
+ errorai/runtime.py,sha256=uITKabXhzRPgy9NGyarXEG31SScVdw3bu6ZaElW30WQ,7353
11
+ errorai/universal.py,sha256=zcu7oH_EjxmUzGIQ_EAwOq3FJOisDKvdQvB6uKXKhIk,255
12
+ errorai-2.0.0.dist-info/METADATA,sha256=w8KMb3A58XXzm8wifIl_IFpedu1nnBELk37C0rTi2OI,2077
13
+ errorai-2.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
14
+ errorai-2.0.0.dist-info/entry_points.txt,sha256=5F4SLE2xjnAcrxsaJdFm2n1klc9o2zhaRwJktPrxB2E,45
15
+ errorai-2.0.0.dist-info/top_level.txt,sha256=RcAm9aPNk9gmal-or7wKBjGOOrnXp7H-JfroHu1HJqo,8
16
+ errorai-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ errorai = errorai.cli:main
@@ -0,0 +1 @@
1
+ errorai