onako 0.4.2__tar.gz → 0.4.4__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.
- {onako-0.4.2 → onako-0.4.4}/PKG-INFO +1 -1
- {onako-0.4.2 → onako-0.4.4}/pyproject.toml +1 -1
- onako-0.4.4/src/onako/__init__.py +1 -0
- {onako-0.4.2 → onako-0.4.4}/src/onako/cli.py +69 -6
- {onako-0.4.2 → onako-0.4.4}/src/onako/server.py +18 -5
- onako-0.4.4/src/onako/static/index.html +837 -0
- {onako-0.4.2 → onako-0.4.4}/src/onako/tmux_orchestrator.py +84 -18
- {onako-0.4.2 → onako-0.4.4}/src/onako.egg-info/PKG-INFO +1 -1
- {onako-0.4.2 → onako-0.4.4}/tests/test_api.py +8 -4
- {onako-0.4.2 → onako-0.4.4}/tests/test_cli_service.py +14 -0
- onako-0.4.2/src/onako/__init__.py +0 -1
- onako-0.4.2/src/onako/static/index.html +0 -465
- {onako-0.4.2 → onako-0.4.4}/README.md +0 -0
- {onako-0.4.2 → onako-0.4.4}/setup.cfg +0 -0
- {onako-0.4.2 → onako-0.4.4}/src/onako.egg-info/SOURCES.txt +0 -0
- {onako-0.4.2 → onako-0.4.4}/src/onako.egg-info/dependency_links.txt +0 -0
- {onako-0.4.2 → onako-0.4.4}/src/onako.egg-info/entry_points.txt +0 -0
- {onako-0.4.2 → onako-0.4.4}/src/onako.egg-info/requires.txt +0 -0
- {onako-0.4.2 → onako-0.4.4}/src/onako.egg-info/top_level.txt +0 -0
- {onako-0.4.2 → onako-0.4.4}/tests/test_cli.py +0 -0
- {onako-0.4.2 → onako-0.4.4}/tests/test_tmux_orchestrator.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.4"
|
|
@@ -10,6 +10,7 @@ import click
|
|
|
10
10
|
ONAKO_DIR = Path.home() / ".onako"
|
|
11
11
|
LOG_DIR = ONAKO_DIR / "logs"
|
|
12
12
|
PID_FILE = ONAKO_DIR / "onako.pid"
|
|
13
|
+
DEV_MARKER = ONAKO_DIR / "onako.dev"
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@click.group(invoke_without_command=True)
|
|
@@ -18,8 +19,9 @@ PID_FILE = ONAKO_DIR / "onako.pid"
|
|
|
18
19
|
@click.option("--session", default="onako", help="tmux session name (auto-detected if inside tmux).")
|
|
19
20
|
@click.option("--dir", "working_dir", default=None, type=click.Path(exists=True), help="Working directory for tasks (default: current directory).")
|
|
20
21
|
@click.option("--dangerously-skip-permissions", "skip_permissions", is_flag=True, default=False, help="Pass --dangerously-skip-permissions to all Claude Code tasks.")
|
|
22
|
+
@click.option("--no-attach", is_flag=True, default=False, help="Start the server without attaching to tmux.")
|
|
21
23
|
@click.pass_context
|
|
22
|
-
def main(ctx, host, port, session, working_dir, skip_permissions):
|
|
24
|
+
def main(ctx, host, port, session, working_dir, skip_permissions, no_attach):
|
|
23
25
|
"""Onako — Dispatch and monitor Claude Code tasks from your phone."""
|
|
24
26
|
if ctx.invoked_subcommand is not None:
|
|
25
27
|
return
|
|
@@ -65,7 +67,7 @@ def main(ctx, host, port, session, working_dir, skip_permissions):
|
|
|
65
67
|
capture_output=True,
|
|
66
68
|
)
|
|
67
69
|
|
|
68
|
-
if not inside_tmux:
|
|
70
|
+
if not inside_tmux and not no_attach:
|
|
69
71
|
os.execvp("tmux", ["tmux", "attach-session", "-t", session])
|
|
70
72
|
|
|
71
73
|
|
|
@@ -115,16 +117,69 @@ def stop():
|
|
|
115
117
|
try:
|
|
116
118
|
pid = int(PID_FILE.read_text().strip())
|
|
117
119
|
os.kill(pid, 15) # SIGTERM
|
|
118
|
-
|
|
120
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
121
|
+
click.echo(f"Onako server stopped{dev} (pid {pid}).")
|
|
119
122
|
stopped = True
|
|
120
123
|
except (ValueError, ProcessLookupError):
|
|
121
124
|
click.echo("Stale pid file found, cleaning up.")
|
|
122
125
|
PID_FILE.unlink(missing_ok=True)
|
|
126
|
+
DEV_MARKER.unlink(missing_ok=True)
|
|
123
127
|
|
|
124
128
|
if not stopped:
|
|
125
129
|
click.echo("Onako service is not running.")
|
|
126
130
|
|
|
127
131
|
|
|
132
|
+
@main.command()
|
|
133
|
+
@click.option("--session", default="onako", help="tmux session name.")
|
|
134
|
+
def purge(session):
|
|
135
|
+
"""Kill all tmux windows from the onako session and clean up."""
|
|
136
|
+
# Auto-detect current tmux session if inside one
|
|
137
|
+
if os.environ.get("TMUX"):
|
|
138
|
+
try:
|
|
139
|
+
result = subprocess.run(
|
|
140
|
+
["tmux", "display-message", "-p", "#S"],
|
|
141
|
+
capture_output=True, text=True,
|
|
142
|
+
)
|
|
143
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
144
|
+
session = result.stdout.strip()
|
|
145
|
+
except FileNotFoundError:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
# Also stop the server if it's running
|
|
149
|
+
if PID_FILE.exists():
|
|
150
|
+
try:
|
|
151
|
+
pid = int(PID_FILE.read_text().strip())
|
|
152
|
+
os.kill(pid, 15)
|
|
153
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
154
|
+
click.echo(f"Stopped onako server{dev} (pid {pid}).")
|
|
155
|
+
except (ValueError, ProcessLookupError):
|
|
156
|
+
pass
|
|
157
|
+
PID_FILE.unlink(missing_ok=True)
|
|
158
|
+
DEV_MARKER.unlink(missing_ok=True)
|
|
159
|
+
|
|
160
|
+
# Kill the tmux session
|
|
161
|
+
result = subprocess.run(
|
|
162
|
+
["tmux", "kill-session", "-t", f"{session}:"],
|
|
163
|
+
capture_output=True, text=True,
|
|
164
|
+
)
|
|
165
|
+
if result.returncode == 0:
|
|
166
|
+
click.echo(f"Killed tmux session: {session}")
|
|
167
|
+
else:
|
|
168
|
+
click.echo(f"No tmux session '{session}' found.")
|
|
169
|
+
|
|
170
|
+
# Clean up the database
|
|
171
|
+
db_path = ONAKO_DIR / "onako.db"
|
|
172
|
+
if db_path.exists():
|
|
173
|
+
import sqlite3
|
|
174
|
+
conn = sqlite3.connect(db_path)
|
|
175
|
+
conn.execute("UPDATE tasks SET status = 'done' WHERE status = 'running'")
|
|
176
|
+
conn.commit()
|
|
177
|
+
conn.close()
|
|
178
|
+
click.echo("Marked all running tasks as done.")
|
|
179
|
+
|
|
180
|
+
click.echo("Purge complete.")
|
|
181
|
+
|
|
182
|
+
|
|
128
183
|
@main.command()
|
|
129
184
|
def status():
|
|
130
185
|
"""Check if Onako is running."""
|
|
@@ -133,7 +188,8 @@ def status():
|
|
|
133
188
|
r = urllib.request.urlopen("http://127.0.0.1:8787/health", timeout=2)
|
|
134
189
|
data = r.read().decode()
|
|
135
190
|
if '"ok"' in data:
|
|
136
|
-
|
|
191
|
+
mode = " (dev)" if DEV_MARKER.exists() else ""
|
|
192
|
+
click.echo(f"Onako server: running{mode}")
|
|
137
193
|
click.echo(" URL: http://127.0.0.1:8787")
|
|
138
194
|
else:
|
|
139
195
|
click.echo("Onako server: not responding correctly")
|
|
@@ -160,7 +216,8 @@ def _start_server(host, port, session, working_dir, skip_permissions=False):
|
|
|
160
216
|
Returns True if the server was started or is already running.
|
|
161
217
|
"""
|
|
162
218
|
if _is_server_running():
|
|
163
|
-
|
|
219
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
220
|
+
click.echo(f"Onako server already running{dev} (pid {PID_FILE.read_text().strip()})")
|
|
164
221
|
return True
|
|
165
222
|
|
|
166
223
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -185,9 +242,15 @@ def _start_server(host, port, session, working_dir, skip_permissions=False):
|
|
|
185
242
|
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
186
243
|
PID_FILE.write_text(str(proc.pid))
|
|
187
244
|
|
|
245
|
+
if os.environ.get("ONAKO_DEV"):
|
|
246
|
+
DEV_MARKER.write_text("")
|
|
247
|
+
else:
|
|
248
|
+
DEV_MARKER.unlink(missing_ok=True)
|
|
249
|
+
|
|
250
|
+
dev = " (dev)" if DEV_MARKER.exists() else ""
|
|
188
251
|
local_ip = _get_local_ip()
|
|
189
252
|
banner = [
|
|
190
|
-
f"Onako server started (pid {proc.pid})",
|
|
253
|
+
f"Onako server started{dev} (pid {proc.pid})",
|
|
191
254
|
f" Dashboard: http://{host}:{port}",
|
|
192
255
|
]
|
|
193
256
|
if local_ip:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shlex
|
|
3
|
+
import uuid
|
|
3
4
|
|
|
4
5
|
from fastapi import FastAPI, HTTPException
|
|
5
6
|
from fastapi.responses import FileResponse
|
|
@@ -37,11 +38,12 @@ def health():
|
|
|
37
38
|
@app.post("/tasks")
|
|
38
39
|
def create_task(req: CreateTaskRequest):
|
|
39
40
|
skip = req.skip_permissions if req.skip_permissions is not None else skip_permissions_default
|
|
41
|
+
claude_session_id = str(uuid.uuid4())
|
|
40
42
|
if skip:
|
|
41
|
-
command = f"claude --dangerously-skip-permissions {shlex.quote(req.prompt)}"
|
|
43
|
+
command = f"claude --dangerously-skip-permissions --session-id {claude_session_id} {shlex.quote(req.prompt)}"
|
|
42
44
|
else:
|
|
43
|
-
command = f"claude {shlex.quote(req.prompt)}"
|
|
44
|
-
task = orch.create_task(command, working_dir=req.working_dir, prompt=req.prompt)
|
|
45
|
+
command = f"claude --session-id {claude_session_id} {shlex.quote(req.prompt)}"
|
|
46
|
+
task = orch.create_task(command, working_dir=req.working_dir, prompt=req.prompt, claude_session_id=claude_session_id)
|
|
45
47
|
return task
|
|
46
48
|
|
|
47
49
|
|
|
@@ -84,12 +86,23 @@ def interrupt_task(task_id: str):
|
|
|
84
86
|
return {"status": "interrupted"}
|
|
85
87
|
|
|
86
88
|
|
|
89
|
+
@app.post("/tasks/{task_id}/resume")
|
|
90
|
+
def resume_task(task_id: str):
|
|
91
|
+
if task_id not in orch.tasks:
|
|
92
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
93
|
+
try:
|
|
94
|
+
new_task = orch.resume_task(task_id, skip_permissions=skip_permissions_default)
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
97
|
+
return new_task
|
|
98
|
+
|
|
99
|
+
|
|
87
100
|
@app.delete("/tasks/{task_id}")
|
|
88
101
|
def delete_task(task_id: str):
|
|
89
102
|
if task_id not in orch.tasks:
|
|
90
103
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
91
|
-
if
|
|
92
|
-
raise HTTPException(status_code=400, detail="Cannot kill the
|
|
104
|
+
if orch.window_count() <= 1:
|
|
105
|
+
raise HTTPException(status_code=400, detail="Cannot kill the last window")
|
|
93
106
|
orch.kill_task(task_id)
|
|
94
107
|
return {"status": "deleted"}
|
|
95
108
|
|