nexcoder 0.1.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.
- nex/__init__.py +6 -0
- nex/agent.py +623 -0
- nex/api_client.py +194 -0
- nex/cli.py +506 -0
- nex/config.py +168 -0
- nex/context.py +252 -0
- nex/exceptions.py +39 -0
- nex/indexer/__init__.py +16 -0
- nex/indexer/index.py +332 -0
- nex/indexer/parser.py +352 -0
- nex/indexer/scanner.py +191 -0
- nex/memory/__init__.py +15 -0
- nex/memory/decisions.py +131 -0
- nex/memory/errors.py +257 -0
- nex/memory/project.py +158 -0
- nex/planner.py +122 -0
- nex/py.typed +0 -0
- nex/reviewer.py +111 -0
- nex/safety.py +235 -0
- nex/test_runner.py +201 -0
- nex/tools/__init__.py +114 -0
- nex/tools/file_ops.py +89 -0
- nex/tools/git_ops.py +183 -0
- nex/tools/search.py +156 -0
- nex/tools/shell.py +72 -0
- nexcoder-0.1.0.dist-info/METADATA +170 -0
- nexcoder-0.1.0.dist-info/RECORD +30 -0
- nexcoder-0.1.0.dist-info/WHEEL +4 -0
- nexcoder-0.1.0.dist-info/entry_points.txt +2 -0
- nexcoder-0.1.0.dist-info/licenses/LICENSE +21 -0
nex/config.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Configuration management for Nex AI.
|
|
2
|
+
|
|
3
|
+
Settings are loaded from three sources in order of priority:
|
|
4
|
+
1. Environment variables (highest priority)
|
|
5
|
+
2. Project-level config: .nex/config.toml
|
|
6
|
+
3. Global config: ~/.config/nex/config.toml (lowest priority)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import tomllib
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
from nex.exceptions import ConfigError
|
|
20
|
+
|
|
21
|
+
console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
_GLOBAL_CONFIG_DIR = Path.home() / ".config" / "nex"
|
|
24
|
+
_GLOBAL_CONFIG_PATH = _GLOBAL_CONFIG_DIR / "config.toml"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class NexConfig:
|
|
29
|
+
"""Nex AI configuration.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
api_key: Anthropic API key.
|
|
33
|
+
model: Default model for code generation and reasoning.
|
|
34
|
+
haiku_model: Model for planning and lightweight tasks.
|
|
35
|
+
max_iterations: Maximum tool calls per task.
|
|
36
|
+
dry_run: If True, show planned actions without executing.
|
|
37
|
+
log_level: Logging verbosity (DEBUG, INFO, WARNING, ERROR).
|
|
38
|
+
nex_dir: Path to the .nex directory (relative to project root).
|
|
39
|
+
test_command: Override for auto-detected test command (empty = auto-detect).
|
|
40
|
+
test_timeout: Maximum seconds to wait for test suite to complete.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
project_dir: Path = field(default_factory=Path.cwd)
|
|
44
|
+
api_key: str = ""
|
|
45
|
+
model: str = "claude-sonnet-4-20250514"
|
|
46
|
+
haiku_model: str = "claude-haiku-4-5-20251001"
|
|
47
|
+
max_iterations: int = 25
|
|
48
|
+
dry_run: bool = False
|
|
49
|
+
log_level: str = "INFO"
|
|
50
|
+
nex_dir: Path = field(default_factory=lambda: Path(".nex"))
|
|
51
|
+
test_command: str = ""
|
|
52
|
+
test_timeout: int = 120
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_config(project_dir: Path) -> NexConfig:
|
|
56
|
+
"""Load configuration from env vars, project config, and global config.
|
|
57
|
+
|
|
58
|
+
Priority: env vars > .nex/config.toml > ~/.config/nex/config.toml
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
project_dir: Root directory of the project.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A fully resolved NexConfig instance.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ConfigError: If the API key is not set anywhere.
|
|
68
|
+
"""
|
|
69
|
+
config = NexConfig(project_dir=project_dir)
|
|
70
|
+
|
|
71
|
+
# Layer 1: Global config (lowest priority)
|
|
72
|
+
global_settings = _load_toml(_GLOBAL_CONFIG_PATH)
|
|
73
|
+
_apply_toml(config, global_settings)
|
|
74
|
+
|
|
75
|
+
# Layer 2: Project config
|
|
76
|
+
project_config_path = project_dir / ".nex" / "config.toml"
|
|
77
|
+
project_settings = _load_toml(project_config_path)
|
|
78
|
+
_apply_toml(config, project_settings)
|
|
79
|
+
|
|
80
|
+
# Layer 3: Environment variables (highest priority)
|
|
81
|
+
_apply_env(config)
|
|
82
|
+
|
|
83
|
+
return config
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ensure_api_key(config: NexConfig) -> None:
|
|
87
|
+
"""Validate that an API key is configured.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
config: The configuration to validate.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ConfigError: If api_key is empty.
|
|
94
|
+
"""
|
|
95
|
+
if not config.api_key:
|
|
96
|
+
raise ConfigError(
|
|
97
|
+
"Anthropic API key not found. Set ANTHROPIC_API_KEY environment variable "
|
|
98
|
+
"or run 'nex auth' to configure it."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def save_global_config(key: str, value: str) -> None:
|
|
103
|
+
"""Save a key-value pair to the global config file.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
key: Configuration key (e.g. "api_key").
|
|
107
|
+
value: Configuration value.
|
|
108
|
+
"""
|
|
109
|
+
_GLOBAL_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
settings: dict[str, Any] = {}
|
|
112
|
+
if _GLOBAL_CONFIG_PATH.is_file():
|
|
113
|
+
settings = _load_toml(_GLOBAL_CONFIG_PATH)
|
|
114
|
+
|
|
115
|
+
settings[key] = value
|
|
116
|
+
|
|
117
|
+
lines = [f'{k} = "{v}"' for k, v in settings.items()]
|
|
118
|
+
_GLOBAL_CONFIG_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
119
|
+
console.print(f"[green]Saved[/green] {key} to {_GLOBAL_CONFIG_PATH}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _load_toml(path: Path) -> dict[str, Any]:
|
|
123
|
+
"""Load a TOML file, returning an empty dict if missing or invalid."""
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
return {}
|
|
126
|
+
try:
|
|
127
|
+
return tomllib.loads(path.read_text(encoding="utf-8"))
|
|
128
|
+
except (tomllib.TOMLDecodeError, OSError) as exc:
|
|
129
|
+
console.print(f"[yellow]Warning:[/yellow] Could not parse {path}: {exc}")
|
|
130
|
+
return {}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _apply_toml(config: NexConfig, settings: dict[str, Any]) -> None:
|
|
134
|
+
"""Merge TOML settings into a NexConfig (only set non-empty values)."""
|
|
135
|
+
if "api_key" in settings:
|
|
136
|
+
config.api_key = str(settings["api_key"])
|
|
137
|
+
if "model" in settings:
|
|
138
|
+
config.model = str(settings["model"])
|
|
139
|
+
if "haiku_model" in settings:
|
|
140
|
+
config.haiku_model = str(settings["haiku_model"])
|
|
141
|
+
if "max_iterations" in settings:
|
|
142
|
+
config.max_iterations = int(settings["max_iterations"])
|
|
143
|
+
if "dry_run" in settings:
|
|
144
|
+
config.dry_run = bool(settings["dry_run"])
|
|
145
|
+
if "log_level" in settings:
|
|
146
|
+
config.log_level = str(settings["log_level"]).upper()
|
|
147
|
+
if "test_command" in settings:
|
|
148
|
+
config.test_command = str(settings["test_command"])
|
|
149
|
+
if "test_timeout" in settings:
|
|
150
|
+
config.test_timeout = int(settings["test_timeout"])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _apply_env(config: NexConfig) -> None:
|
|
154
|
+
"""Override config with environment variables where set."""
|
|
155
|
+
if api_key := os.environ.get("ANTHROPIC_API_KEY"):
|
|
156
|
+
config.api_key = api_key
|
|
157
|
+
if model := os.environ.get("NEX_MODEL"):
|
|
158
|
+
config.model = model
|
|
159
|
+
if max_iter := os.environ.get("NEX_MAX_ITERATIONS"):
|
|
160
|
+
config.max_iterations = int(max_iter)
|
|
161
|
+
if dry_run := os.environ.get("NEX_DRY_RUN"):
|
|
162
|
+
config.dry_run = dry_run.lower() in ("true", "1", "yes")
|
|
163
|
+
if log_level := os.environ.get("NEX_LOG_LEVEL"):
|
|
164
|
+
config.log_level = log_level.upper()
|
|
165
|
+
if test_cmd := os.environ.get("NEX_TEST_COMMAND"):
|
|
166
|
+
config.test_command = test_cmd
|
|
167
|
+
if test_timeout := os.environ.get("NEX_TEST_TIMEOUT"):
|
|
168
|
+
config.test_timeout = int(test_timeout)
|
nex/context.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Context assembly for API calls with priority-based token budgeting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from nex.indexer.index import CodeIndex, IndexBuilder
|
|
10
|
+
from nex.memory.errors import ErrorPattern
|
|
11
|
+
|
|
12
|
+
console = Console(stderr=True)
|
|
13
|
+
|
|
14
|
+
_COMMENT_PREFIXES = ("#", "//", "/*", "*", '"""', "'''")
|
|
15
|
+
|
|
16
|
+
_SYSTEM_PROMPT_TEMPLATE = """\
|
|
17
|
+
You are Nex, an AI coding agent that works on the user's codebase. You have access to tools \
|
|
18
|
+
for reading files, writing files, running commands, searching code, and listing directories.
|
|
19
|
+
|
|
20
|
+
## Project Context
|
|
21
|
+
{project_memory}
|
|
22
|
+
|
|
23
|
+
## Past Errors to Avoid
|
|
24
|
+
{error_patterns}
|
|
25
|
+
|
|
26
|
+
## Relevant Code
|
|
27
|
+
{relevant_code}
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
- Always read existing code before modifying it. Match the project's style.
|
|
31
|
+
- Run tests after making changes. If tests fail, fix them before reporting success.
|
|
32
|
+
- Never execute destructive commands without user approval.
|
|
33
|
+
- When you make an architectural decision, explain why briefly.
|
|
34
|
+
- If you're unsure about something, ask the user rather than guessing.
|
|
35
|
+
- Keep changes minimal — don't refactor code that isn't related to the task.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ContextAssembler:
|
|
40
|
+
"""Assembles the context window for API calls with priority-based budgeting.
|
|
41
|
+
|
|
42
|
+
Token budget: 150K of 200K window reserved for context, 50K for response.
|
|
43
|
+
Priority order: system prompt -> project memory -> error patterns -> relevant code.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
TOKEN_BUDGET = 150_000
|
|
47
|
+
|
|
48
|
+
def __init__(self, project_dir: Path) -> None:
|
|
49
|
+
"""Initialize the context assembler.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
project_dir: Project root directory.
|
|
53
|
+
"""
|
|
54
|
+
self._project_dir = project_dir
|
|
55
|
+
|
|
56
|
+
def build_system_prompt(
|
|
57
|
+
self,
|
|
58
|
+
project_memory: str,
|
|
59
|
+
error_patterns: list[ErrorPattern],
|
|
60
|
+
relevant_code: list[tuple[str, str]],
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Assemble the system prompt from all context sources.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
project_memory: Contents of .nex/memory.md.
|
|
66
|
+
error_patterns: Recent similar error patterns.
|
|
67
|
+
relevant_code: (file_path, content) pairs of relevant code.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The assembled system prompt string.
|
|
71
|
+
"""
|
|
72
|
+
# Format error patterns
|
|
73
|
+
if error_patterns:
|
|
74
|
+
error_text = "\n".join(
|
|
75
|
+
f"- [{ep.error_type}] {ep.what_failed} -> Fix: {ep.what_fixed}"
|
|
76
|
+
+ (f" (in {ep.file_path})" if ep.file_path else "")
|
|
77
|
+
for ep in error_patterns
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
error_text = "No relevant past errors found."
|
|
81
|
+
|
|
82
|
+
# Format relevant code
|
|
83
|
+
if relevant_code:
|
|
84
|
+
code_sections = []
|
|
85
|
+
for file_path, content in relevant_code:
|
|
86
|
+
code_sections.append(f"### {file_path}\n```\n{content}\n```")
|
|
87
|
+
code_text = "\n\n".join(code_sections)
|
|
88
|
+
else:
|
|
89
|
+
code_text = "No relevant code indexed yet."
|
|
90
|
+
|
|
91
|
+
return _SYSTEM_PROMPT_TEMPLATE.format(
|
|
92
|
+
project_memory=project_memory or "No project memory found. Run 'nex init' first.",
|
|
93
|
+
error_patterns=error_text,
|
|
94
|
+
relevant_code=code_text,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def select_relevant_code(
|
|
98
|
+
self,
|
|
99
|
+
task: str,
|
|
100
|
+
index: CodeIndex | None,
|
|
101
|
+
budget_tokens: int = 100_000,
|
|
102
|
+
) -> list[tuple[str, str]]:
|
|
103
|
+
"""Select relevant code files based on task description.
|
|
104
|
+
|
|
105
|
+
Uses file-level TF-IDF relevance scoring from the index. Applies a
|
|
106
|
+
relevance threshold to filter noise. Two-phase budget: 60% for full
|
|
107
|
+
file content, 40% for signature-only summaries of remaining files.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
task: The user's task description.
|
|
111
|
+
index: Pre-loaded code index.
|
|
112
|
+
budget_tokens: Maximum tokens worth of code to include.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of (file_path, content) pairs.
|
|
116
|
+
"""
|
|
117
|
+
if index is None:
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
builder = IndexBuilder(self._project_dir)
|
|
121
|
+
try:
|
|
122
|
+
ranked_files = builder.search_files(task, index)
|
|
123
|
+
except Exception:
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
if not ranked_files:
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
# Apply relevance threshold
|
|
130
|
+
top_score = ranked_files[0][1]
|
|
131
|
+
threshold = self._relevance_threshold(top_score)
|
|
132
|
+
ranked_files = [(fp, sc) for fp, sc in ranked_files if sc >= threshold]
|
|
133
|
+
|
|
134
|
+
# Two-phase budget: 60% full content, 40% signature summaries
|
|
135
|
+
full_budget = int(budget_tokens * 0.60)
|
|
136
|
+
sig_budget = budget_tokens - full_budget
|
|
137
|
+
|
|
138
|
+
results: list[tuple[str, str]] = []
|
|
139
|
+
tokens_used = 0
|
|
140
|
+
remaining_files: list[str] = []
|
|
141
|
+
|
|
142
|
+
# Phase 1: Full file content
|
|
143
|
+
for file_path, _score in ranked_files:
|
|
144
|
+
abs_path = self._project_dir / file_path
|
|
145
|
+
if not abs_path.is_file():
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
content = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
150
|
+
except OSError:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
file_tokens = self.estimate_tokens(content)
|
|
154
|
+
if tokens_used + file_tokens <= full_budget:
|
|
155
|
+
results.append((file_path, content))
|
|
156
|
+
tokens_used += file_tokens
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Try truncation (first 200 lines)
|
|
160
|
+
lines = content.splitlines()[:200]
|
|
161
|
+
truncated = "\n".join(lines) + "\n... (truncated)"
|
|
162
|
+
trunc_tokens = self.estimate_tokens(truncated)
|
|
163
|
+
if tokens_used + trunc_tokens <= full_budget:
|
|
164
|
+
results.append((file_path, truncated))
|
|
165
|
+
tokens_used += trunc_tokens
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Defer to signature phase
|
|
169
|
+
remaining_files.append(file_path)
|
|
170
|
+
|
|
171
|
+
# Phase 2: Signature-only summaries for remaining files
|
|
172
|
+
sig_tokens_used = 0
|
|
173
|
+
for file_path in remaining_files:
|
|
174
|
+
sig_text = self._extract_signatures(file_path, builder, index)
|
|
175
|
+
if not sig_text:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
sig_tokens = self.estimate_tokens(sig_text)
|
|
179
|
+
if sig_tokens_used + sig_tokens > sig_budget:
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
results.append((file_path, f"(signatures only)\n{sig_text}"))
|
|
183
|
+
sig_tokens_used += sig_tokens
|
|
184
|
+
|
|
185
|
+
return results
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def estimate_tokens(text: str) -> int:
|
|
189
|
+
"""Estimate token count with code/comment-aware heuristic.
|
|
190
|
+
|
|
191
|
+
Code lines average ~3.5 chars/token due to variable names and syntax.
|
|
192
|
+
Comment/docstring lines average ~4.5 chars/token (more natural language).
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
text: Input text.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Estimated number of tokens.
|
|
199
|
+
"""
|
|
200
|
+
if not text:
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
total = 0
|
|
204
|
+
for line in text.splitlines():
|
|
205
|
+
stripped = line.lstrip()
|
|
206
|
+
length = len(line)
|
|
207
|
+
if not stripped:
|
|
208
|
+
total += 1 # blank line ≈ 1 token
|
|
209
|
+
elif stripped.startswith(_COMMENT_PREFIXES):
|
|
210
|
+
total += max(1, int(length / 4.5))
|
|
211
|
+
else:
|
|
212
|
+
total += max(1, int(length / 3.5))
|
|
213
|
+
return total
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _relevance_threshold(top_score: float) -> float:
|
|
217
|
+
"""Compute the minimum relevance score to include a file.
|
|
218
|
+
|
|
219
|
+
Files below this threshold are considered noise.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
top_score: The highest file relevance score.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Threshold value (at least 1.0).
|
|
226
|
+
"""
|
|
227
|
+
return max(top_score * 0.10, 1.0)
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _extract_signatures(file_path: str, builder: IndexBuilder, index: CodeIndex) -> str:
|
|
231
|
+
"""Extract function/class signatures for a file as a compact summary.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
file_path: Relative file path.
|
|
235
|
+
builder: IndexBuilder instance.
|
|
236
|
+
index: Pre-loaded code index.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Formatted string of signatures, or empty string if none found.
|
|
240
|
+
"""
|
|
241
|
+
symbols = builder.get_file_symbols(file_path, index)
|
|
242
|
+
if not symbols:
|
|
243
|
+
return ""
|
|
244
|
+
|
|
245
|
+
parts: list[str] = []
|
|
246
|
+
for sym in symbols:
|
|
247
|
+
if sym.kind == "import":
|
|
248
|
+
continue
|
|
249
|
+
parts.append(sym.signature)
|
|
250
|
+
if sym.docstring:
|
|
251
|
+
parts.append(f" {sym.docstring}")
|
|
252
|
+
return "\n".join(parts)
|
nex/exceptions.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Nex AI exception hierarchy.
|
|
2
|
+
|
|
3
|
+
All exceptions inherit from NexError so callers can catch the base
|
|
4
|
+
class when they want to handle any Nex-specific failure uniformly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NexError(Exception):
|
|
11
|
+
"""Base exception for all Nex errors."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigError(NexError):
|
|
15
|
+
"""Configuration-related errors (missing API key, invalid config, etc.)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class APIError(NexError):
|
|
19
|
+
"""Errors communicating with the Anthropic API."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ToolError(NexError):
|
|
27
|
+
"""Errors during tool execution (file ops, shell commands, etc.)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SafetyError(NexError):
|
|
31
|
+
"""Safety layer violations (destructive commands, path traversal, etc.)."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class IndexerError(NexError):
|
|
35
|
+
"""Errors during codebase indexing or AST parsing."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NexMemoryError(NexError):
|
|
39
|
+
"""Errors in the memory system (project memory, error DB, decision log)."""
|
nex/indexer/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Codebase indexer — file discovery, AST parsing, and index building."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from nex.indexer.index import CodeIndex, IndexBuilder
|
|
6
|
+
from nex.indexer.parser import ASTParser, Symbol
|
|
7
|
+
from nex.indexer.scanner import FileInfo, FileScanner
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ASTParser",
|
|
11
|
+
"CodeIndex",
|
|
12
|
+
"FileInfo",
|
|
13
|
+
"FileScanner",
|
|
14
|
+
"IndexBuilder",
|
|
15
|
+
"Symbol",
|
|
16
|
+
]
|