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 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,2 @@
1
+ """OADSON Terminal — AI coding agent for your local shell."""
2
+ __version__ = "1.0.1"
@@ -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