neuroncli 2.3.0__tar.gz → 3.0.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.
- {neuroncli-2.3.0 → neuroncli-3.0.0}/PKG-INFO +3 -3
- neuroncli-3.0.0/neuroncli/bin/neuron.exe +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/cli.py +43 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/openrouter_client.py +231 -220
- neuroncli-3.0.0/neuroncli/repo_map.py +212 -0
- neuroncli-3.0.0/neuroncli/validators.py +169 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli.egg-info/PKG-INFO +3 -3
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli.egg-info/SOURCES.txt +3 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/pyproject.toml +6 -3
- {neuroncli-2.3.0 → neuroncli-3.0.0}/README.md +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/__init__.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/__main__.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/agent.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/auth.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/config.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/git_integration.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/ollama_client.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/prompts.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/provider.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/session.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/tools.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli/ui.py +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli.egg-info/dependency_links.txt +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli.egg-info/entry_points.txt +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli.egg-info/requires.txt +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/neuroncli.egg-info/top_level.txt +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/setup.cfg +0 -0
- {neuroncli-2.3.0 → neuroncli-3.0.0}/tests/test_agent.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: neuroncli
|
|
3
|
-
Version:
|
|
4
|
-
Summary: AI Coding Agent — Free Claude Code alternative
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: AI Coding Agent — Free Claude Code alternative. Native Rust binary with plan mode, shell escapes, and Azure AI + OpenRouter support.
|
|
5
5
|
Author-email: Rahul S <rahultech72216@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://zero-x.live
|
|
8
8
|
Project-URL: Repository, https://github.com/RAHUL-DevelopeRR/neuroncli
|
|
9
9
|
Project-URL: Issues, https://github.com/RAHUL-DevelopeRR/neuroncli/issues
|
|
10
|
-
Keywords: ai,coding,agent,cli,llm,
|
|
10
|
+
Keywords: ai,coding,agent,cli,llm,claude-code,rust,code-assistant,neuron
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Environment :: Console
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
Binary file
|
|
@@ -353,6 +353,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
353
353
|
except Exception:
|
|
354
354
|
pass
|
|
355
355
|
|
|
356
|
+
# ── Try native Rust binary first ─────────────────────────
|
|
357
|
+
|
|
358
|
+
native = _find_native_binary()
|
|
359
|
+
if native:
|
|
360
|
+
import subprocess
|
|
361
|
+
args_list = argv if argv is not None else sys.argv[1:]
|
|
362
|
+
try:
|
|
363
|
+
result = subprocess.run([native] + args_list)
|
|
364
|
+
return result.returncode
|
|
365
|
+
except (OSError, FileNotFoundError):
|
|
366
|
+
pass # Fall through to Python implementation
|
|
367
|
+
|
|
368
|
+
# ── Python fallback ──────────────────────────────────────
|
|
356
369
|
parser = build_parser()
|
|
357
370
|
args = parser.parse_args(argv)
|
|
358
371
|
|
|
@@ -405,5 +418,35 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
405
418
|
return 0
|
|
406
419
|
|
|
407
420
|
|
|
421
|
+
def _find_native_binary() -> str | None:
|
|
422
|
+
"""Locate the native Rust binary — bundled in package or in PATH."""
|
|
423
|
+
import shutil
|
|
424
|
+
|
|
425
|
+
# Check bundled binary in package
|
|
426
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
427
|
+
ext = ".exe" if sys.platform == "win32" else ""
|
|
428
|
+
bundled = os.path.join(pkg_dir, "bin", f"neuron{ext}")
|
|
429
|
+
if os.path.isfile(bundled) and os.access(bundled, os.X_OK):
|
|
430
|
+
return bundled
|
|
431
|
+
|
|
432
|
+
# Check PATH (but not ourselves — avoid infinite recursion)
|
|
433
|
+
path_binary = shutil.which(f"neuron{ext}")
|
|
434
|
+
if path_binary:
|
|
435
|
+
# Verify it's the Rust binary by checking --version output
|
|
436
|
+
import subprocess
|
|
437
|
+
try:
|
|
438
|
+
result = subprocess.run(
|
|
439
|
+
[path_binary, "--version"],
|
|
440
|
+
capture_output=True, text=True, timeout=3
|
|
441
|
+
)
|
|
442
|
+
if "NeuronCLI" in result.stdout:
|
|
443
|
+
return path_binary
|
|
444
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
|
|
408
450
|
if __name__ == "__main__":
|
|
409
451
|
raise SystemExit(main())
|
|
452
|
+
|
|
@@ -1,220 +1,231 @@
|
|
|
1
|
-
"""NeuronCLI — OpenRouter HTTP streaming client. Zero external dependencies.
|
|
2
|
-
|
|
3
|
-
Supports reasoning models (Kimi K2.5) where the response has both
|
|
4
|
-
a `reasoning` field (chain-of-thought) and a `content` field (final answer).
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import urllib.error
|
|
11
|
-
import urllib.request
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from typing import Generator
|
|
14
|
-
|
|
15
|
-
from .config import AgentConfig
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class OpenRouterConnectionError(Exception):
|
|
19
|
-
"""Raised when the OpenRouter API is unreachable or returns an error."""
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass(frozen=True)
|
|
24
|
-
class ChatMessage:
|
|
25
|
-
"""A single message in the conversation."""
|
|
26
|
-
role: str # "system", "user", "assistant"
|
|
27
|
-
content: str
|
|
28
|
-
|
|
29
|
-
def to_dict(self) -> dict[str, str]:
|
|
30
|
-
return {"role": self.role, "content": self.content}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class OpenRouterClient:
|
|
34
|
-
"""
|
|
35
|
-
HTTP client for OpenRouter's OpenAI-compatible /api/v1/chat/completions.
|
|
36
|
-
Uses only stdlib — no pip dependencies needed.
|
|
37
|
-
|
|
38
|
-
Handles reasoning models (like Kimi K2.5) that emit:
|
|
39
|
-
- `delta.reasoning` tokens (thinking, shown dimmed)
|
|
40
|
-
- `delta.content` tokens (final answer)
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
API_BASE = "https://openrouter.ai/api/v1"
|
|
44
|
-
|
|
45
|
-
def __init__(self, config: AgentConfig | None = None):
|
|
46
|
-
self.config = config or AgentConfig.from_env()
|
|
47
|
-
|
|
48
|
-
def health_check(self) -> bool:
|
|
49
|
-
"""Check if OpenRouter API is reachable and the API key is valid."""
|
|
50
|
-
try:
|
|
51
|
-
req = urllib.request.Request(
|
|
52
|
-
f"{self.API_BASE}/models",
|
|
53
|
-
headers=self._headers(),
|
|
54
|
-
)
|
|
55
|
-
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
56
|
-
return resp.status == 200
|
|
57
|
-
except (urllib.error.URLError, OSError):
|
|
58
|
-
return False
|
|
59
|
-
|
|
60
|
-
def list_models(self) -> list[str]:
|
|
61
|
-
"""List coding-relevant models from OpenRouter."""
|
|
62
|
-
try:
|
|
63
|
-
req = urllib.request.Request(
|
|
64
|
-
f"{self.API_BASE}/models",
|
|
65
|
-
headers=self._headers(),
|
|
66
|
-
)
|
|
67
|
-
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
68
|
-
data = json.loads(resp.read().decode())
|
|
69
|
-
models = data.get("data", [])
|
|
70
|
-
keywords = ("kimi", "deepseek", "qwen", "codestral", "claude", "gemini")
|
|
71
|
-
names = [m["id"] for m in models
|
|
72
|
-
if any(k in m["id"].lower() for k in keywords)]
|
|
73
|
-
return sorted(names)[:25]
|
|
74
|
-
except (urllib.error.URLError, OSError):
|
|
75
|
-
return []
|
|
76
|
-
|
|
77
|
-
def chat(self, messages: list[ChatMessage], model: str | None = None) -> str:
|
|
78
|
-
"""Send a chat request and return the full response text."""
|
|
79
|
-
payload = self._build_payload(messages, model, stream=False)
|
|
80
|
-
data = self._post(payload)
|
|
81
|
-
choice = data.get("choices", [{}])[0]
|
|
82
|
-
msg = choice.get("message", {})
|
|
83
|
-
# Kimi K2.5 reasoning models: content may be in `content` or `reasoning`
|
|
84
|
-
content = msg.get("content") or ""
|
|
85
|
-
if not content:
|
|
86
|
-
content = msg.get("reasoning", "")
|
|
87
|
-
return content
|
|
88
|
-
|
|
89
|
-
def chat_stream(
|
|
90
|
-
self,
|
|
91
|
-
messages: list[ChatMessage],
|
|
92
|
-
model: str | None = None,
|
|
93
|
-
) -> Generator[str, None, None]:
|
|
94
|
-
"""
|
|
95
|
-
Stream a chat response token by token.
|
|
96
|
-
|
|
97
|
-
For reasoning models (Kimi K2.5), yields:
|
|
98
|
-
- reasoning tokens with a dim prefix (hidden thinking)
|
|
99
|
-
- content tokens normally (the actual response)
|
|
100
|
-
"""
|
|
101
|
-
payload = self._build_payload(messages, model, stream=True)
|
|
102
|
-
body = json.dumps(payload).encode("utf-8")
|
|
103
|
-
req = urllib.request.Request(
|
|
104
|
-
f"{self.API_BASE}/chat/completions",
|
|
105
|
-
data=body,
|
|
106
|
-
headers=self._headers(json_body=True),
|
|
107
|
-
method="POST",
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
resp = urllib.request.urlopen(req, timeout=300)
|
|
112
|
-
except urllib.error.HTTPError as exc:
|
|
113
|
-
error_body = exc.read().decode("utf-8", errors="replace")
|
|
114
|
-
try:
|
|
115
|
-
err_data = json.loads(error_body)
|
|
116
|
-
msg = err_data.get("error", {}).get("message", error_body)
|
|
117
|
-
except json.JSONDecodeError:
|
|
118
|
-
msg = error_body
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
except urllib.error.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
1
|
+
"""NeuronCLI — OpenRouter HTTP streaming client. Zero external dependencies.
|
|
2
|
+
|
|
3
|
+
Supports reasoning models (Kimi K2.5) where the response has both
|
|
4
|
+
a `reasoning` field (chain-of-thought) and a `content` field (final answer).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Generator
|
|
14
|
+
|
|
15
|
+
from .config import AgentConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OpenRouterConnectionError(Exception):
|
|
19
|
+
"""Raised when the OpenRouter API is unreachable or returns an error."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ChatMessage:
|
|
25
|
+
"""A single message in the conversation."""
|
|
26
|
+
role: str # "system", "user", "assistant"
|
|
27
|
+
content: str
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, str]:
|
|
30
|
+
return {"role": self.role, "content": self.content}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OpenRouterClient:
|
|
34
|
+
"""
|
|
35
|
+
HTTP client for OpenRouter's OpenAI-compatible /api/v1/chat/completions.
|
|
36
|
+
Uses only stdlib — no pip dependencies needed.
|
|
37
|
+
|
|
38
|
+
Handles reasoning models (like Kimi K2.5) that emit:
|
|
39
|
+
- `delta.reasoning` tokens (thinking, shown dimmed)
|
|
40
|
+
- `delta.content` tokens (final answer)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
API_BASE = "https://openrouter.ai/api/v1"
|
|
44
|
+
|
|
45
|
+
def __init__(self, config: AgentConfig | None = None):
|
|
46
|
+
self.config = config or AgentConfig.from_env()
|
|
47
|
+
|
|
48
|
+
def health_check(self) -> bool:
|
|
49
|
+
"""Check if OpenRouter API is reachable and the API key is valid."""
|
|
50
|
+
try:
|
|
51
|
+
req = urllib.request.Request(
|
|
52
|
+
f"{self.API_BASE}/models",
|
|
53
|
+
headers=self._headers(),
|
|
54
|
+
)
|
|
55
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
56
|
+
return resp.status == 200
|
|
57
|
+
except (urllib.error.URLError, OSError):
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def list_models(self) -> list[str]:
|
|
61
|
+
"""List coding-relevant models from OpenRouter."""
|
|
62
|
+
try:
|
|
63
|
+
req = urllib.request.Request(
|
|
64
|
+
f"{self.API_BASE}/models",
|
|
65
|
+
headers=self._headers(),
|
|
66
|
+
)
|
|
67
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
68
|
+
data = json.loads(resp.read().decode())
|
|
69
|
+
models = data.get("data", [])
|
|
70
|
+
keywords = ("kimi", "deepseek", "qwen", "codestral", "claude", "gemini")
|
|
71
|
+
names = [m["id"] for m in models
|
|
72
|
+
if any(k in m["id"].lower() for k in keywords)]
|
|
73
|
+
return sorted(names)[:25]
|
|
74
|
+
except (urllib.error.URLError, OSError):
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
def chat(self, messages: list[ChatMessage], model: str | None = None) -> str:
|
|
78
|
+
"""Send a chat request and return the full response text."""
|
|
79
|
+
payload = self._build_payload(messages, model, stream=False)
|
|
80
|
+
data = self._post(payload)
|
|
81
|
+
choice = data.get("choices", [{}])[0]
|
|
82
|
+
msg = choice.get("message", {})
|
|
83
|
+
# Kimi K2.5 reasoning models: content may be in `content` or `reasoning`
|
|
84
|
+
content = msg.get("content") or ""
|
|
85
|
+
if not content:
|
|
86
|
+
content = msg.get("reasoning", "")
|
|
87
|
+
return content
|
|
88
|
+
|
|
89
|
+
def chat_stream(
|
|
90
|
+
self,
|
|
91
|
+
messages: list[ChatMessage],
|
|
92
|
+
model: str | None = None,
|
|
93
|
+
) -> Generator[str, None, None]:
|
|
94
|
+
"""
|
|
95
|
+
Stream a chat response token by token.
|
|
96
|
+
|
|
97
|
+
For reasoning models (Kimi K2.5), yields:
|
|
98
|
+
- reasoning tokens with a dim prefix (hidden thinking)
|
|
99
|
+
- content tokens normally (the actual response)
|
|
100
|
+
"""
|
|
101
|
+
payload = self._build_payload(messages, model, stream=True)
|
|
102
|
+
body = json.dumps(payload).encode("utf-8")
|
|
103
|
+
req = urllib.request.Request(
|
|
104
|
+
f"{self.API_BASE}/chat/completions",
|
|
105
|
+
data=body,
|
|
106
|
+
headers=self._headers(json_body=True),
|
|
107
|
+
method="POST",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
resp = urllib.request.urlopen(req, timeout=300)
|
|
112
|
+
except urllib.error.HTTPError as exc:
|
|
113
|
+
error_body = exc.read().decode("utf-8", errors="replace")
|
|
114
|
+
try:
|
|
115
|
+
err_data = json.loads(error_body)
|
|
116
|
+
msg = err_data.get("error", {}).get("message", error_body)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
msg = error_body
|
|
119
|
+
if exc.code == 402:
|
|
120
|
+
raise OpenRouterConnectionError(
|
|
121
|
+
f"\n [X] OpenRouter: Insufficient credits (402)\n"
|
|
122
|
+
f" {msg}\n\n"
|
|
123
|
+
f" Fix options:\n"
|
|
124
|
+
f" 1. Add $5 credits: https://openrouter.ai/settings/credits\n"
|
|
125
|
+
f" 2. Switch to a free model: neuron --model moonshotai/kimi-k2.5:free\n"
|
|
126
|
+
f" 3. Use Ollama (offline): NEURON_PROVIDER=ollama neuron\n"
|
|
127
|
+
f" 4. Re-auth with a funded account: neuron --reauth\n"
|
|
128
|
+
) from exc
|
|
129
|
+
raise OpenRouterConnectionError(
|
|
130
|
+
f"\n [X] OpenRouter API error ({exc.code})\n"
|
|
131
|
+
f" {msg}"
|
|
132
|
+
) from exc
|
|
133
|
+
except urllib.error.URLError as exc:
|
|
134
|
+
raise OpenRouterConnectionError(
|
|
135
|
+
f"\n [X] Cannot reach OpenRouter API.\n"
|
|
136
|
+
f" Check your internet connection.\n"
|
|
137
|
+
f" Error: {exc}"
|
|
138
|
+
) from exc
|
|
139
|
+
|
|
140
|
+
in_reasoning = False
|
|
141
|
+
try:
|
|
142
|
+
for raw_line in resp:
|
|
143
|
+
line = raw_line.decode("utf-8", errors="replace").strip()
|
|
144
|
+
if not line or line == "data: [DONE]":
|
|
145
|
+
continue
|
|
146
|
+
if not line.startswith("data: "):
|
|
147
|
+
continue
|
|
148
|
+
try:
|
|
149
|
+
chunk = json.loads(line[6:])
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
continue
|
|
152
|
+
choices = chunk.get("choices", [])
|
|
153
|
+
if not choices:
|
|
154
|
+
continue
|
|
155
|
+
delta = choices[0].get("delta", {})
|
|
156
|
+
|
|
157
|
+
# Handle reasoning tokens (Kimi K2.5 chain-of-thought)
|
|
158
|
+
reasoning_token = delta.get("reasoning", "")
|
|
159
|
+
content_token = delta.get("content", "")
|
|
160
|
+
|
|
161
|
+
if reasoning_token:
|
|
162
|
+
if not in_reasoning:
|
|
163
|
+
yield "\033[2m\033[90m" # DIM + GRAY
|
|
164
|
+
in_reasoning = True
|
|
165
|
+
yield reasoning_token
|
|
166
|
+
|
|
167
|
+
if content_token:
|
|
168
|
+
if in_reasoning:
|
|
169
|
+
yield "\033[0m\n" # Reset after reasoning
|
|
170
|
+
in_reasoning = False
|
|
171
|
+
yield content_token
|
|
172
|
+
finally:
|
|
173
|
+
if in_reasoning:
|
|
174
|
+
yield "\033[0m\n"
|
|
175
|
+
resp.close()
|
|
176
|
+
|
|
177
|
+
# ── Internal helpers ──────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
def _headers(self, json_body: bool = False) -> dict[str, str]:
|
|
180
|
+
h: dict[str, str] = {
|
|
181
|
+
"Authorization": f"Bearer {self.config.api_key}",
|
|
182
|
+
"HTTP-Referer": "https://github.com/RAHUL-DevelopeRR/neuroncli",
|
|
183
|
+
"X-Title": "NeuronCLI",
|
|
184
|
+
}
|
|
185
|
+
if json_body:
|
|
186
|
+
h["Content-Type"] = "application/json"
|
|
187
|
+
return h
|
|
188
|
+
|
|
189
|
+
def _build_payload(
|
|
190
|
+
self,
|
|
191
|
+
messages: list[ChatMessage],
|
|
192
|
+
model: str | None,
|
|
193
|
+
stream: bool,
|
|
194
|
+
) -> dict:
|
|
195
|
+
return {
|
|
196
|
+
"model": model or self.config.model,
|
|
197
|
+
"messages": [m.to_dict() for m in messages],
|
|
198
|
+
"stream": stream,
|
|
199
|
+
"max_tokens": self.config.max_tokens,
|
|
200
|
+
"temperature": self.config.temperature,
|
|
201
|
+
"top_p": self.config.top_p,
|
|
202
|
+
"route": "fallback", # Auto-select cheapest available provider
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def _post(self, payload: dict) -> dict:
|
|
206
|
+
body = json.dumps(payload).encode("utf-8")
|
|
207
|
+
req = urllib.request.Request(
|
|
208
|
+
f"{self.API_BASE}/chat/completions",
|
|
209
|
+
data=body,
|
|
210
|
+
headers=self._headers(json_body=True),
|
|
211
|
+
method="POST",
|
|
212
|
+
)
|
|
213
|
+
try:
|
|
214
|
+
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
215
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
216
|
+
except urllib.error.HTTPError as exc:
|
|
217
|
+
error_body = exc.read().decode("utf-8", errors="replace")
|
|
218
|
+
try:
|
|
219
|
+
err_data = json.loads(error_body)
|
|
220
|
+
msg = err_data.get("error", {}).get("message", error_body)
|
|
221
|
+
except json.JSONDecodeError:
|
|
222
|
+
msg = error_body
|
|
223
|
+
raise OpenRouterConnectionError(
|
|
224
|
+
f"\n [X] OpenRouter API error ({exc.code})\n"
|
|
225
|
+
f" {msg}"
|
|
226
|
+
) from exc
|
|
227
|
+
except urllib.error.URLError as exc:
|
|
228
|
+
raise OpenRouterConnectionError(
|
|
229
|
+
f"\n [X] Cannot reach OpenRouter API.\n"
|
|
230
|
+
f" Error: {exc}"
|
|
231
|
+
) from exc
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""NeuronCLI — AST-based semantic repo map.
|
|
2
|
+
|
|
3
|
+
Generates a concise structural overview of the project for the system prompt.
|
|
4
|
+
Replaces the 200-item directory dump with ~500 tokens of meaningful context.
|
|
5
|
+
Inspired by Aider's repo-map concept.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Directories to always skip
|
|
16
|
+
_SKIP_DIRS = {
|
|
17
|
+
".git", "__pycache__", "node_modules", ".venv", "venv",
|
|
18
|
+
".mypy_cache", ".pytest_cache", "dist", "build", ".tox",
|
|
19
|
+
".eggs", ".next", ".nuxt", "coverage", ".coverage",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_repo_map(working_dir: str, max_tokens: int = 500) -> str:
|
|
24
|
+
"""Generate a concise structural overview of the project.
|
|
25
|
+
|
|
26
|
+
Output format:
|
|
27
|
+
src/
|
|
28
|
+
agent.py: class Agent [run, compact, clear_history]
|
|
29
|
+
tools.py: class ToolRegistry [register, execute]; read_file, write_file
|
|
30
|
+
tests/
|
|
31
|
+
test_agent.py: TestAgent [test_run_simple, test_tools]
|
|
32
|
+
"""
|
|
33
|
+
root = Path(working_dir)
|
|
34
|
+
if not root.is_dir():
|
|
35
|
+
return f"[Not a directory: {working_dir}]"
|
|
36
|
+
|
|
37
|
+
lines: list[str] = []
|
|
38
|
+
_walk_dir(root, root, lines, depth=0, max_depth=3)
|
|
39
|
+
|
|
40
|
+
full_map = "\n".join(lines)
|
|
41
|
+
return _smart_truncate(full_map, max_tokens)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _walk_dir(base: Path, current: Path, lines: list[str], depth: int, max_depth: int):
|
|
45
|
+
"""Recursively build the repo map."""
|
|
46
|
+
if depth >= max_depth:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
entries = sorted(current.iterdir(), key=lambda e: (not e.is_dir(), e.name.lower()))
|
|
51
|
+
except (PermissionError, OSError):
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
for entry in entries:
|
|
55
|
+
if entry.name.startswith(".") or entry.name in _SKIP_DIRS:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
rel = entry.relative_to(base)
|
|
59
|
+
indent = " " * depth
|
|
60
|
+
|
|
61
|
+
if entry.is_dir():
|
|
62
|
+
# Count meaningful files in directory
|
|
63
|
+
try:
|
|
64
|
+
child_count = sum(1 for c in entry.iterdir()
|
|
65
|
+
if not c.name.startswith(".") and c.name not in _SKIP_DIRS)
|
|
66
|
+
except (PermissionError, OSError):
|
|
67
|
+
child_count = 0
|
|
68
|
+
|
|
69
|
+
if child_count > 0:
|
|
70
|
+
lines.append(f"{indent}{entry.name}/")
|
|
71
|
+
_walk_dir(base, entry, lines, depth + 1, max_depth)
|
|
72
|
+
|
|
73
|
+
elif entry.is_file():
|
|
74
|
+
symbols = _extract_symbols(entry)
|
|
75
|
+
if symbols:
|
|
76
|
+
lines.append(f"{indent}{entry.name}: {symbols}")
|
|
77
|
+
else:
|
|
78
|
+
lines.append(f"{indent}{entry.name}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _extract_symbols(filepath: Path) -> str:
|
|
82
|
+
"""Extract key symbols from a file based on its type."""
|
|
83
|
+
ext = filepath.suffix.lower()
|
|
84
|
+
|
|
85
|
+
if ext == ".py":
|
|
86
|
+
return _extract_python_symbols(filepath)
|
|
87
|
+
elif ext in (".js", ".ts", ".jsx", ".tsx"):
|
|
88
|
+
return _extract_js_symbols(filepath)
|
|
89
|
+
elif ext == ".go":
|
|
90
|
+
return _extract_go_symbols(filepath)
|
|
91
|
+
|
|
92
|
+
# For other files, return size hint
|
|
93
|
+
try:
|
|
94
|
+
size = filepath.stat().st_size
|
|
95
|
+
if size > 10240:
|
|
96
|
+
return f"({size // 1024}KB)"
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
99
|
+
return ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_python_symbols(filepath: Path) -> str:
|
|
103
|
+
"""Use Python's ast module to extract classes and top-level functions."""
|
|
104
|
+
try:
|
|
105
|
+
source = filepath.read_text(encoding="utf-8", errors="replace")
|
|
106
|
+
tree = ast.parse(source, filename=str(filepath))
|
|
107
|
+
except (SyntaxError, OSError, ValueError):
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
parts = []
|
|
111
|
+
|
|
112
|
+
for node in ast.iter_child_nodes(tree):
|
|
113
|
+
if isinstance(node, ast.ClassDef):
|
|
114
|
+
methods = [n.name for n in ast.iter_child_nodes(node)
|
|
115
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
116
|
+
and not n.name.startswith("_")]
|
|
117
|
+
if methods:
|
|
118
|
+
methods_str = ", ".join(methods[:5])
|
|
119
|
+
if len(methods) > 5:
|
|
120
|
+
methods_str += f" +{len(methods) - 5}"
|
|
121
|
+
parts.append(f"class {node.name} [{methods_str}]")
|
|
122
|
+
else:
|
|
123
|
+
parts.append(f"class {node.name}")
|
|
124
|
+
|
|
125
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
126
|
+
if not node.name.startswith("_"):
|
|
127
|
+
parts.append(node.name + "()")
|
|
128
|
+
|
|
129
|
+
if not parts:
|
|
130
|
+
# Fallback: count lines
|
|
131
|
+
try:
|
|
132
|
+
line_count = len(source.splitlines())
|
|
133
|
+
return f"({line_count} lines)"
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
return "; ".join(parts[:6])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _extract_js_symbols(filepath: Path) -> str:
|
|
141
|
+
"""Basic regex extraction for JS/TS files."""
|
|
142
|
+
import re
|
|
143
|
+
try:
|
|
144
|
+
source = filepath.read_text(encoding="utf-8", errors="replace")
|
|
145
|
+
except OSError:
|
|
146
|
+
return ""
|
|
147
|
+
|
|
148
|
+
parts = []
|
|
149
|
+
|
|
150
|
+
# Classes
|
|
151
|
+
for match in re.finditer(r'(?:export\s+)?class\s+(\w+)', source):
|
|
152
|
+
parts.append(f"class {match.group(1)}")
|
|
153
|
+
# Named exports / functions
|
|
154
|
+
for match in re.finditer(r'(?:export\s+)?(?:async\s+)?function\s+(\w+)', source):
|
|
155
|
+
parts.append(match.group(1) + "()")
|
|
156
|
+
# Arrow function exports
|
|
157
|
+
for match in re.finditer(r'export\s+(?:const|let)\s+(\w+)\s*=', source):
|
|
158
|
+
name = match.group(1)
|
|
159
|
+
if name not in [p.rstrip("()") for p in parts]:
|
|
160
|
+
parts.append(name)
|
|
161
|
+
|
|
162
|
+
return "; ".join(parts[:6])
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _extract_go_symbols(filepath: Path) -> str:
|
|
166
|
+
"""Basic regex extraction for Go files."""
|
|
167
|
+
import re
|
|
168
|
+
try:
|
|
169
|
+
source = filepath.read_text(encoding="utf-8", errors="replace")
|
|
170
|
+
except OSError:
|
|
171
|
+
return ""
|
|
172
|
+
|
|
173
|
+
parts = []
|
|
174
|
+
for match in re.finditer(r'type\s+(\w+)\s+struct', source):
|
|
175
|
+
parts.append(f"type {match.group(1)}")
|
|
176
|
+
for match in re.finditer(r'func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(', source):
|
|
177
|
+
name = match.group(1)
|
|
178
|
+
if name[0].isupper(): # Only exported
|
|
179
|
+
parts.append(name + "()")
|
|
180
|
+
|
|
181
|
+
return "; ".join(parts[:6])
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _smart_truncate(map_text: str, max_tokens: int) -> str:
|
|
185
|
+
"""Progressively reduce detail to fit within token budget.
|
|
186
|
+
|
|
187
|
+
Estimation: 1 token ≈ 4 characters.
|
|
188
|
+
"""
|
|
189
|
+
max_chars = max_tokens * 4
|
|
190
|
+
if len(map_text) <= max_chars:
|
|
191
|
+
return map_text
|
|
192
|
+
|
|
193
|
+
# Phase 1: Remove size hints like (123 lines), (45KB)
|
|
194
|
+
import re
|
|
195
|
+
truncated = re.sub(r'\s*\(\d+\s*(?:lines|KB|B)\)', '', map_text)
|
|
196
|
+
if len(truncated) <= max_chars:
|
|
197
|
+
return truncated
|
|
198
|
+
|
|
199
|
+
# Phase 2: Remove method lists from classes
|
|
200
|
+
truncated = re.sub(r'\s*\[.+?\]', '', truncated)
|
|
201
|
+
if len(truncated) <= max_chars:
|
|
202
|
+
return truncated
|
|
203
|
+
|
|
204
|
+
# Phase 3: Just keep directory structure + filenames
|
|
205
|
+
lines = truncated.split("\n")
|
|
206
|
+
kept = [l for l in lines if l.strip().endswith("/") or "." in l.split("/")[-1]]
|
|
207
|
+
truncated = "\n".join(kept)
|
|
208
|
+
if len(truncated) <= max_chars:
|
|
209
|
+
return truncated
|
|
210
|
+
|
|
211
|
+
# Phase 4: Hard truncate
|
|
212
|
+
return truncated[:max_chars] + "\n... (truncated)"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""NeuronCLI — Post-write file validation engine.
|
|
2
|
+
|
|
3
|
+
Automatically validates files after write/edit operations.
|
|
4
|
+
Feeds errors back into the agent loop for self-correction.
|
|
5
|
+
Inspired by Crush's LSP diagnostics + Aider's lint-fix pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ValidationResult:
|
|
19
|
+
"""Result of validating a generated file."""
|
|
20
|
+
valid: bool
|
|
21
|
+
errors: list[str] = field(default_factory=list)
|
|
22
|
+
warnings: list[str] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def feedback(self) -> str:
|
|
26
|
+
"""Format as feedback string for the agent loop."""
|
|
27
|
+
if self.valid:
|
|
28
|
+
return ""
|
|
29
|
+
parts = []
|
|
30
|
+
for e in self.errors:
|
|
31
|
+
parts.append(f" ERROR: {e}")
|
|
32
|
+
for w in self.warnings:
|
|
33
|
+
parts.append(f" WARNING: {w}")
|
|
34
|
+
return "\n".join(parts)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_file(path: str, content: str | None = None) -> ValidationResult:
|
|
38
|
+
"""Validate a file based on its extension.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
path: File path (used for extension detection)
|
|
42
|
+
content: File content (read from disk if not provided)
|
|
43
|
+
"""
|
|
44
|
+
p = Path(path)
|
|
45
|
+
ext = p.suffix.lower()
|
|
46
|
+
|
|
47
|
+
if content is None:
|
|
48
|
+
try:
|
|
49
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
50
|
+
except OSError as e:
|
|
51
|
+
return ValidationResult(valid=False, errors=[f"Cannot read file: {e}"])
|
|
52
|
+
|
|
53
|
+
validators = {
|
|
54
|
+
".py": _validate_python,
|
|
55
|
+
".json": _validate_json,
|
|
56
|
+
".html": _validate_html,
|
|
57
|
+
".htm": _validate_html,
|
|
58
|
+
".css": _validate_css,
|
|
59
|
+
".js": _validate_javascript,
|
|
60
|
+
".ts": _validate_javascript,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
validator = validators.get(ext)
|
|
64
|
+
if validator is None:
|
|
65
|
+
return ValidationResult(valid=True) # No validator = assume OK
|
|
66
|
+
|
|
67
|
+
return validator(str(p), content)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── Python Validator ──────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
def _validate_python(path: str, content: str) -> ValidationResult:
|
|
73
|
+
"""Validate Python via py_compile (catches syntax errors)."""
|
|
74
|
+
errors = []
|
|
75
|
+
warnings = []
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
compile(content, path, "exec")
|
|
79
|
+
except SyntaxError as e:
|
|
80
|
+
errors.append(f"SyntaxError at line {e.lineno}: {e.msg}")
|
|
81
|
+
|
|
82
|
+
# Check for common mistakes
|
|
83
|
+
if "import *" in content:
|
|
84
|
+
warnings.append("Wildcard import detected (import *)")
|
|
85
|
+
|
|
86
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors, warnings=warnings)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── JSON Validator ────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def _validate_json(path: str, content: str) -> ValidationResult:
|
|
92
|
+
"""Validate JSON structure."""
|
|
93
|
+
try:
|
|
94
|
+
json.loads(content)
|
|
95
|
+
return ValidationResult(valid=True)
|
|
96
|
+
except json.JSONDecodeError as e:
|
|
97
|
+
return ValidationResult(valid=False, errors=[f"Invalid JSON at line {e.lineno}: {e.msg}"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── HTML Validator ────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def _validate_html(path: str, content: str) -> ValidationResult:
|
|
103
|
+
"""Validate HTML structure — checks for complete document."""
|
|
104
|
+
errors = []
|
|
105
|
+
warnings = []
|
|
106
|
+
|
|
107
|
+
# Check for escaped tags (common LLM mistake)
|
|
108
|
+
if "<" in content and "<html" not in content.lower():
|
|
109
|
+
errors.append("HTML tags are escaped (< instead of <). The LLM outputted escaped HTML.")
|
|
110
|
+
|
|
111
|
+
# Check for required structural elements
|
|
112
|
+
lower = content.lower()
|
|
113
|
+
if "<!doctype" not in lower:
|
|
114
|
+
warnings.append("Missing <!DOCTYPE html> declaration")
|
|
115
|
+
if "<html" not in lower:
|
|
116
|
+
errors.append("Missing <html> tag")
|
|
117
|
+
if "<head" not in lower:
|
|
118
|
+
warnings.append("Missing <head> tag")
|
|
119
|
+
if "<body" not in lower:
|
|
120
|
+
errors.append("Missing <body> tag")
|
|
121
|
+
|
|
122
|
+
# Check for unclosed tags (basic check)
|
|
123
|
+
open_tags = len(re.findall(r'<(?!/)(?!meta)(?!br)(?!hr)(?!img)(?!input)(?!link)([a-z]+)', lower))
|
|
124
|
+
close_tags = len(re.findall(r'</([a-z]+)', lower))
|
|
125
|
+
if open_tags > 0 and close_tags == 0:
|
|
126
|
+
errors.append("No closing tags found — file may be truncated")
|
|
127
|
+
|
|
128
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors, warnings=warnings)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── CSS Validator ─────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
def _validate_css(path: str, content: str) -> ValidationResult:
|
|
134
|
+
"""Basic CSS validation — brace matching."""
|
|
135
|
+
errors = []
|
|
136
|
+
|
|
137
|
+
open_braces = content.count("{")
|
|
138
|
+
close_braces = content.count("}")
|
|
139
|
+
if open_braces != close_braces:
|
|
140
|
+
errors.append(f"Unmatched braces: {open_braces} open, {close_braces} close")
|
|
141
|
+
|
|
142
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── JavaScript Validator ──────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def _validate_javascript(path: str, content: str) -> ValidationResult:
|
|
148
|
+
"""Validate JS/TS — try node --check if available, else brace match."""
|
|
149
|
+
errors = []
|
|
150
|
+
|
|
151
|
+
# Try node --check (fast syntax check)
|
|
152
|
+
try:
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
["node", "--check", path],
|
|
155
|
+
capture_output=True, text=True, timeout=5,
|
|
156
|
+
)
|
|
157
|
+
if result.returncode != 0:
|
|
158
|
+
# Extract the actual error line
|
|
159
|
+
stderr = result.stderr.strip()
|
|
160
|
+
if stderr:
|
|
161
|
+
errors.append(stderr.split("\n")[0])
|
|
162
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
163
|
+
# Node not available — fall back to brace matching
|
|
164
|
+
open_braces = content.count("{")
|
|
165
|
+
close_braces = content.count("}")
|
|
166
|
+
if abs(open_braces - close_braces) > 2:
|
|
167
|
+
errors.append(f"Suspicious brace mismatch: {open_braces} open, {close_braces} close")
|
|
168
|
+
|
|
169
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: neuroncli
|
|
3
|
-
Version:
|
|
4
|
-
Summary: AI Coding Agent — Free Claude Code alternative
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: AI Coding Agent — Free Claude Code alternative. Native Rust binary with plan mode, shell escapes, and Azure AI + OpenRouter support.
|
|
5
5
|
Author-email: Rahul S <rahultech72216@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://zero-x.live
|
|
8
8
|
Project-URL: Repository, https://github.com/RAHUL-DevelopeRR/neuroncli
|
|
9
9
|
Project-URL: Issues, https://github.com/RAHUL-DevelopeRR/neuroncli/issues
|
|
10
|
-
Keywords: ai,coding,agent,cli,llm,
|
|
10
|
+
Keywords: ai,coding,agent,cli,llm,claude-code,rust,code-assistant,neuron
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Environment :: Console
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -11,13 +11,16 @@ neuroncli/ollama_client.py
|
|
|
11
11
|
neuroncli/openrouter_client.py
|
|
12
12
|
neuroncli/prompts.py
|
|
13
13
|
neuroncli/provider.py
|
|
14
|
+
neuroncli/repo_map.py
|
|
14
15
|
neuroncli/session.py
|
|
15
16
|
neuroncli/tools.py
|
|
16
17
|
neuroncli/ui.py
|
|
18
|
+
neuroncli/validators.py
|
|
17
19
|
neuroncli.egg-info/PKG-INFO
|
|
18
20
|
neuroncli.egg-info/SOURCES.txt
|
|
19
21
|
neuroncli.egg-info/dependency_links.txt
|
|
20
22
|
neuroncli.egg-info/entry_points.txt
|
|
21
23
|
neuroncli.egg-info/requires.txt
|
|
22
24
|
neuroncli.egg-info/top_level.txt
|
|
25
|
+
neuroncli/bin/neuron.exe
|
|
23
26
|
tests/test_agent.py
|
|
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "neuroncli"
|
|
7
|
-
version = "
|
|
8
|
-
description = "AI Coding Agent — Free Claude Code alternative
|
|
7
|
+
version = "3.0.0"
|
|
8
|
+
description = "AI Coding Agent — Free Claude Code alternative. Native Rust binary with plan mode, shell escapes, and Azure AI + OpenRouter support."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Rahul S", email = "rahultech72216@gmail.com"},
|
|
14
14
|
]
|
|
15
|
-
keywords = ["ai", "coding", "agent", "cli", "llm", "
|
|
15
|
+
keywords = ["ai", "coding", "agent", "cli", "llm", "claude-code", "rust", "code-assistant", "neuron"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
18
18
|
"Environment :: Console",
|
|
@@ -41,5 +41,8 @@ neuron = "neuroncli.cli:main"
|
|
|
41
41
|
[tool.setuptools.packages.find]
|
|
42
42
|
include = ["neuroncli*"]
|
|
43
43
|
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
neuroncli = ["bin/*"]
|
|
46
|
+
|
|
44
47
|
[tool.pytest.ini_options]
|
|
45
48
|
testpaths = ["tests"]
|
|
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
|