oadson 1.0.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.
oadson/config.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ OADSON config — reads/writes ~/.oadson/config.json
3
+ """
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ CONFIG_DIR = Path.home() / ".oadson"
9
+ CONFIG_FILE = CONFIG_DIR / "config.json"
10
+
11
+
12
+ def get_config() -> dict:
13
+ if CONFIG_FILE.exists():
14
+ try:
15
+ return json.loads(CONFIG_FILE.read_text())
16
+ except Exception:
17
+ return {}
18
+ return {}
19
+
20
+
21
+ def save_config(cfg: dict):
22
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
23
+ CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
24
+
25
+
26
+ def get_backend_url() -> str:
27
+ return get_config().get("backend_url", "").rstrip("/")
28
+
29
+
30
+ def get_token() -> str:
31
+ return get_config().get("token", "")
32
+
33
+
34
+ def get_permissions() -> dict:
35
+ """
36
+ Permission system for local tool execution.
37
+
38
+ auto_allow — run without prompting
39
+ confirm — show diff/plan, wait for y/n
40
+ always_backup — copy to .oadson_backup/ before touching
41
+ never_allow — block outright (no override)
42
+ """
43
+ defaults = {
44
+ "auto_allow": [
45
+ "read_file", "list_dir", "git_status",
46
+ "git_diff", "search_files", "run_read_only",
47
+ "pwd", "ls", "cat", "find", "grep",
48
+ "head", "tail", "wc", "echo", "which",
49
+ ],
50
+ "confirm": [
51
+ "write_file", "create_file", "run_bash",
52
+ "git_commit", "git_push", "pip_install",
53
+ "npm_install",
54
+ ],
55
+ "always_backup": [
56
+ "delete_file", "overwrite_file",
57
+ ],
58
+ "never_allow": [
59
+ "rm -rf /", "rm -rf ~", ":(){ :|: & };:",
60
+ "dd if=", "mkfs", "format",
61
+ ],
62
+ }
63
+ cfg_perms = get_config().get("permissions", {})
64
+ # Merge user overrides with defaults
65
+ for key in defaults:
66
+ if key in cfg_perms:
67
+ defaults[key] = cfg_perms[key]
68
+ return defaults
69
+
70
+
71
+ def is_configured() -> bool:
72
+ cfg = get_config()
73
+ return bool(cfg.get("backend_url") and cfg.get("token"))
oadson/context.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ context.py — Auto shell context collector.
3
+
4
+ Runs silently on every request. OADSON sees your shell state
5
+ without you having to describe it. Mirrors how Claude Code works —
6
+ it already knows your cwd, git branch, last error before you type.
7
+ """
8
+ import os
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+
13
+ def _run(cmd: str, timeout: int = 5) -> str:
14
+ try:
15
+ r = subprocess.run(
16
+ cmd, shell=True, capture_output=True,
17
+ text=True, timeout=timeout
18
+ )
19
+ return r.stdout.strip()
20
+ except Exception:
21
+ return ""
22
+
23
+
24
+ def _run_err(cmd: str, timeout: int = 5) -> str:
25
+ try:
26
+ r = subprocess.run(
27
+ cmd, shell=True, capture_output=True,
28
+ text=True, timeout=timeout
29
+ )
30
+ return (r.stdout + r.stderr).strip()
31
+ except Exception:
32
+ return ""
33
+
34
+
35
+ def collect(last_exit_code: int = 0, last_command: str = "") -> dict:
36
+ """
37
+ Collect shell context. Called before every backend request.
38
+ Fast — all subprocesses have a 5s timeout and failures are silent.
39
+ """
40
+ cwd = os.getcwd()
41
+ home = str(Path.home())
42
+
43
+ # Git context
44
+ git_branch = _run("git rev-parse --abbrev-ref HEAD 2>/dev/null")
45
+ git_status = _run("git status --short 2>/dev/null")
46
+ git_diff_stat = _run("git diff --stat HEAD 2>/dev/null | tail -1")
47
+
48
+ # Project type detection
49
+ project_type = _detect_project_type(cwd)
50
+
51
+ # Recent files changed (git-aware)
52
+ recent_files = _run("git diff --name-only HEAD 2>/dev/null | head -10")
53
+ if not recent_files:
54
+ # Fallback: recently modified files
55
+ recent_files = _run(
56
+ "find . -maxdepth 2 -newer . -type f "
57
+ "! -path './.git/*' ! -path './node_modules/*' "
58
+ "! -path './__pycache__/*' 2>/dev/null | head -10"
59
+ )
60
+
61
+ # Directory tree (shallow)
62
+ tree = _run(
63
+ "find . -maxdepth 2 -not -path './.git/*' "
64
+ "-not -path './node_modules/*' "
65
+ "-not -path './__pycache__/*' "
66
+ "-not -name '*.pyc' 2>/dev/null | head -40"
67
+ )
68
+
69
+ ctx = {
70
+ "cwd": cwd.replace(home, "~"),
71
+ "shell": os.environ.get("SHELL", "bash"),
72
+ "os": _run("uname -s") or "Linux",
73
+ "project_type": project_type,
74
+ "last_exit_code": last_exit_code,
75
+ }
76
+
77
+ if last_command:
78
+ ctx["last_command"] = last_command
79
+
80
+ if git_branch:
81
+ ctx["git"] = {
82
+ "branch": git_branch,
83
+ "status": git_status or "clean",
84
+ "diff_stat": git_diff_stat or "",
85
+ "changed_files": recent_files or "",
86
+ }
87
+
88
+ if tree:
89
+ ctx["directory_tree"] = tree
90
+
91
+ return ctx
92
+
93
+
94
+ def _detect_project_type(cwd: str) -> str:
95
+ p = Path(cwd)
96
+ checks = [
97
+ (p / "package.json", "node"),
98
+ (p / "pyproject.toml", "python"),
99
+ (p / "requirements.txt", "python"),
100
+ (p / "setup.py", "python"),
101
+ (p / "Cargo.toml", "rust"),
102
+ (p / "go.mod", "go"),
103
+ (p / "pom.xml", "java"),
104
+ (p / "Dockerfile", "docker"),
105
+ (p / "docker-compose.yml", "docker"),
106
+ (p / "main.py", "python"),
107
+ (p / "index.js", "node"),
108
+ (p / "index.ts", "typescript"),
109
+ ]
110
+ for path, kind in checks:
111
+ if path.exists():
112
+ return kind
113
+ return "unknown"
114
+
115
+
116
+ def format_for_prompt(ctx: dict) -> str:
117
+ """
118
+ Format context into a compact system prompt block.
119
+ Injected into every request so OADSON knows your shell state.
120
+ """
121
+ lines = [
122
+ "=== SHELL CONTEXT ===",
123
+ f"cwd: {ctx['cwd']}",
124
+ f"project: {ctx.get('project_type', 'unknown')}",
125
+ f"shell: {ctx.get('shell', 'bash')}",
126
+ f"os: {ctx.get('os', 'Linux')}",
127
+ f"last_exit_code: {ctx.get('last_exit_code', 0)}",
128
+ ]
129
+
130
+ if ctx.get("last_command"):
131
+ lines.append(f"last_command: {ctx['last_command']}")
132
+
133
+ if ctx.get("git"):
134
+ g = ctx["git"]
135
+ lines.append(f"git_branch: {g['branch']}")
136
+ lines.append(f"git_status: {g['status']}")
137
+ if g.get("changed_files"):
138
+ lines.append(f"changed_files:\n{g['changed_files']}")
139
+
140
+ if ctx.get("directory_tree"):
141
+ lines.append(f"directory_tree:\n{ctx['directory_tree']}")
142
+
143
+ lines.append("=== END CONTEXT ===")
144
+ return "\n".join(lines)
oadson/executor.py ADDED
@@ -0,0 +1,367 @@
1
+ """
2
+ executor.py — Local tool executor.
3
+
4
+ This is the Claude Code equivalent for OADSON.
5
+ Tools are executed locally on YOUR machine, not on Railway.
6
+ Railway sends the plan; this file carries it out — with your permission.
7
+
8
+ Permission tiers:
9
+ auto_allow → runs silently, no prompt
10
+ confirm → shows what will change, waits for y/n
11
+ always_backup → copies file first, then confirms
12
+ never_allow → blocked, no override
13
+ """
14
+ import os
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ from rich.console import Console
23
+ from rich.syntax import Syntax
24
+ from rich.panel import Panel
25
+ from rich.prompt import Confirm
26
+
27
+ from oadson.config import get_permissions
28
+
29
+ console = Console()
30
+
31
+ BACKUP_DIR = Path.home() / ".oadson" / "backups"
32
+
33
+
34
+ # ── TOOL REGISTRY ──
35
+ # Maps tool names the AI can call → local handler functions.
36
+
37
+ def _backup_file(path: str) -> Optional[str]:
38
+ """Copy file to ~/.oadson/backups/ before touching it."""
39
+ src = Path(path)
40
+ if not src.exists():
41
+ return None
42
+ BACKUP_DIR.mkdir(parents=True, exist_ok=True)
43
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
44
+ dest = BACKUP_DIR / f"{src.name}.{ts}.bak"
45
+ shutil.copy2(src, dest)
46
+ return str(dest)
47
+
48
+
49
+ def _show_diff(path: str, new_content: str):
50
+ """Show a unified diff of current file vs proposed content."""
51
+ src = Path(path)
52
+ if src.exists():
53
+ old_lines = src.read_text().splitlines(keepends=True)
54
+ else:
55
+ old_lines = []
56
+ new_lines = new_content.splitlines(keepends=True)
57
+
58
+ import difflib
59
+ diff = list(difflib.unified_diff(
60
+ old_lines, new_lines,
61
+ fromfile=f"current: {path}",
62
+ tofile=f"proposed: {path}",
63
+ lineterm=""
64
+ ))
65
+ if diff:
66
+ diff_text = "\n".join(diff)
67
+ console.print(Syntax(diff_text, "diff", theme="monokai"))
68
+ else:
69
+ console.print("[dim]No changes.[/dim]")
70
+
71
+
72
+ def _is_never_allowed(command: str, perms: dict) -> bool:
73
+ return any(blocked in command for blocked in perms.get("never_allow", []))
74
+
75
+
76
+ def _get_tier(tool_name: str, command: str, perms: dict) -> str:
77
+ """Determine permission tier for this tool call."""
78
+ # Check never_allow first
79
+ if command and _is_never_allowed(command, perms):
80
+ return "never"
81
+
82
+ for item in perms.get("never_allow", []):
83
+ if item in tool_name:
84
+ return "never"
85
+
86
+ for item in perms.get("always_backup", []):
87
+ if item in tool_name:
88
+ return "backup"
89
+
90
+ for item in perms.get("confirm", []):
91
+ if item in tool_name:
92
+ return "confirm"
93
+
94
+ for item in perms.get("auto_allow", []):
95
+ if item in tool_name:
96
+ return "auto"
97
+
98
+ # Default: confirm anything unknown
99
+ return "confirm"
100
+
101
+
102
+ # ── INDIVIDUAL TOOL HANDLERS ──
103
+
104
+ def tool_read_file(args: dict) -> dict:
105
+ path = args.get("path", "")
106
+ try:
107
+ content = Path(path).read_text()
108
+ return {"status": "success", "content": content, "path": path}
109
+ except FileNotFoundError:
110
+ return {"status": "error", "reason": f"File not found: {path}"}
111
+ except Exception as e:
112
+ return {"status": "error", "reason": str(e)}
113
+
114
+
115
+ def tool_write_file(args: dict) -> dict:
116
+ path = args.get("path", "")
117
+ content = args.get("content", "")
118
+ perms = get_permissions()
119
+ tier = _get_tier("write_file", "", perms)
120
+
121
+ if tier == "never":
122
+ return {"status": "blocked", "reason": "write_file is in never_allow"}
123
+
124
+ # Show diff
125
+ console.print(f"\n[bold yellow]⚡ OADSON wants to write:[/bold yellow] {path}")
126
+ _show_diff(path, content)
127
+
128
+ if tier == "backup":
129
+ backup = _backup_file(path)
130
+ if backup:
131
+ console.print(f"[dim]Backup saved to: {backup}[/dim]")
132
+
133
+ if tier in ("confirm", "backup"):
134
+ if not Confirm.ask("[bold]Apply this change?[/bold]"):
135
+ return {"status": "cancelled", "reason": "User declined"}
136
+
137
+ try:
138
+ p = Path(path)
139
+ p.parent.mkdir(parents=True, exist_ok=True)
140
+ p.write_text(content)
141
+ return {"status": "success", "path": path, "bytes": len(content)}
142
+ except Exception as e:
143
+ return {"status": "error", "reason": str(e)}
144
+
145
+
146
+ def tool_create_file(args: dict) -> dict:
147
+ """Same as write_file but makes intent explicit."""
148
+ path = args.get("path", "")
149
+ content = args.get("content", "")
150
+ perms = get_permissions()
151
+
152
+ if Path(path).exists():
153
+ console.print(f"[bold yellow]⚠ File exists:[/bold yellow] {path} — treating as overwrite")
154
+ return tool_write_file(args)
155
+
156
+ console.print(f"\n[bold green]✨ OADSON wants to create:[/bold green] {path}")
157
+ if content:
158
+ lang = _detect_lang(path)
159
+ console.print(Panel(
160
+ Syntax(content, lang, theme="monokai", line_numbers=True),
161
+ title=f"New file: {path}",
162
+ border_style="green"
163
+ ))
164
+
165
+ tier = _get_tier("create_file", "", perms)
166
+ if tier in ("confirm", "backup"):
167
+ if not Confirm.ask("[bold]Create this file?[/bold]"):
168
+ return {"status": "cancelled", "reason": "User declined"}
169
+
170
+ try:
171
+ p = Path(path)
172
+ p.parent.mkdir(parents=True, exist_ok=True)
173
+ p.write_text(content)
174
+ return {"status": "success", "path": path, "created": True}
175
+ except Exception as e:
176
+ return {"status": "error", "reason": str(e)}
177
+
178
+
179
+ def tool_delete_file(args: dict) -> dict:
180
+ path = args.get("path", "")
181
+ perms = get_permissions()
182
+
183
+ if not Path(path).exists():
184
+ return {"status": "error", "reason": f"File not found: {path}"}
185
+
186
+ # Always backup before delete
187
+ backup = _backup_file(path)
188
+ console.print(f"\n[bold red]🗑 OADSON wants to DELETE:[/bold red] {path}")
189
+ if backup:
190
+ console.print(f"[dim]Backup saved to: {backup}[/dim]")
191
+
192
+ if not Confirm.ask("[bold red]Confirm DELETE?[/bold red]"):
193
+ return {"status": "cancelled", "reason": "User declined"}
194
+
195
+ try:
196
+ Path(path).unlink()
197
+ return {"status": "success", "path": path, "deleted": True, "backup": backup}
198
+ except Exception as e:
199
+ return {"status": "error", "reason": str(e)}
200
+
201
+
202
+ def tool_list_dir(args: dict) -> dict:
203
+ path = args.get("path", ".")
204
+ try:
205
+ p = Path(path)
206
+ items = []
207
+ for item in sorted(p.iterdir()):
208
+ items.append({
209
+ "name": item.name,
210
+ "type": "dir" if item.is_dir() else "file",
211
+ "size": item.stat().st_size if item.is_file() else None,
212
+ })
213
+ return {"status": "success", "path": str(p.resolve()), "items": items}
214
+ except Exception as e:
215
+ return {"status": "error", "reason": str(e)}
216
+
217
+
218
+ def tool_run_bash(args: dict) -> dict:
219
+ """
220
+ Run a bash command locally.
221
+ Always confirms unless the command is in auto_allow patterns.
222
+ Shows exactly what will run before running it.
223
+ """
224
+ command = args.get("command", "").strip()
225
+ if not command:
226
+ return {"status": "error", "reason": "No command provided"}
227
+
228
+ perms = get_permissions()
229
+ tier = _get_tier("run_bash", command, perms)
230
+
231
+ if tier == "never":
232
+ return {"status": "blocked", "reason": f"Command blocked by never_allow: {command}"}
233
+
234
+ # Check auto_allow patterns
235
+ if tier == "auto":
236
+ # Double-check: if it has destructive patterns, force confirm anyway
237
+ destructive = ["rm ", "rmdir", "mv ", "chmod 777", "> /", "dd if"]
238
+ if any(d in command for d in destructive):
239
+ tier = "confirm"
240
+
241
+ if tier in ("confirm", "backup"):
242
+ console.print(f"\n[bold yellow]⚡ OADSON wants to run:[/bold yellow]")
243
+ console.print(Panel(
244
+ Syntax(command, "bash", theme="monokai"),
245
+ border_style="yellow"
246
+ ))
247
+ if not Confirm.ask("[bold]Run this command?[/bold]"):
248
+ return {"status": "cancelled", "reason": "User declined"}
249
+
250
+ try:
251
+ result = subprocess.run(
252
+ command,
253
+ shell=True,
254
+ capture_output=True,
255
+ text=True,
256
+ timeout=60,
257
+ cwd=os.getcwd(),
258
+ )
259
+ return {
260
+ "status": "success" if result.returncode == 0 else "error",
261
+ "command": command,
262
+ "stdout": result.stdout,
263
+ "stderr": result.stderr or None,
264
+ "return_code": result.returncode,
265
+ }
266
+ except subprocess.TimeoutExpired:
267
+ return {"status": "timeout", "command": command, "reason": "Command exceeded 60s"}
268
+ except Exception as e:
269
+ return {"status": "error", "command": command, "reason": str(e)}
270
+
271
+
272
+ def tool_search_files(args: dict) -> dict:
273
+ pattern = args.get("pattern", "")
274
+ path = args.get("path", ".")
275
+ try:
276
+ result = subprocess.run(
277
+ f"grep -rn {repr(pattern)} {path} --include='*.py' --include='*.js' "
278
+ f"--include='*.ts' --include='*.json' --include='*.md' "
279
+ f"--exclude-dir=node_modules --exclude-dir=.git 2>/dev/null | head -50",
280
+ shell=True, capture_output=True, text=True, timeout=10
281
+ )
282
+ return {"status": "success", "matches": result.stdout}
283
+ except Exception as e:
284
+ return {"status": "error", "reason": str(e)}
285
+
286
+
287
+ def tool_git_status(args: dict) -> dict:
288
+ try:
289
+ status = subprocess.run("git status", shell=True, capture_output=True, text=True).stdout
290
+ branch = subprocess.run("git branch --show-current", shell=True, capture_output=True, text=True).stdout.strip()
291
+ return {"status": "success", "branch": branch, "output": status}
292
+ except Exception as e:
293
+ return {"status": "error", "reason": str(e)}
294
+
295
+
296
+ def tool_git_diff(args: dict) -> dict:
297
+ path = args.get("path", "")
298
+ cmd = f"git diff {path}" if path else "git diff"
299
+ try:
300
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
301
+ return {"status": "success", "diff": result.stdout}
302
+ except Exception as e:
303
+ return {"status": "error", "reason": str(e)}
304
+
305
+
306
+ def tool_http_request(args: dict) -> dict:
307
+ """Fire HTTP requests — useful for testing your Railway API."""
308
+ import httpx
309
+ method = args.get("method", "GET").upper()
310
+ url = args.get("url", "")
311
+ headers = args.get("headers", {})
312
+ body = args.get("body", None)
313
+
314
+ console.print(f"\n[bold cyan]🌐 OADSON wants to call:[/bold cyan] {method} {url}")
315
+ if not Confirm.ask("[bold]Send request?[/bold]"):
316
+ return {"status": "cancelled"}
317
+
318
+ try:
319
+ with httpx.Client(timeout=30) as client:
320
+ resp = client.request(method, url, headers=headers, json=body)
321
+ return {
322
+ "status": "success",
323
+ "http_status": resp.status_code,
324
+ "headers": dict(resp.headers),
325
+ "body": resp.text[:4000],
326
+ }
327
+ except Exception as e:
328
+ return {"status": "error", "reason": str(e)}
329
+
330
+
331
+ # ── TOOL DISPATCH TABLE ──
332
+ TOOLS = {
333
+ "read_file": tool_read_file,
334
+ "write_file": tool_write_file,
335
+ "create_file": tool_create_file,
336
+ "delete_file": tool_delete_file,
337
+ "list_dir": tool_list_dir,
338
+ "run_bash": tool_run_bash,
339
+ "search_files": tool_search_files,
340
+ "git_status": tool_git_status,
341
+ "git_diff": tool_git_diff,
342
+ "http_request": tool_http_request,
343
+ }
344
+
345
+
346
+ def execute_tool(tool_name: str, args: dict) -> dict:
347
+ """
348
+ Dispatch a tool call from the AI to the local handler.
349
+ Returns result dict always — never raises.
350
+ """
351
+ handler = TOOLS.get(tool_name)
352
+ if not handler:
353
+ return {"status": "error", "reason": f"Unknown tool: {tool_name}"}
354
+ try:
355
+ return handler(args)
356
+ except Exception as e:
357
+ return {"status": "error", "reason": f"Tool {tool_name} crashed: {e}"}
358
+
359
+
360
+ def _detect_lang(path: str) -> str:
361
+ ext = Path(path).suffix.lstrip(".")
362
+ return {
363
+ "py": "python", "js": "javascript", "ts": "typescript",
364
+ "json": "json", "md": "markdown", "sh": "bash",
365
+ "html": "html", "css": "css", "rs": "rust",
366
+ "go": "go", "yaml": "yaml", "yml": "yaml",
367
+ }.get(ext, "text")
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: oadson
3
+ Version: 1.0.0
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
+ ```