onako 0.2.1__py3-none-any.whl → 0.4.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.
- onako/__init__.py +1 -1
- onako/cli.py +140 -125
- onako/server.py +20 -3
- onako/static/index.html +58 -3
- onako/tmux_orchestrator.py +63 -17
- {onako-0.2.1.dist-info → onako-0.4.0.dist-info}/METADATA +11 -17
- onako-0.4.0.dist-info/RECORD +10 -0
- onako/templates/com.onako.server.plist.tpl +0 -34
- onako/templates/onako.service.tpl +0 -12
- onako-0.2.1.dist-info/RECORD +0 -12
- {onako-0.2.1.dist-info → onako-0.4.0.dist-info}/WHEEL +0 -0
- {onako-0.2.1.dist-info → onako-0.4.0.dist-info}/entry_points.txt +0 -0
- {onako-0.2.1.dist-info → onako-0.4.0.dist-info}/top_level.txt +0 -0
onako/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.4.0"
|
onako/cli.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import
|
|
2
|
+
import socket
|
|
3
3
|
import shutil
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
@@ -9,12 +9,52 @@ import click
|
|
|
9
9
|
|
|
10
10
|
ONAKO_DIR = Path.home() / ".onako"
|
|
11
11
|
LOG_DIR = ONAKO_DIR / "logs"
|
|
12
|
+
PID_FILE = ONAKO_DIR / "onako.pid"
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
@click.group()
|
|
15
|
-
|
|
15
|
+
@click.group(invoke_without_command=True)
|
|
16
|
+
@click.option("--host", default="0.0.0.0", help="Host to bind to.")
|
|
17
|
+
@click.option("--port", default=8787, type=int, help="Port to bind to.")
|
|
18
|
+
@click.option("--session", default="onako", help="tmux session name (auto-detected if inside tmux).")
|
|
19
|
+
@click.option("--dir", "working_dir", default=None, type=click.Path(exists=True), help="Working directory for tasks (default: current directory).")
|
|
20
|
+
@click.option("--dangerously-skip-permissions", "skip_permissions", is_flag=True, default=False, help="Pass --dangerously-skip-permissions to all Claude Code tasks.")
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def main(ctx, host, port, session, working_dir, skip_permissions):
|
|
16
23
|
"""Onako — Dispatch and monitor Claude Code tasks from your phone."""
|
|
17
|
-
|
|
24
|
+
if ctx.invoked_subcommand is not None:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
_check_prerequisites()
|
|
28
|
+
working_dir = str(Path(working_dir).resolve()) if working_dir else os.getcwd()
|
|
29
|
+
|
|
30
|
+
# Auto-detect current tmux session if inside one
|
|
31
|
+
if os.environ.get("TMUX"):
|
|
32
|
+
try:
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
["tmux", "display-message", "-p", "#S"],
|
|
35
|
+
capture_output=True, text=True,
|
|
36
|
+
)
|
|
37
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
38
|
+
detected = result.stdout.strip()
|
|
39
|
+
click.echo(f"Detected tmux session: {detected}")
|
|
40
|
+
session = detected
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
_start_server(host, port, session, working_dir, skip_permissions)
|
|
45
|
+
|
|
46
|
+
# If not inside tmux, ensure session exists and attach
|
|
47
|
+
if not os.environ.get("TMUX"):
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
["tmux", "has-session", "-t", session],
|
|
50
|
+
capture_output=True,
|
|
51
|
+
)
|
|
52
|
+
if result.returncode != 0:
|
|
53
|
+
subprocess.run(
|
|
54
|
+
["tmux", "new-session", "-d", "-s", session],
|
|
55
|
+
check=True,
|
|
56
|
+
)
|
|
57
|
+
os.execvp("tmux", ["tmux", "attach-session", "-t", session])
|
|
18
58
|
|
|
19
59
|
|
|
20
60
|
@main.command()
|
|
@@ -27,24 +67,25 @@ def version():
|
|
|
27
67
|
@main.command()
|
|
28
68
|
@click.option("--host", default="0.0.0.0", help="Host to bind to.")
|
|
29
69
|
@click.option("--port", default=8787, type=int, help="Port to bind to.")
|
|
70
|
+
@click.option("--session", default="onako", help="tmux session name.")
|
|
30
71
|
@click.option("--dir", "working_dir", default=None, type=click.Path(exists=True), help="Working directory for tasks (default: current directory).")
|
|
31
|
-
@click.option("--
|
|
32
|
-
def serve(host, port, working_dir,
|
|
72
|
+
@click.option("--dangerously-skip-permissions", "skip_permissions", is_flag=True, default=False, help="Pass --dangerously-skip-permissions to all Claude Code tasks.")
|
|
73
|
+
def serve(host, port, session, working_dir, skip_permissions):
|
|
33
74
|
"""Start the Onako server."""
|
|
34
75
|
working_dir = str(Path(working_dir).resolve()) if working_dir else os.getcwd()
|
|
35
76
|
|
|
36
|
-
if background:
|
|
37
|
-
_start_background(host, port, working_dir)
|
|
38
|
-
return
|
|
39
|
-
|
|
40
77
|
_check_prerequisites()
|
|
41
78
|
|
|
42
79
|
os.environ["ONAKO_WORKING_DIR"] = working_dir
|
|
80
|
+
os.environ["ONAKO_SESSION"] = session
|
|
81
|
+
if skip_permissions:
|
|
82
|
+
os.environ["ONAKO_SKIP_PERMISSIONS"] = "1"
|
|
43
83
|
|
|
44
84
|
from onako import __version__
|
|
45
85
|
click.echo(f"Onako v{__version__}")
|
|
46
86
|
click.echo(f"Starting server at http://{host}:{port}")
|
|
47
87
|
click.echo(f"Working directory: {working_dir}")
|
|
88
|
+
click.echo(f"Session: {session}")
|
|
48
89
|
click.echo()
|
|
49
90
|
|
|
50
91
|
import uvicorn
|
|
@@ -55,32 +96,21 @@ def serve(host, port, working_dir, background):
|
|
|
55
96
|
@main.command()
|
|
56
97
|
def stop():
|
|
57
98
|
"""Stop the background Onako service."""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
elif system == "Linux":
|
|
74
|
-
unit_path = Path.home() / ".config" / "systemd" / "user" / "onako.service"
|
|
75
|
-
if unit_path.exists():
|
|
76
|
-
subprocess.run(["systemctl", "--user", "disable", "--now", "onako"])
|
|
77
|
-
unit_path.unlink()
|
|
78
|
-
subprocess.run(["systemctl", "--user", "daemon-reload"])
|
|
79
|
-
click.echo("Onako service stopped and removed.")
|
|
80
|
-
else:
|
|
81
|
-
click.echo("Onako service is not running.")
|
|
82
|
-
else:
|
|
83
|
-
click.echo(f"Not supported on {system}.")
|
|
99
|
+
stopped = False
|
|
100
|
+
|
|
101
|
+
# Try pid file first (from `onako start`)
|
|
102
|
+
if PID_FILE.exists():
|
|
103
|
+
try:
|
|
104
|
+
pid = int(PID_FILE.read_text().strip())
|
|
105
|
+
os.kill(pid, 15) # SIGTERM
|
|
106
|
+
click.echo(f"Onako server stopped (pid {pid}).")
|
|
107
|
+
stopped = True
|
|
108
|
+
except (ValueError, ProcessLookupError):
|
|
109
|
+
click.echo("Stale pid file found, cleaning up.")
|
|
110
|
+
PID_FILE.unlink(missing_ok=True)
|
|
111
|
+
|
|
112
|
+
if not stopped:
|
|
113
|
+
click.echo("Onako service is not running.")
|
|
84
114
|
|
|
85
115
|
|
|
86
116
|
@main.command()
|
|
@@ -99,110 +129,95 @@ def status():
|
|
|
99
129
|
click.echo("Onako server: not running")
|
|
100
130
|
|
|
101
131
|
|
|
102
|
-
def
|
|
103
|
-
"""
|
|
104
|
-
|
|
132
|
+
def _is_server_running():
|
|
133
|
+
"""Check if the onako server is already running via pid file."""
|
|
134
|
+
if not PID_FILE.exists():
|
|
135
|
+
return False
|
|
136
|
+
try:
|
|
137
|
+
pid = int(PID_FILE.read_text().strip())
|
|
138
|
+
os.kill(pid, 0) # signal 0 = check if process exists
|
|
139
|
+
return True
|
|
140
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
141
|
+
PID_FILE.unlink(missing_ok=True)
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _start_server(host, port, session, working_dir, skip_permissions=False):
|
|
146
|
+
"""Start the Onako server in the background if not already running.
|
|
147
|
+
|
|
148
|
+
Returns True if the server was started or is already running.
|
|
149
|
+
"""
|
|
150
|
+
if _is_server_running():
|
|
151
|
+
click.echo(f"Onako server already running (pid {PID_FILE.read_text().strip()})")
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
105
155
|
onako_bin = shutil.which("onako")
|
|
106
156
|
if not onako_bin:
|
|
107
157
|
click.echo("Error: 'onako' command not found on PATH.", err=True)
|
|
108
158
|
sys.exit(1)
|
|
109
159
|
|
|
110
|
-
LOG_DIR.
|
|
160
|
+
log_out = LOG_DIR / "onako.log"
|
|
161
|
+
|
|
162
|
+
cmd = [onako_bin, "serve", "--host", host, "--port", str(port), "--session", session, "--dir", working_dir]
|
|
163
|
+
if skip_permissions:
|
|
164
|
+
cmd.append("--dangerously-skip-permissions")
|
|
165
|
+
|
|
166
|
+
with open(log_out, "a") as log_fh:
|
|
167
|
+
proc = subprocess.Popen(
|
|
168
|
+
cmd,
|
|
169
|
+
stdout=log_fh,
|
|
170
|
+
stderr=subprocess.STDOUT,
|
|
171
|
+
start_new_session=True,
|
|
172
|
+
)
|
|
173
|
+
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
PID_FILE.write_text(str(proc.pid))
|
|
175
|
+
|
|
176
|
+
local_ip = _get_local_ip()
|
|
177
|
+
banner = [
|
|
178
|
+
f"Onako server started (pid {proc.pid})",
|
|
179
|
+
f" Dashboard: http://{host}:{port}",
|
|
180
|
+
]
|
|
181
|
+
if local_ip:
|
|
182
|
+
banner.append(f" http://{local_ip}:{port}")
|
|
183
|
+
banner.append(f" Session: {session}")
|
|
184
|
+
banner.append(f" Logs: {log_out}")
|
|
185
|
+
for line in banner:
|
|
186
|
+
click.echo(line)
|
|
187
|
+
|
|
188
|
+
# Wait for server to be ready
|
|
189
|
+
import urllib.request
|
|
190
|
+
for _ in range(20):
|
|
191
|
+
try:
|
|
192
|
+
urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=1)
|
|
193
|
+
break
|
|
194
|
+
except Exception:
|
|
195
|
+
import time
|
|
196
|
+
time.sleep(0.25)
|
|
111
197
|
|
|
112
|
-
|
|
113
|
-
path_dirs = set()
|
|
114
|
-
for cmd in ["tmux", "claude"]:
|
|
115
|
-
p = shutil.which(cmd)
|
|
116
|
-
if p:
|
|
117
|
-
path_dirs.add(str(Path(p).parent))
|
|
118
|
-
path_dirs.update(["/usr/local/bin", "/usr/bin", "/bin"])
|
|
119
|
-
path_value = ":".join(sorted(path_dirs))
|
|
120
|
-
|
|
121
|
-
if system == "Darwin":
|
|
122
|
-
_install_launchd(onako_bin, host, port, working_dir, path_value)
|
|
123
|
-
elif system == "Linux":
|
|
124
|
-
_install_systemd(onako_bin, host, port, working_dir, path_value)
|
|
125
|
-
else:
|
|
126
|
-
click.echo(f"Background mode is not supported on {system}. Run 'onako serve' manually.", err=True)
|
|
127
|
-
sys.exit(1)
|
|
198
|
+
return True
|
|
128
199
|
|
|
129
200
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
plist_path = Path.home() / "Library" / "LaunchAgents" / "com.onako.server.plist"
|
|
142
|
-
plist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
-
|
|
144
|
-
# Unload existing service if present
|
|
145
|
-
uid = os.getuid()
|
|
146
|
-
subprocess.run(
|
|
147
|
-
["launchctl", "bootout", f"gui/{uid}", str(plist_path)],
|
|
148
|
-
capture_output=True,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
plist_path.write_text(plist)
|
|
152
|
-
|
|
153
|
-
# Register and start the service
|
|
154
|
-
result = subprocess.run(
|
|
155
|
-
["launchctl", "bootstrap", f"gui/{uid}", str(plist_path)],
|
|
156
|
-
capture_output=True, text=True,
|
|
157
|
-
)
|
|
158
|
-
if result.returncode != 0:
|
|
159
|
-
subprocess.run(["launchctl", "load", str(plist_path)], check=True)
|
|
160
|
-
|
|
161
|
-
# kickstart forces the service to run now (bootstrap alone may not start it)
|
|
162
|
-
subprocess.run(
|
|
163
|
-
["launchctl", "kickstart", f"gui/{uid}/com.onako.server"],
|
|
164
|
-
capture_output=True,
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
click.echo(f"Onako running in background at http://{host}:{port}")
|
|
168
|
-
click.echo(f" Working directory: {working_dir}")
|
|
169
|
-
click.echo(f" Logs: {LOG_DIR}")
|
|
170
|
-
click.echo()
|
|
171
|
-
click.echo("If macOS blocks this service, allow it in:")
|
|
172
|
-
click.echo(" System Settings > General > Login Items & Extensions")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _install_systemd(onako_bin, host, port, working_dir, path_value):
|
|
176
|
-
from importlib.resources import files
|
|
177
|
-
tpl = files("onako").joinpath("templates", "onako.service.tpl").read_text()
|
|
178
|
-
unit = tpl.format(
|
|
179
|
-
onako_bin=onako_bin,
|
|
180
|
-
host=host,
|
|
181
|
-
port=port,
|
|
182
|
-
working_dir=working_dir,
|
|
183
|
-
path_value=path_value,
|
|
184
|
-
)
|
|
185
|
-
unit_path = Path.home() / ".config" / "systemd" / "user" / "onako.service"
|
|
186
|
-
unit_path.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
-
unit_path.write_text(unit)
|
|
188
|
-
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
|
|
189
|
-
subprocess.run(["systemctl", "--user", "enable", "--now", "onako"], check=True)
|
|
190
|
-
click.echo(f"Onako running in background at http://{host}:{port}")
|
|
191
|
-
click.echo(f" Working directory: {working_dir}")
|
|
201
|
+
|
|
202
|
+
def _get_local_ip():
|
|
203
|
+
"""Get the machine's local network IP address."""
|
|
204
|
+
try:
|
|
205
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
206
|
+
s.connect(("8.8.8.8", 80))
|
|
207
|
+
ip = s.getsockname()[0]
|
|
208
|
+
s.close()
|
|
209
|
+
return ip
|
|
210
|
+
except Exception:
|
|
211
|
+
return None
|
|
192
212
|
|
|
193
213
|
|
|
194
214
|
def _check_prerequisites():
|
|
195
215
|
"""Check that tmux and claude are installed."""
|
|
196
|
-
|
|
197
|
-
if not tmux_path:
|
|
216
|
+
if not shutil.which("tmux"):
|
|
198
217
|
click.echo("Error: tmux is not installed.", err=True)
|
|
199
218
|
click.echo("Install it with: brew install tmux (macOS) or apt install tmux (Linux)", err=True)
|
|
200
219
|
sys.exit(1)
|
|
201
|
-
click.echo(f" tmux: {tmux_path}")
|
|
202
220
|
|
|
203
|
-
|
|
204
|
-
if not claude_path:
|
|
221
|
+
if not shutil.which("claude"):
|
|
205
222
|
click.echo("Warning: claude CLI not found on PATH.", err=True)
|
|
206
223
|
click.echo("Install Claude Code from: https://docs.anthropic.com/en/docs/claude-code", err=True)
|
|
207
|
-
else:
|
|
208
|
-
click.echo(f" claude: {claude_path}")
|
onako/server.py
CHANGED
|
@@ -8,13 +8,16 @@ from pydantic import BaseModel
|
|
|
8
8
|
from onako.tmux_orchestrator import TmuxOrchestrator
|
|
9
9
|
|
|
10
10
|
app = FastAPI()
|
|
11
|
-
|
|
11
|
+
session_name = os.environ.get("ONAKO_SESSION", "onako")
|
|
12
|
+
skip_permissions_default = os.environ.get("ONAKO_SKIP_PERMISSIONS") == "1"
|
|
13
|
+
orch = TmuxOrchestrator(session_name=session_name)
|
|
12
14
|
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class CreateTaskRequest(BaseModel):
|
|
16
18
|
prompt: str
|
|
17
19
|
working_dir: str | None = None
|
|
20
|
+
skip_permissions: bool | None = None
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
class SendMessageRequest(BaseModel):
|
|
@@ -28,12 +31,16 @@ def dashboard():
|
|
|
28
31
|
|
|
29
32
|
@app.get("/health")
|
|
30
33
|
def health():
|
|
31
|
-
return {"status": "ok"}
|
|
34
|
+
return {"status": "ok", "skip_permissions": skip_permissions_default}
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
@app.post("/tasks")
|
|
35
38
|
def create_task(req: CreateTaskRequest):
|
|
36
|
-
|
|
39
|
+
skip = req.skip_permissions if req.skip_permissions is not None else skip_permissions_default
|
|
40
|
+
if skip:
|
|
41
|
+
command = f"claude --dangerously-skip-permissions {shlex.quote(req.prompt)}"
|
|
42
|
+
else:
|
|
43
|
+
command = f"claude {shlex.quote(req.prompt)}"
|
|
37
44
|
task = orch.create_task(command, working_dir=req.working_dir, prompt=req.prompt)
|
|
38
45
|
return task
|
|
39
46
|
|
|
@@ -69,10 +76,20 @@ def send_message(task_id: str, req: SendMessageRequest):
|
|
|
69
76
|
return {"status": "sent"}
|
|
70
77
|
|
|
71
78
|
|
|
79
|
+
@app.post("/tasks/{task_id}/interrupt")
|
|
80
|
+
def interrupt_task(task_id: str):
|
|
81
|
+
if task_id not in orch.tasks:
|
|
82
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
83
|
+
orch.send_interrupt(task_id)
|
|
84
|
+
return {"status": "interrupted"}
|
|
85
|
+
|
|
86
|
+
|
|
72
87
|
@app.delete("/tasks/{task_id}")
|
|
73
88
|
def delete_task(task_id: str):
|
|
74
89
|
if task_id not in orch.tasks:
|
|
75
90
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
91
|
+
if task_id == "onako-main":
|
|
92
|
+
raise HTTPException(status_code=400, detail="Cannot kill the main window")
|
|
76
93
|
orch.kill_task(task_id)
|
|
77
94
|
return {"status": "deleted"}
|
|
78
95
|
|
onako/static/index.html
CHANGED
|
@@ -55,6 +55,16 @@
|
|
|
55
55
|
}
|
|
56
56
|
.status-running { color: #22c55e; }
|
|
57
57
|
.status-done { color: #888; }
|
|
58
|
+
.origin-badge {
|
|
59
|
+
display: inline-block;
|
|
60
|
+
background: #444;
|
|
61
|
+
color: #aaa;
|
|
62
|
+
font-size: 10px;
|
|
63
|
+
padding: 1px 6px;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
margin-left: 6px;
|
|
66
|
+
vertical-align: middle;
|
|
67
|
+
}
|
|
58
68
|
.empty-state {
|
|
59
69
|
text-align: center;
|
|
60
70
|
color: #666;
|
|
@@ -88,6 +98,16 @@
|
|
|
88
98
|
cursor: pointer;
|
|
89
99
|
font-size: 14px;
|
|
90
100
|
}
|
|
101
|
+
#interrupt-btn {
|
|
102
|
+
background: #f59e0b;
|
|
103
|
+
color: white;
|
|
104
|
+
border: none;
|
|
105
|
+
padding: 6px 12px;
|
|
106
|
+
border-radius: 6px;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
}
|
|
110
|
+
#interrupt-btn.hidden { display: none; }
|
|
91
111
|
#kill-btn {
|
|
92
112
|
background: #ef4444;
|
|
93
113
|
color: white;
|
|
@@ -211,6 +231,7 @@
|
|
|
211
231
|
<div id="detail-header">
|
|
212
232
|
<button id="back-btn">← Back</button>
|
|
213
233
|
<span id="detail-task-id"></span>
|
|
234
|
+
<button id="interrupt-btn">Interrupt</button>
|
|
214
235
|
<button id="kill-btn">Kill</button>
|
|
215
236
|
</div>
|
|
216
237
|
<div id="output"></div>
|
|
@@ -225,6 +246,10 @@
|
|
|
225
246
|
<div id="modal">
|
|
226
247
|
<textarea id="prompt-input" placeholder="What do you want done?"></textarea>
|
|
227
248
|
<input id="workdir-input" type="text" placeholder="Working directory (optional)">
|
|
249
|
+
<label id="skip-perms-label" style="display:flex;align-items:center;gap:8px;margin-top:8px;font-size:13px;color:#aaa;cursor:pointer;">
|
|
250
|
+
<input type="checkbox" id="skip-perms-input" style="width:auto;margin:0;">
|
|
251
|
+
Skip permissions
|
|
252
|
+
</label>
|
|
228
253
|
<button id="submit-task-btn">Start Task</button>
|
|
229
254
|
</div>
|
|
230
255
|
|
|
@@ -233,6 +258,7 @@
|
|
|
233
258
|
let currentTaskId = null;
|
|
234
259
|
let currentTaskStatus = null;
|
|
235
260
|
let pollInterval = null;
|
|
261
|
+
let skipPermissionsDefault = false;
|
|
236
262
|
|
|
237
263
|
function timeAgo(dateStr) {
|
|
238
264
|
if (!dateStr) return '';
|
|
@@ -250,6 +276,15 @@
|
|
|
250
276
|
document.getElementById('connection-banner').style.display = show ? 'block' : 'none';
|
|
251
277
|
}
|
|
252
278
|
|
|
279
|
+
async function loadConfig() {
|
|
280
|
+
try {
|
|
281
|
+
const res = await fetch(`${API}/health`);
|
|
282
|
+
const data = await res.json();
|
|
283
|
+
skipPermissionsDefault = data.skip_permissions || false;
|
|
284
|
+
document.getElementById('skip-perms-input').checked = skipPermissionsDefault;
|
|
285
|
+
} catch (e) {}
|
|
286
|
+
}
|
|
287
|
+
|
|
253
288
|
async function loadTasks() {
|
|
254
289
|
try {
|
|
255
290
|
const res = await fetch(`${API}/tasks`);
|
|
@@ -260,7 +295,7 @@
|
|
|
260
295
|
} else {
|
|
261
296
|
list.innerHTML = tasks.map(t => `
|
|
262
297
|
<div class="task-item" onclick="showTask('${t.id}')">
|
|
263
|
-
<div class="task-id">${t.id}</div>
|
|
298
|
+
<div class="task-id">${t.id}${t.origin === 'external' ? '<span class="origin-badge">external</span>' : ''}</div>
|
|
264
299
|
<div class="task-prompt">${escapeHtml(t.prompt)}</div>
|
|
265
300
|
<div class="task-meta">
|
|
266
301
|
<span class="status-${t.status}">${t.status}</span>
|
|
@@ -281,7 +316,13 @@
|
|
|
281
316
|
document.getElementById('list-view').classList.add('hidden');
|
|
282
317
|
document.getElementById('detail-view').classList.add('active');
|
|
283
318
|
document.getElementById('detail-task-id').textContent = id;
|
|
284
|
-
|
|
319
|
+
if (id === 'onako-main') {
|
|
320
|
+
document.getElementById('interrupt-btn').classList.add('hidden');
|
|
321
|
+
document.getElementById('kill-btn').classList.add('hidden');
|
|
322
|
+
} else {
|
|
323
|
+
document.getElementById('interrupt-btn').classList.remove('hidden');
|
|
324
|
+
document.getElementById('kill-btn').classList.remove('hidden');
|
|
325
|
+
}
|
|
285
326
|
await refreshOutput();
|
|
286
327
|
pollInterval = setInterval(refreshOutput, 3000);
|
|
287
328
|
}
|
|
@@ -301,6 +342,7 @@
|
|
|
301
342
|
// Stop polling and hide kill button when task is done
|
|
302
343
|
if (data.status === 'done' && currentTaskStatus !== 'done') {
|
|
303
344
|
currentTaskStatus = 'done';
|
|
345
|
+
document.getElementById('interrupt-btn').classList.add('hidden');
|
|
304
346
|
document.getElementById('kill-btn').classList.add('hidden');
|
|
305
347
|
if (pollInterval) {
|
|
306
348
|
clearInterval(pollInterval);
|
|
@@ -338,6 +380,15 @@
|
|
|
338
380
|
}
|
|
339
381
|
}
|
|
340
382
|
|
|
383
|
+
async function interruptTask() {
|
|
384
|
+
if (!currentTaskId) return;
|
|
385
|
+
try {
|
|
386
|
+
await fetch(`${API}/tasks/${currentTaskId}/interrupt`, {method: 'POST'});
|
|
387
|
+
} catch (e) {
|
|
388
|
+
showConnectionError(true);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
341
392
|
async function killTask() {
|
|
342
393
|
if (!currentTaskId) return;
|
|
343
394
|
if (!confirm('Kill this task?')) return;
|
|
@@ -352,6 +403,7 @@
|
|
|
352
403
|
function showModal() {
|
|
353
404
|
document.getElementById('modal').style.display = 'block';
|
|
354
405
|
document.getElementById('modal-overlay').style.display = 'block';
|
|
406
|
+
document.getElementById('skip-perms-input').checked = skipPermissionsDefault;
|
|
355
407
|
document.getElementById('prompt-input').focus();
|
|
356
408
|
}
|
|
357
409
|
|
|
@@ -364,7 +416,8 @@
|
|
|
364
416
|
const prompt = document.getElementById('prompt-input').value.trim();
|
|
365
417
|
if (!prompt) return;
|
|
366
418
|
const workdir = document.getElementById('workdir-input').value.trim() || null;
|
|
367
|
-
const
|
|
419
|
+
const skipPerms = document.getElementById('skip-perms-input').checked;
|
|
420
|
+
const body = {prompt, skip_permissions: skipPerms};
|
|
368
421
|
if (workdir) body.working_dir = workdir;
|
|
369
422
|
try {
|
|
370
423
|
const res = await fetch(`${API}/tasks`, {
|
|
@@ -393,6 +446,7 @@
|
|
|
393
446
|
document.getElementById('modal-overlay').addEventListener('click', hideModal);
|
|
394
447
|
document.getElementById('submit-task-btn').addEventListener('click', submitTask);
|
|
395
448
|
document.getElementById('back-btn').addEventListener('click', showList);
|
|
449
|
+
document.getElementById('interrupt-btn').addEventListener('click', interruptTask);
|
|
396
450
|
document.getElementById('kill-btn').addEventListener('click', killTask);
|
|
397
451
|
document.getElementById('send-btn').addEventListener('click', sendMessage);
|
|
398
452
|
document.getElementById('message-input').addEventListener('keydown', e => {
|
|
@@ -403,6 +457,7 @@
|
|
|
403
457
|
});
|
|
404
458
|
|
|
405
459
|
// Init
|
|
460
|
+
loadConfig();
|
|
406
461
|
loadTasks();
|
|
407
462
|
setInterval(() => { if (!currentTaskId) loadTasks(); }, 10000);
|
|
408
463
|
</script>
|
onako/tmux_orchestrator.py
CHANGED
|
@@ -15,6 +15,7 @@ class TmuxOrchestrator:
|
|
|
15
15
|
self.session_name = session_name
|
|
16
16
|
self.db_path = db_path or DB_PATH
|
|
17
17
|
self.tasks: dict[str, dict] = {}
|
|
18
|
+
self._window_ids: dict[str, str] = {} # window_name -> @id
|
|
18
19
|
self._init_db()
|
|
19
20
|
self._load_tasks()
|
|
20
21
|
self._ensure_session()
|
|
@@ -27,7 +28,7 @@ class TmuxOrchestrator:
|
|
|
27
28
|
)
|
|
28
29
|
if result.returncode != 0:
|
|
29
30
|
subprocess.run(
|
|
30
|
-
["tmux", "new-session", "-d", "-s", self.session_name],
|
|
31
|
+
["tmux", "new-session", "-d", "-s", self.session_name, "-n", "onako-main"],
|
|
31
32
|
check=True,
|
|
32
33
|
)
|
|
33
34
|
|
|
@@ -39,15 +40,21 @@ class TmuxOrchestrator:
|
|
|
39
40
|
id TEXT PRIMARY KEY,
|
|
40
41
|
prompt TEXT,
|
|
41
42
|
status TEXT,
|
|
42
|
-
started_at TEXT
|
|
43
|
+
started_at TEXT,
|
|
44
|
+
origin TEXT DEFAULT 'managed'
|
|
43
45
|
)
|
|
44
46
|
""")
|
|
47
|
+
# Migrate existing DBs that lack the origin column
|
|
48
|
+
cursor = conn.execute("PRAGMA table_info(tasks)")
|
|
49
|
+
columns = [row[1] for row in cursor.fetchall()]
|
|
50
|
+
if "origin" not in columns:
|
|
51
|
+
conn.execute("ALTER TABLE tasks ADD COLUMN origin TEXT DEFAULT 'managed'")
|
|
45
52
|
conn.commit()
|
|
46
53
|
conn.close()
|
|
47
54
|
|
|
48
55
|
def _load_tasks(self):
|
|
49
56
|
conn = sqlite3.connect(self.db_path)
|
|
50
|
-
rows = conn.execute("SELECT id, prompt, status, started_at FROM tasks").fetchall()
|
|
57
|
+
rows = conn.execute("SELECT id, prompt, status, started_at, origin FROM tasks").fetchall()
|
|
51
58
|
conn.close()
|
|
52
59
|
for row in rows:
|
|
53
60
|
self.tasks[row[0]] = {
|
|
@@ -55,13 +62,14 @@ class TmuxOrchestrator:
|
|
|
55
62
|
"prompt": row[1],
|
|
56
63
|
"status": row[2],
|
|
57
64
|
"started_at": row[3],
|
|
65
|
+
"origin": row[4] or "managed",
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
def _save_task(self, task: dict):
|
|
61
69
|
conn = sqlite3.connect(self.db_path)
|
|
62
70
|
conn.execute(
|
|
63
|
-
"INSERT OR REPLACE INTO tasks (id, prompt, status, started_at) VALUES (?, ?, ?, ?)",
|
|
64
|
-
(task["id"], task["prompt"], task["status"], task["started_at"]),
|
|
71
|
+
"INSERT OR REPLACE INTO tasks (id, prompt, status, started_at, origin) VALUES (?, ?, ?, ?, ?)",
|
|
72
|
+
(task["id"], task["prompt"], task["status"], task["started_at"], task.get("origin", "managed")),
|
|
65
73
|
)
|
|
66
74
|
conn.commit()
|
|
67
75
|
conn.close()
|
|
@@ -69,19 +77,36 @@ class TmuxOrchestrator:
|
|
|
69
77
|
def _run_tmux(self, *args) -> subprocess.CompletedProcess:
|
|
70
78
|
return subprocess.run(["tmux", *args], capture_output=True, text=True)
|
|
71
79
|
|
|
80
|
+
def _task_target(self, task_id: str) -> str:
|
|
81
|
+
"""Return a safe tmux target for a task, using window ID when available."""
|
|
82
|
+
window_id = self._window_ids.get(task_id)
|
|
83
|
+
if window_id:
|
|
84
|
+
return window_id
|
|
85
|
+
return f"{self.session_name}:{task_id}"
|
|
86
|
+
|
|
72
87
|
def create_task(self, command: str, working_dir: str | None = None, prompt: str | None = None) -> dict:
|
|
73
88
|
self._ensure_session()
|
|
74
89
|
task_id = f"task-{secrets.token_hex(4)}"
|
|
75
90
|
self._run_tmux(
|
|
76
91
|
"new-window", "-t", self.session_name, "-n", task_id,
|
|
77
92
|
)
|
|
93
|
+
# Look up the window ID for safe targeting
|
|
94
|
+
result = self._run_tmux(
|
|
95
|
+
"list-windows", "-t", self.session_name, "-F", "#{window_name}|#{window_id}",
|
|
96
|
+
)
|
|
97
|
+
if result.stdout.strip():
|
|
98
|
+
for line in result.stdout.strip().split("\n"):
|
|
99
|
+
name, _, wid = line.partition("|")
|
|
100
|
+
if name == task_id and wid:
|
|
101
|
+
self._window_ids[task_id] = wid
|
|
102
|
+
break
|
|
78
103
|
if working_dir:
|
|
79
104
|
self._run_tmux(
|
|
80
|
-
"send-keys", "-t",
|
|
105
|
+
"send-keys", "-t", self._task_target(task_id),
|
|
81
106
|
f"cd {shlex.quote(working_dir)}", "Enter",
|
|
82
107
|
)
|
|
83
108
|
self._run_tmux(
|
|
84
|
-
"send-keys", "-t",
|
|
109
|
+
"send-keys", "-t", self._task_target(task_id),
|
|
85
110
|
command, "Enter",
|
|
86
111
|
)
|
|
87
112
|
task = {
|
|
@@ -89,12 +114,14 @@ class TmuxOrchestrator:
|
|
|
89
114
|
"prompt": prompt or command,
|
|
90
115
|
"status": "running",
|
|
91
116
|
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
117
|
+
"origin": "managed",
|
|
92
118
|
}
|
|
93
119
|
self.tasks[task_id] = task
|
|
94
120
|
self._save_task(task)
|
|
95
121
|
return task
|
|
96
122
|
|
|
97
123
|
def list_tasks(self) -> list[dict]:
|
|
124
|
+
self.rediscover_tasks()
|
|
98
125
|
self._sync_task_status()
|
|
99
126
|
return list(self.tasks.values())
|
|
100
127
|
|
|
@@ -126,32 +153,41 @@ class TmuxOrchestrator:
|
|
|
126
153
|
|
|
127
154
|
def get_raw_output(self, task_id: str) -> str:
|
|
128
155
|
result = self._run_tmux(
|
|
129
|
-
"capture-pane", "-t",
|
|
156
|
+
"capture-pane", "-t", self._task_target(task_id),
|
|
130
157
|
"-p", "-S", "-",
|
|
131
158
|
)
|
|
132
159
|
return result.stdout
|
|
133
160
|
|
|
134
161
|
def send_message(self, task_id: str, message: str):
|
|
135
162
|
self._run_tmux(
|
|
136
|
-
"send-keys", "-t",
|
|
163
|
+
"send-keys", "-t", self._task_target(task_id),
|
|
137
164
|
"-l", message,
|
|
138
165
|
)
|
|
139
166
|
self._run_tmux(
|
|
140
|
-
"send-keys", "-t",
|
|
167
|
+
"send-keys", "-t", self._task_target(task_id),
|
|
141
168
|
"Enter",
|
|
142
169
|
)
|
|
143
170
|
|
|
171
|
+
def send_interrupt(self, task_id: str):
|
|
172
|
+
self._run_tmux("send-keys", "-t", self._task_target(task_id), "Escape")
|
|
173
|
+
|
|
144
174
|
def kill_task(self, task_id: str):
|
|
145
|
-
self._run_tmux("kill-window", "-t",
|
|
175
|
+
self._run_tmux("kill-window", "-t", self._task_target(task_id))
|
|
146
176
|
if task_id in self.tasks:
|
|
147
177
|
self.tasks[task_id]["status"] = "done"
|
|
148
178
|
self._save_task(self.tasks[task_id])
|
|
149
179
|
|
|
150
180
|
def _sync_task_status(self):
|
|
151
181
|
result = self._run_tmux(
|
|
152
|
-
"list-windows", "-t", self.session_name, "-F", "#{window_name}",
|
|
182
|
+
"list-windows", "-t", self.session_name, "-F", "#{window_name}|#{window_id}",
|
|
153
183
|
)
|
|
154
|
-
active_windows = set(
|
|
184
|
+
active_windows = set()
|
|
185
|
+
if result.stdout.strip():
|
|
186
|
+
for line in result.stdout.strip().split("\n"):
|
|
187
|
+
parts = line.split("|", 1)
|
|
188
|
+
active_windows.add(parts[0])
|
|
189
|
+
if len(parts) > 1:
|
|
190
|
+
self._window_ids[parts[0]] = parts[1]
|
|
155
191
|
for task_id, task in self.tasks.items():
|
|
156
192
|
if task["status"] == "running" and task_id not in active_windows:
|
|
157
193
|
task["status"] = "done"
|
|
@@ -160,17 +196,27 @@ class TmuxOrchestrator:
|
|
|
160
196
|
def rediscover_tasks(self):
|
|
161
197
|
"""Rediscover tasks from existing tmux windows on server restart."""
|
|
162
198
|
result = self._run_tmux(
|
|
163
|
-
"list-windows", "-t", self.session_name, "-F", "#{window_name}",
|
|
199
|
+
"list-windows", "-t", self.session_name, "-F", "#{window_name}|#{window_id}",
|
|
164
200
|
)
|
|
165
201
|
if not result.stdout.strip():
|
|
166
202
|
return
|
|
167
|
-
for
|
|
168
|
-
|
|
203
|
+
for line in result.stdout.strip().split("\n"):
|
|
204
|
+
parts = line.split("|", 1)
|
|
205
|
+
window_name = parts[0]
|
|
206
|
+
window_id = parts[1] if len(parts) > 1 else None
|
|
207
|
+
if window_id:
|
|
208
|
+
self._window_ids[window_name] = window_id
|
|
209
|
+
if window_name not in self.tasks:
|
|
210
|
+
is_managed = window_name.startswith("task-")
|
|
169
211
|
task = {
|
|
170
212
|
"id": window_name,
|
|
171
|
-
"prompt": "(rediscovered)",
|
|
213
|
+
"prompt": "(rediscovered)" if is_managed else window_name,
|
|
172
214
|
"status": "running",
|
|
173
215
|
"started_at": None,
|
|
216
|
+
"origin": "managed" if is_managed else "external",
|
|
174
217
|
}
|
|
175
218
|
self.tasks[window_name] = task
|
|
176
219
|
self._save_task(task)
|
|
220
|
+
elif self.tasks[window_name]["status"] == "done":
|
|
221
|
+
self.tasks[window_name]["status"] = "running"
|
|
222
|
+
self._save_task(self.tasks[window_name])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onako
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Dispatch and monitor Claude Code tasks from your phone
|
|
5
5
|
Author: Amir
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,27 +32,21 @@ Requires [tmux](https://github.com/tmux/tmux) and [Claude Code](https://docs.ant
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
|
|
36
|
-
onako
|
|
35
|
+
onako # starts server, drops you into tmux
|
|
36
|
+
onako --session my-project # custom session name
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Open http://localhost:8787 on your phone (same network) or set up [Tailscale](https://tailscale.com) for access from anywhere.
|
|
40
|
-
|
|
41
|
-
### Run in the background
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
onako serve --background
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
This installs a system service (launchd on macOS, systemd on Linux) that starts on boot. Tasks run in the directory where you ran the command.
|
|
39
|
+
If you're already inside tmux, onako auto-detects your session and skips the attach. Open http://localhost:8787 on your phone (same network) or set up [Tailscale](https://tailscale.com) for access from anywhere.
|
|
48
40
|
|
|
49
41
|
```bash
|
|
50
|
-
onako
|
|
51
|
-
onako status
|
|
52
|
-
onako
|
|
53
|
-
onako version
|
|
42
|
+
onako stop # stop the server
|
|
43
|
+
onako status # check if running
|
|
44
|
+
onako serve # foreground server (for development)
|
|
45
|
+
onako version # print version
|
|
54
46
|
```
|
|
55
47
|
|
|
56
48
|
## How it works
|
|
57
49
|
|
|
58
|
-
|
|
50
|
+
Onako monitors all tmux windows in the configured session. Windows it creates (via the dashboard) are "managed" tasks. Windows created by you or other tools are discovered automatically as "external" — both get full dashboard support: view output, send messages, kill.
|
|
51
|
+
|
|
52
|
+
Task state is persisted in SQLite so it survives server restarts.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
onako/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
|
|
2
|
+
onako/cli.py,sha256=8nsQj0XbgRlS9DsgYfao2uc1hyTE8q9ULVyGtPOgP9I,7629
|
|
3
|
+
onako/server.py,sha256=NfP2CaecxAzuq0tSIaatfYX3d98EuLfhIwK00oHNjro,2883
|
|
4
|
+
onako/tmux_orchestrator.py,sha256=URAOdYo88SzW1ef0o9_hKxJ0SeTtuqZlex1kgTMv42w,8277
|
|
5
|
+
onako/static/index.html,sha256=ioombLZg8uhVHL64saB50ikYBUYNDgw5yClA-QoNxEo,16802
|
|
6
|
+
onako-0.4.0.dist-info/METADATA,sha256=dv588takf4ASUQzsuF7mMYPwJs1Gq4WDU741BvAwegY,1945
|
|
7
|
+
onako-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
onako-0.4.0.dist-info/entry_points.txt,sha256=51KRJzuVpr69iT_k4JO0Lj3DQv_HbgtGjTBTev13JAQ,41
|
|
9
|
+
onako-0.4.0.dist-info/top_level.txt,sha256=EZsc5qq2paM9GTbaFE9Xar4B5wFdfIqK9l_bDQVcmZ4,6
|
|
10
|
+
onako-0.4.0.dist-info/RECORD,,
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>com.onako.server</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>{onako_bin}</string>
|
|
10
|
-
<string>serve</string>
|
|
11
|
-
<string>--host</string>
|
|
12
|
-
<string>{host}</string>
|
|
13
|
-
<string>--port</string>
|
|
14
|
-
<string>{port}</string>
|
|
15
|
-
<string>--dir</string>
|
|
16
|
-
<string>{working_dir}</string>
|
|
17
|
-
</array>
|
|
18
|
-
<key>WorkingDirectory</key>
|
|
19
|
-
<string>{working_dir}</string>
|
|
20
|
-
<key>RunAtLoad</key>
|
|
21
|
-
<true/>
|
|
22
|
-
<key>KeepAlive</key>
|
|
23
|
-
<true/>
|
|
24
|
-
<key>StandardOutPath</key>
|
|
25
|
-
<string>{log_dir}/server.stdout.log</string>
|
|
26
|
-
<key>StandardErrorPath</key>
|
|
27
|
-
<string>{log_dir}/server.stderr.log</string>
|
|
28
|
-
<key>EnvironmentVariables</key>
|
|
29
|
-
<dict>
|
|
30
|
-
<key>PATH</key>
|
|
31
|
-
<string>{path_value}</string>
|
|
32
|
-
</dict>
|
|
33
|
-
</dict>
|
|
34
|
-
</plist>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
[Unit]
|
|
2
|
-
Description=Onako - Claude Code Task Orchestrator
|
|
3
|
-
After=network.target
|
|
4
|
-
|
|
5
|
-
[Service]
|
|
6
|
-
ExecStart={onako_bin} serve --host {host} --port {port} --dir {working_dir}
|
|
7
|
-
WorkingDirectory={working_dir}
|
|
8
|
-
Restart=on-failure
|
|
9
|
-
Environment=PATH={path_value}
|
|
10
|
-
|
|
11
|
-
[Install]
|
|
12
|
-
WantedBy=default.target
|
onako-0.2.1.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
onako/__init__.py,sha256=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
|
|
2
|
-
onako/cli.py,sha256=scmBfnyRAaT_yJziVQyumV6SyureGx6FXumV_r6KGxE,7217
|
|
3
|
-
onako/server.py,sha256=lBNaT8Xq5Jw8EpN1CZNWzKQ2TudAMpSE4L0PY4ucW10,2065
|
|
4
|
-
onako/tmux_orchestrator.py,sha256=vnhNda6VRhPIi28EaHJn67WiRCPvMGnoG7uwCD2MDtU,6111
|
|
5
|
-
onako/static/index.html,sha256=7TjxfF38Spnuerd6u0vbwGpJApstsxzVS3F-YUGCp7I,14403
|
|
6
|
-
onako/templates/com.onako.server.plist.tpl,sha256=XvjvRz_AnjcREVjDPFu2qGMVCxojp6hhTwMfvrFcEbY,994
|
|
7
|
-
onako/templates/onako.service.tpl,sha256=EOVOaLtxC1FrcLdy7DtEbtf9ImgN6PmnMb57ZT81nTM,280
|
|
8
|
-
onako-0.2.1.dist-info/METADATA,sha256=L52xdawAyV0GjqXFwbdGJfZnE6o4P8tGKu0oF8ZPWt0,1891
|
|
9
|
-
onako-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
-
onako-0.2.1.dist-info/entry_points.txt,sha256=51KRJzuVpr69iT_k4JO0Lj3DQv_HbgtGjTBTev13JAQ,41
|
|
11
|
-
onako-0.2.1.dist-info/top_level.txt,sha256=EZsc5qq2paM9GTbaFE9Xar4B5wFdfIqK9l_bDQVcmZ4,6
|
|
12
|
-
onako-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|