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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onako
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Dispatch and monitor Claude Code tasks from your phone
5
5
  Author: Amir
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "onako"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Dispatch and monitor Claude Code tasks from your phone"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- click.echo(f"Onako server stopped (pid {pid}).")
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
- click.echo("Onako server: running")
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
- click.echo(f"Onako server already running (pid {PID_FILE.read_text().strip()})")
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 task_id == "onako-main":
92
- raise HTTPException(status_code=400, detail="Cannot kill the main window")
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