nexcoder 0.1.4__tar.gz → 0.1.5__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.
Files changed (45) hide show
  1. {nexcoder-0.1.4 → nexcoder-0.1.5}/PKG-INFO +1 -1
  2. {nexcoder-0.1.4 → nexcoder-0.1.5}/pyproject.toml +1 -1
  3. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/__init__.py +3 -1
  4. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/api_client.py +42 -11
  5. {nexcoder-0.1.4 → nexcoder-0.1.5}/.github/workflows/ci.yml +0 -0
  6. {nexcoder-0.1.4 → nexcoder-0.1.5}/.github/workflows/publish.yml +0 -0
  7. {nexcoder-0.1.4 → nexcoder-0.1.5}/.gitignore +0 -0
  8. {nexcoder-0.1.4 → nexcoder-0.1.5}/CHANGELOG.md +0 -0
  9. {nexcoder-0.1.4 → nexcoder-0.1.5}/LICENSE +0 -0
  10. {nexcoder-0.1.4 → nexcoder-0.1.5}/README.md +0 -0
  11. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/agent.py +0 -0
  12. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/cli.py +0 -0
  13. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/config.py +0 -0
  14. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/context.py +0 -0
  15. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/exceptions.py +0 -0
  16. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/indexer/__init__.py +0 -0
  17. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/indexer/index.py +0 -0
  18. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/indexer/parser.py +0 -0
  19. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/indexer/scanner.py +0 -0
  20. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/memory/__init__.py +0 -0
  21. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/memory/decisions.py +0 -0
  22. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/memory/errors.py +0 -0
  23. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/memory/project.py +0 -0
  24. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/planner.py +0 -0
  25. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/py.typed +0 -0
  26. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/reviewer.py +0 -0
  27. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/safety.py +0 -0
  28. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/test_runner.py +0 -0
  29. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/tools/__init__.py +0 -0
  30. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/tools/file_ops.py +0 -0
  31. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/tools/git_ops.py +0 -0
  32. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/tools/search.py +0 -0
  33. {nexcoder-0.1.4 → nexcoder-0.1.5}/src/nex/tools/shell.py +0 -0
  34. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/conftest.py +0 -0
  35. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_agent.py +0 -0
  36. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_chat.py +0 -0
  37. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_cli.py +0 -0
  38. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_context.py +0 -0
  39. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_errors.py +0 -0
  40. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_indexer.py +0 -0
  41. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_memory.py +0 -0
  42. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_rate_limiter.py +0 -0
  43. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_safety.py +0 -0
  44. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_test_runner.py +0 -0
  45. {nexcoder-0.1.4 → nexcoder-0.1.5}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nexcoder
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: The coding agent that remembers — AI coding assistant with persistent memory and error learning.
5
5
  Project-URL: Homepage, https://github.com/nex-ai/nex-ai
6
6
  Project-URL: Repository, https://github.com/nex-ai/nex-ai
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nexcoder"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "The coding agent that remembers — AI coding assistant with persistent memory and error learning."
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  license = "MIT"
@@ -2,5 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.1.0"
5
+ from importlib.metadata import version
6
+
7
+ __version__ = version("nexcoder")
6
8
  __app_name__ = "nex"
@@ -122,6 +122,30 @@ class RateLimiter:
122
122
  return
123
123
 
124
124
 
125
+ def _extract_retry_after(exc: Exception, default: float = 60.0) -> float:
126
+ """Extract retry-after seconds from an Anthropic API error.
127
+
128
+ Inspects the exception's response headers for a ``retry-after`` value.
129
+ Falls back to *default* if the header is missing or unparseable.
130
+
131
+ Args:
132
+ exc: The exception raised by the Anthropic SDK.
133
+ default: Fallback wait time in seconds.
134
+
135
+ Returns:
136
+ Number of seconds to wait before retrying.
137
+ """
138
+ resp = getattr(exc, "response", None)
139
+ if resp is not None:
140
+ header = getattr(resp, "headers", {}).get("retry-after")
141
+ if header:
142
+ try:
143
+ return max(float(header), 1.0)
144
+ except (ValueError, TypeError):
145
+ pass
146
+ return default
147
+
148
+
125
149
  class AnthropicClient:
126
150
  """Async wrapper around the Anthropic API.
127
151
 
@@ -140,7 +164,7 @@ class AnthropicClient:
140
164
  self,
141
165
  api_key: str,
142
166
  default_model: str = "claude-sonnet-4-20250514",
143
- max_retries: int = 3,
167
+ max_retries: int = 5,
144
168
  ) -> None:
145
169
  """Initialize the Anthropic client.
146
170
 
@@ -196,7 +220,9 @@ class AnthropicClient:
196
220
  kwargs["tools"] = tools
197
221
 
198
222
  last_error: Exception | None = None
199
- for attempt in range(self._max_retries + 1):
223
+ # 429 rate limits need more retries with longer waits than server errors
224
+ max_attempts = self._max_retries + 1
225
+ for attempt in range(max_attempts):
200
226
  try:
201
227
  response = await self._client.messages.create(**kwargs)
202
228
 
@@ -230,16 +256,21 @@ class AnthropicClient:
230
256
  except Exception as exc:
231
257
  last_error = exc
232
258
  status_code = getattr(exc, "status_code", None)
259
+ is_rate_limit = status_code == 429
260
+ is_retryable = status_code in (500, 502, 503, 529)
261
+
262
+ if (is_rate_limit or is_retryable) and attempt < max_attempts - 1:
263
+ if is_rate_limit:
264
+ # Rate limits: extract retry-after from response headers,
265
+ # or default to 60s (the full rate-limit window).
266
+ wait = _extract_retry_after(exc, default=60.0)
267
+ else:
268
+ # Server errors: short exponential backoff
269
+ wait = float(2**attempt)
233
270
 
234
- # Retry on rate limit or server errors
235
- if status_code in (429, 500, 502, 503, 529) and attempt < self._max_retries:
236
- wait = 2**attempt
237
- retry_after = getattr(exc, "retry_after", None)
238
- if retry_after:
239
- wait = max(wait, float(retry_after))
240
271
  console.print(
241
- f"[yellow]API error {status_code}, retrying in {wait}s "
242
- f"(attempt {attempt + 1}/{self._max_retries})...[/yellow]"
272
+ f"[yellow]API error {status_code}, retrying in {wait:.0f}s "
273
+ f"(attempt {attempt + 1}/{max_attempts - 1})...[/yellow]"
243
274
  )
244
275
  await asyncio.sleep(wait)
245
276
  continue
@@ -250,7 +281,7 @@ class AnthropicClient:
250
281
  ) from exc
251
282
 
252
283
  raise APIError(
253
- f"Failed after {self._max_retries} retries: {last_error}",
284
+ f"Failed after {max_attempts - 1} retries: {last_error}",
254
285
  )
255
286
 
256
287
  async def close(self) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes