oadson 1.0.1__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.
- oadson-1.0.1/PKG-INFO +118 -0
- oadson-1.0.1/README.md +92 -0
- oadson-1.0.1/oadson/__init__.py +2 -0
- oadson-1.0.1/oadson/agent.py +245 -0
- oadson-1.0.1/oadson/cli.py +437 -0
- oadson-1.0.1/oadson/config.py +73 -0
- oadson-1.0.1/oadson/context.py +144 -0
- oadson-1.0.1/oadson/executor.py +367 -0
- oadson-1.0.1/oadson.egg-info/PKG-INFO +118 -0
- oadson-1.0.1/oadson.egg-info/SOURCES.txt +14 -0
- oadson-1.0.1/oadson.egg-info/dependency_links.txt +1 -0
- oadson-1.0.1/oadson.egg-info/entry_points.txt +2 -0
- oadson-1.0.1/oadson.egg-info/requires.txt +4 -0
- oadson-1.0.1/oadson.egg-info/top_level.txt +1 -0
- oadson-1.0.1/pyproject.toml +44 -0
- oadson-1.0.1/setup.cfg +4 -0
oadson-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oadson
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: OADSON Terminal — AI coding agent for your local shell
|
|
5
|
+
Author-email: Goodness Olayinka <goodnessolayinka2@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/goodnessolayinka/Oadson_v2
|
|
8
|
+
Project-URL: Repository, https://github.com/goodnessolayinka/Oadson_v2
|
|
9
|
+
Keywords: ai,cli,coding-agent,terminal,oadson
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Terminals
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: rich>=13
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
26
|
+
|
|
27
|
+
# oadson
|
|
28
|
+
|
|
29
|
+
**OADSON Terminal** — AI coding agent for your local shell.
|
|
30
|
+
|
|
31
|
+
Powered by your Railway OADSON V2 backend.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install oadson
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
oadson setup
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Enter your Railway backend URL and API token when prompted.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### One-shot
|
|
54
|
+
```bash
|
|
55
|
+
oadson "fix the import error in main.py"
|
|
56
|
+
oadson "write a README for this project"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Interactive REPL
|
|
60
|
+
```bash
|
|
61
|
+
oadson
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Pipe mode
|
|
65
|
+
```bash
|
|
66
|
+
cat error.log | oadson
|
|
67
|
+
cat file.py | oadson "refactor this"
|
|
68
|
+
python main.py 2>&1 | oadson "what's wrong"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Session management
|
|
72
|
+
```bash
|
|
73
|
+
oadson session new "auth refactor"
|
|
74
|
+
oadson session list
|
|
75
|
+
oadson session use 2
|
|
76
|
+
oadson session delete 1
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## REPL commands
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|---------|-------------|
|
|
85
|
+
| `/new [name]` | Start fresh session |
|
|
86
|
+
| `/sessions` | List all sessions |
|
|
87
|
+
| `/delete N` | Delete session #N |
|
|
88
|
+
| `/clear` | Clear screen |
|
|
89
|
+
| `/help` | Show help |
|
|
90
|
+
| `/exit` | Quit |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Permission system
|
|
95
|
+
|
|
96
|
+
OADSON always shows what it's about to do before doing it.
|
|
97
|
+
|
|
98
|
+
| Action | Behaviour |
|
|
99
|
+
|--------|-----------|
|
|
100
|
+
| Read files, git status, ls | Auto — no prompt |
|
|
101
|
+
| Write/create files | Shows diff → asks y/n |
|
|
102
|
+
| Delete files | Auto-backup → asks y/n |
|
|
103
|
+
| Run bash commands | Shows command → asks y/n |
|
|
104
|
+
| Destructive patterns (`rm -rf /`) | Blocked always |
|
|
105
|
+
|
|
106
|
+
Customize permissions in `~/.oadson/config.json`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Config location
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
~/.oadson/
|
|
114
|
+
config.json ← backend URL, token, permissions
|
|
115
|
+
sessions.json ← session history
|
|
116
|
+
history ← REPL input history
|
|
117
|
+
backups/ ← auto-backups before delete/overwrite
|
|
118
|
+
```
|
oadson-1.0.1/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# oadson
|
|
2
|
+
|
|
3
|
+
**OADSON Terminal** — AI coding agent for your local shell.
|
|
4
|
+
|
|
5
|
+
Powered by your Railway OADSON V2 backend.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install oadson
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
oadson setup
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Enter your Railway backend URL and API token when prompted.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### One-shot
|
|
28
|
+
```bash
|
|
29
|
+
oadson "fix the import error in main.py"
|
|
30
|
+
oadson "write a README for this project"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Interactive REPL
|
|
34
|
+
```bash
|
|
35
|
+
oadson
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Pipe mode
|
|
39
|
+
```bash
|
|
40
|
+
cat error.log | oadson
|
|
41
|
+
cat file.py | oadson "refactor this"
|
|
42
|
+
python main.py 2>&1 | oadson "what's wrong"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Session management
|
|
46
|
+
```bash
|
|
47
|
+
oadson session new "auth refactor"
|
|
48
|
+
oadson session list
|
|
49
|
+
oadson session use 2
|
|
50
|
+
oadson session delete 1
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## REPL commands
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
|---------|-------------|
|
|
59
|
+
| `/new [name]` | Start fresh session |
|
|
60
|
+
| `/sessions` | List all sessions |
|
|
61
|
+
| `/delete N` | Delete session #N |
|
|
62
|
+
| `/clear` | Clear screen |
|
|
63
|
+
| `/help` | Show help |
|
|
64
|
+
| `/exit` | Quit |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Permission system
|
|
69
|
+
|
|
70
|
+
OADSON always shows what it's about to do before doing it.
|
|
71
|
+
|
|
72
|
+
| Action | Behaviour |
|
|
73
|
+
|--------|-----------|
|
|
74
|
+
| Read files, git status, ls | Auto — no prompt |
|
|
75
|
+
| Write/create files | Shows diff → asks y/n |
|
|
76
|
+
| Delete files | Auto-backup → asks y/n |
|
|
77
|
+
| Run bash commands | Shows command → asks y/n |
|
|
78
|
+
| Destructive patterns (`rm -rf /`) | Blocked always |
|
|
79
|
+
|
|
80
|
+
Customize permissions in `~/.oadson/config.json`.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Config location
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
~/.oadson/
|
|
88
|
+
config.json ← backend URL, token, permissions
|
|
89
|
+
sessions.json ← session history
|
|
90
|
+
history ← REPL input history
|
|
91
|
+
backups/ ← auto-backups before delete/overwrite
|
|
92
|
+
```
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent.py — Local agent loop.
|
|
3
|
+
|
|
4
|
+
Sends your message + shell context to Railway.
|
|
5
|
+
Railway's AI responds with text and/or tool calls.
|
|
6
|
+
Tool calls are executed locally here, results fed back.
|
|
7
|
+
Loop repeats until AI has no more tool calls.
|
|
8
|
+
|
|
9
|
+
This is the core of OADSON terminal — same pattern as Claude Code.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.spinner import Spinner
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
from oadson.config import get_backend_url, get_token
|
|
21
|
+
from oadson.context import collect, format_for_prompt
|
|
22
|
+
from oadson.executor import execute_tool
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
MAX_ITERATIONS = 10 # safety cap on tool call loops
|
|
27
|
+
REQUEST_TIMEOUT = 90 # seconds — generous for Nigeria network
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NetworkError(Exception):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _headers() -> dict:
|
|
35
|
+
return {
|
|
36
|
+
"Authorization": f"Bearer {get_token()}",
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"X-OADSON-Surface": "terminal",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_payload(
|
|
43
|
+
message: str,
|
|
44
|
+
session_id: str,
|
|
45
|
+
context: dict,
|
|
46
|
+
tool_results: Optional[list] = None,
|
|
47
|
+
) -> dict:
|
|
48
|
+
payload = {
|
|
49
|
+
"message": message,
|
|
50
|
+
"session_id": session_id,
|
|
51
|
+
"surface": "terminal",
|
|
52
|
+
"context": format_for_prompt(context),
|
|
53
|
+
}
|
|
54
|
+
if tool_results:
|
|
55
|
+
payload["tool_results"] = tool_results
|
|
56
|
+
return payload
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run(
|
|
60
|
+
message: str,
|
|
61
|
+
session_id: str,
|
|
62
|
+
last_exit_code: int = 0,
|
|
63
|
+
last_command: str = "",
|
|
64
|
+
stream: bool = True,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Main agent loop. Call this for every user message.
|
|
68
|
+
|
|
69
|
+
Returns the final text reply from the AI.
|
|
70
|
+
Exits cleanly on network error — no crash, just a message.
|
|
71
|
+
"""
|
|
72
|
+
backend = get_backend_url()
|
|
73
|
+
if not backend:
|
|
74
|
+
return "[bold red]⚠ Not configured.[/bold red] Run: oadson setup"
|
|
75
|
+
|
|
76
|
+
# Collect shell context
|
|
77
|
+
ctx = collect(last_exit_code=last_exit_code, last_command=last_command)
|
|
78
|
+
|
|
79
|
+
tool_results = []
|
|
80
|
+
final_reply = ""
|
|
81
|
+
iterations = 0
|
|
82
|
+
|
|
83
|
+
while iterations < MAX_ITERATIONS:
|
|
84
|
+
iterations += 1
|
|
85
|
+
payload = _build_payload(message, session_id, ctx, tool_results or None)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
if stream:
|
|
89
|
+
final_reply = _stream_request(backend, payload)
|
|
90
|
+
else:
|
|
91
|
+
final_reply = _plain_request(backend, payload)
|
|
92
|
+
|
|
93
|
+
except NetworkError as e:
|
|
94
|
+
return f"[bold red]⚠ No network:[/bold red] {e}\nYour session is safe — retry when connected."
|
|
95
|
+
except Exception as e:
|
|
96
|
+
return f"[bold red]⚠ Error:[/bold red] {e}"
|
|
97
|
+
|
|
98
|
+
# Parse tool calls from response
|
|
99
|
+
calls = _extract_tool_calls(final_reply)
|
|
100
|
+
if not calls:
|
|
101
|
+
break # AI is done — no more tools needed
|
|
102
|
+
|
|
103
|
+
# Execute each tool locally
|
|
104
|
+
tool_results = []
|
|
105
|
+
for call in calls:
|
|
106
|
+
tool_name = call.get("tool")
|
|
107
|
+
args = call.get("args", {})
|
|
108
|
+
|
|
109
|
+
console.print(f"\n[dim]→ Tool: {tool_name}[/dim]")
|
|
110
|
+
result = execute_tool(tool_name, args)
|
|
111
|
+
|
|
112
|
+
tool_results.append({
|
|
113
|
+
"tool": tool_name,
|
|
114
|
+
"args": args,
|
|
115
|
+
"result": result,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# Show result summary
|
|
119
|
+
if result.get("status") == "success":
|
|
120
|
+
console.print(f"[green]✓ {tool_name}[/green]")
|
|
121
|
+
elif result.get("status") == "cancelled":
|
|
122
|
+
console.print(f"[yellow]↩ {tool_name} cancelled[/yellow]")
|
|
123
|
+
else:
|
|
124
|
+
console.print(f"[red]✗ {tool_name}: {result.get('reason', 'failed')}[/red]")
|
|
125
|
+
|
|
126
|
+
# Feed results back for next iteration
|
|
127
|
+
message = "" # subsequent turns are tool-result driven
|
|
128
|
+
|
|
129
|
+
return final_reply
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _stream_request(backend: str, payload: dict) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Calls /api/v2/chat/stream and parses the SSE response.
|
|
135
|
+
Backend sends a single "done" event with the full reply:
|
|
136
|
+
event: done
|
|
137
|
+
data: {"reply": "...", "model": "...", "steps": [...]}
|
|
138
|
+
"""
|
|
139
|
+
url = f"{backend}/api/v2/chat/stream"
|
|
140
|
+
full_text = ""
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
|
|
144
|
+
with client.stream("POST", url, json=payload, headers=_headers()) as resp:
|
|
145
|
+
if resp.status_code == 401:
|
|
146
|
+
raise NetworkError("Invalid token — run: oadson setup")
|
|
147
|
+
if resp.status_code == 404:
|
|
148
|
+
return _plain_request(backend, payload)
|
|
149
|
+
if resp.status_code != 200:
|
|
150
|
+
raise NetworkError(f"HTTP {resp.status_code}")
|
|
151
|
+
|
|
152
|
+
for chunk in resp.iter_text():
|
|
153
|
+
for line in chunk.splitlines():
|
|
154
|
+
line = line.strip()
|
|
155
|
+
if not line or line.startswith("event:"):
|
|
156
|
+
continue
|
|
157
|
+
if line.startswith("data:"):
|
|
158
|
+
data_str = line[5:].strip()
|
|
159
|
+
if data_str == "[DONE]":
|
|
160
|
+
break
|
|
161
|
+
try:
|
|
162
|
+
data = json.loads(data_str)
|
|
163
|
+
text = (
|
|
164
|
+
data.get("reply") or
|
|
165
|
+
data.get("text") or
|
|
166
|
+
data.get("token") or
|
|
167
|
+
data.get("content") or
|
|
168
|
+
data.get("response") or ""
|
|
169
|
+
)
|
|
170
|
+
if text:
|
|
171
|
+
full_text += text
|
|
172
|
+
console.print(text, end="", highlight=False)
|
|
173
|
+
except json.JSONDecodeError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
except httpx.ConnectError as e:
|
|
177
|
+
raise NetworkError(f"Cannot reach {backend} — {e}")
|
|
178
|
+
except httpx.TimeoutException:
|
|
179
|
+
raise NetworkError(f"Request timed out after {REQUEST_TIMEOUT}s")
|
|
180
|
+
|
|
181
|
+
return full_text
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _plain_request(backend: str, payload: dict) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Non-streaming fallback — shows spinner, returns full reply.
|
|
187
|
+
"""
|
|
188
|
+
url = f"{backend}/api/v2/chat"
|
|
189
|
+
|
|
190
|
+
with Live(Spinner("dots", text="OADSON is thinking..."), console=console, refresh_per_second=10):
|
|
191
|
+
try:
|
|
192
|
+
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
|
|
193
|
+
resp = client.post(url, json=payload, headers=_headers())
|
|
194
|
+
except httpx.ConnectError as e:
|
|
195
|
+
raise NetworkError(f"Cannot reach {backend} — {e}")
|
|
196
|
+
except httpx.TimeoutException:
|
|
197
|
+
raise NetworkError(f"Request timed out after {REQUEST_TIMEOUT}s")
|
|
198
|
+
|
|
199
|
+
if resp.status_code == 401:
|
|
200
|
+
raise NetworkError("Invalid token — run: oadson setup")
|
|
201
|
+
if resp.status_code != 200:
|
|
202
|
+
raise NetworkError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
203
|
+
|
|
204
|
+
data = resp.json()
|
|
205
|
+
return (
|
|
206
|
+
data.get("response") or
|
|
207
|
+
data.get("text") or
|
|
208
|
+
data.get("message") or
|
|
209
|
+
str(data)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _extract_tool_calls(text: str) -> list:
|
|
214
|
+
"""
|
|
215
|
+
Parse tool calls from AI response.
|
|
216
|
+
Railway returns them as JSON blocks: ```tool_call\n{...}\n```
|
|
217
|
+
or as a JSON array in the response body.
|
|
218
|
+
"""
|
|
219
|
+
calls = []
|
|
220
|
+
|
|
221
|
+
# Format 1: ```tool_call\n{"tool": ..., "args": ...}\n```
|
|
222
|
+
import re
|
|
223
|
+
blocks = re.findall(r"```tool_call\s*\n([\s\S]*?)\n```", text)
|
|
224
|
+
for block in blocks:
|
|
225
|
+
try:
|
|
226
|
+
call = json.loads(block.strip())
|
|
227
|
+
if isinstance(call, list):
|
|
228
|
+
calls.extend(call)
|
|
229
|
+
elif isinstance(call, dict) and "tool" in call:
|
|
230
|
+
calls.append(call)
|
|
231
|
+
except json.JSONDecodeError:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# Format 2: JSON array anywhere in text
|
|
235
|
+
if not calls:
|
|
236
|
+
arr = re.findall(r"\[\s*\{[^]]*\"tool\"[^]]*\}\s*\]", text, re.DOTALL)
|
|
237
|
+
for match in arr:
|
|
238
|
+
try:
|
|
239
|
+
parsed = json.loads(match)
|
|
240
|
+
if isinstance(parsed, list):
|
|
241
|
+
calls.extend(parsed)
|
|
242
|
+
except json.JSONDecodeError:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
return calls
|