remotedev-agent 1.3.0__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.
Files changed (32) hide show
  1. remotedev_agent-1.3.0/LICENSE +21 -0
  2. remotedev_agent-1.3.0/MANIFEST.in +3 -0
  3. remotedev_agent-1.3.0/PKG-INFO +133 -0
  4. remotedev_agent-1.3.0/README.md +96 -0
  5. remotedev_agent-1.3.0/pyproject.toml +59 -0
  6. remotedev_agent-1.3.0/remotedev_agent/__init__.py +3 -0
  7. remotedev_agent-1.3.0/remotedev_agent/cli.py +265 -0
  8. remotedev_agent-1.3.0/remotedev_agent/config.py +37 -0
  9. remotedev_agent-1.3.0/remotedev_agent/daemon.py +541 -0
  10. remotedev_agent-1.3.0/remotedev_agent/diagnostics.py +205 -0
  11. remotedev_agent-1.3.0/remotedev_agent/discovery.py +33 -0
  12. remotedev_agent-1.3.0/remotedev_agent/git_ops.py +34 -0
  13. remotedev_agent-1.3.0/remotedev_agent/process_manager.py +460 -0
  14. remotedev_agent-1.3.0/remotedev_agent/pty_manager.py +286 -0
  15. remotedev_agent-1.3.0/remotedev_agent/sandbox_manager.py +46 -0
  16. remotedev_agent-1.3.0/remotedev_agent/service/__init__.py +0 -0
  17. remotedev_agent-1.3.0/remotedev_agent/service/linux.py +102 -0
  18. remotedev_agent-1.3.0/remotedev_agent/service/macos.py +180 -0
  19. remotedev_agent-1.3.0/remotedev_agent.egg-info/PKG-INFO +133 -0
  20. remotedev_agent-1.3.0/remotedev_agent.egg-info/SOURCES.txt +30 -0
  21. remotedev_agent-1.3.0/remotedev_agent.egg-info/dependency_links.txt +1 -0
  22. remotedev_agent-1.3.0/remotedev_agent.egg-info/entry_points.txt +2 -0
  23. remotedev_agent-1.3.0/remotedev_agent.egg-info/requires.txt +9 -0
  24. remotedev_agent-1.3.0/remotedev_agent.egg-info/top_level.txt +1 -0
  25. remotedev_agent-1.3.0/setup.cfg +4 -0
  26. remotedev_agent-1.3.0/tests/test_daemon.py +208 -0
  27. remotedev_agent-1.3.0/tests/test_diagnostics.py +195 -0
  28. remotedev_agent-1.3.0/tests/test_discovery.py +29 -0
  29. remotedev_agent-1.3.0/tests/test_macos_installer.py +100 -0
  30. remotedev_agent-1.3.0/tests/test_process_manager.py +123 -0
  31. remotedev_agent-1.3.0/tests/test_pty_manager.py +350 -0
  32. remotedev_agent-1.3.0/tests/test_scrollback_persistence.py +167 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shaun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include remotedev_agent *.py
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: remotedev-agent
3
+ Version: 1.3.0
4
+ Summary: Agent daemon for Remote AI Maestro — manages PTY sessions and AI agent subprocesses on remote machines
5
+ Author-email: Shaun <scwj1210@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shaunchew/remote-ai-maestro
8
+ Project-URL: Repository, https://github.com/shaunchew/remote-ai-maestro
9
+ Project-URL: Documentation, https://github.com/shaunchew/remote-ai-maestro#readme
10
+ Project-URL: Issues, https://github.com/shaunchew/remote-ai-maestro/issues
11
+ Keywords: remote,ai,agent,terminal,pty,tmux,claude,codex
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: System :: Shells
24
+ Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: websockets>=13.0
29
+ Requires-Dist: click>=8.0
30
+ Requires-Dist: ptyprocess>=0.7.0
31
+ Requires-Dist: watchdog>=3.0
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest>=8.0; extra == "test"
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
35
+ Requires-Dist: pytest-mock>=3.12; extra == "test"
36
+ Dynamic: license-file
37
+
38
+ # remotedev-agent
39
+
40
+ Agent daemon for [Remote AI Maestro](https://remote-ai-maestro.thesavvydeveloper.com) -- connects your machine to the platform so you can remotely control AI coding agents (Claude Code, Codex, Aider, Copilot, Cursor) via a browser-based terminal.
41
+
42
+ ## What it does
43
+
44
+ - Opens tmux-backed PTY sessions that survive disconnects and daemon restarts
45
+ - Discovers AI agents available on your machine's PATH
46
+ - Streams terminal I/O to the Remote AI Maestro relay over WebSocket
47
+ - Manages sandbox directories (create, clone repos, delete)
48
+ - Installs as a background service (launchd on macOS, systemd on Linux)
49
+
50
+ ## Requirements
51
+
52
+ - Python 3.10+
53
+ - tmux installed and on PATH
54
+ - macOS or Linux
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install remotedev-agent
60
+ ```
61
+
62
+ ## Quick start
63
+
64
+ 1. Register a machine on the [Remote AI Maestro dashboard](https://remote-ai-maestro.thesavvydeveloper.com) to get your machine ID and token.
65
+
66
+ 2. Configure the agent:
67
+
68
+ ```bash
69
+ remotedev-agent configure \
70
+ --relay-url wss://relay.thesavvydeveloper.com \
71
+ --machine-id YOUR_MACHINE_ID \
72
+ --token YOUR_MACHINE_TOKEN
73
+ ```
74
+
75
+ 3. Start the agent:
76
+
77
+ ```bash
78
+ remotedev-agent start
79
+ ```
80
+
81
+ The agent connects to the relay and begins accepting terminal sessions from the web dashboard.
82
+
83
+ ## Interactive setup
84
+
85
+ If you prefer prompts instead of flags:
86
+
87
+ ```bash
88
+ remotedev-agent setup
89
+ ```
90
+
91
+ ## Install as a background service
92
+
93
+ ```bash
94
+ remotedev-agent install-service
95
+ ```
96
+
97
+ This creates a launchd plist (macOS) or systemd unit (Linux) so the agent starts automatically on boot.
98
+
99
+ To remove the service:
100
+
101
+ ```bash
102
+ remotedev-agent uninstall-service
103
+ ```
104
+
105
+ ## Configuration
106
+
107
+ Config is stored at `~/.remotedev-agent/config.json` with three fields:
108
+
109
+ | Field | Description |
110
+ |-------|-------------|
111
+ | `relay_url` | WebSocket URL of the relay server |
112
+ | `machine_id` | UUID assigned when you register the machine |
113
+ | `token` | Machine authentication token |
114
+
115
+ ## How it works
116
+
117
+ The agent daemon maintains a persistent WebSocket connection to the Remote AI Maestro relay server. When a user opens a sandbox terminal in their browser, the relay forwards the request to the agent, which creates a tmux-backed PTY session on your machine. All terminal input/output is streamed in real time through the relay.
118
+
119
+ AI agents (Claude Code, Codex, etc.) are discovered by probing your PATH at startup. Users can invoke any discovered agent from the browser terminal.
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ # Install in editable mode with test dependencies
125
+ pip install -e ".[test]"
126
+
127
+ # Run tests
128
+ pytest
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,96 @@
1
+ # remotedev-agent
2
+
3
+ Agent daemon for [Remote AI Maestro](https://remote-ai-maestro.thesavvydeveloper.com) -- connects your machine to the platform so you can remotely control AI coding agents (Claude Code, Codex, Aider, Copilot, Cursor) via a browser-based terminal.
4
+
5
+ ## What it does
6
+
7
+ - Opens tmux-backed PTY sessions that survive disconnects and daemon restarts
8
+ - Discovers AI agents available on your machine's PATH
9
+ - Streams terminal I/O to the Remote AI Maestro relay over WebSocket
10
+ - Manages sandbox directories (create, clone repos, delete)
11
+ - Installs as a background service (launchd on macOS, systemd on Linux)
12
+
13
+ ## Requirements
14
+
15
+ - Python 3.10+
16
+ - tmux installed and on PATH
17
+ - macOS or Linux
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install remotedev-agent
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ 1. Register a machine on the [Remote AI Maestro dashboard](https://remote-ai-maestro.thesavvydeveloper.com) to get your machine ID and token.
28
+
29
+ 2. Configure the agent:
30
+
31
+ ```bash
32
+ remotedev-agent configure \
33
+ --relay-url wss://relay.thesavvydeveloper.com \
34
+ --machine-id YOUR_MACHINE_ID \
35
+ --token YOUR_MACHINE_TOKEN
36
+ ```
37
+
38
+ 3. Start the agent:
39
+
40
+ ```bash
41
+ remotedev-agent start
42
+ ```
43
+
44
+ The agent connects to the relay and begins accepting terminal sessions from the web dashboard.
45
+
46
+ ## Interactive setup
47
+
48
+ If you prefer prompts instead of flags:
49
+
50
+ ```bash
51
+ remotedev-agent setup
52
+ ```
53
+
54
+ ## Install as a background service
55
+
56
+ ```bash
57
+ remotedev-agent install-service
58
+ ```
59
+
60
+ This creates a launchd plist (macOS) or systemd unit (Linux) so the agent starts automatically on boot.
61
+
62
+ To remove the service:
63
+
64
+ ```bash
65
+ remotedev-agent uninstall-service
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ Config is stored at `~/.remotedev-agent/config.json` with three fields:
71
+
72
+ | Field | Description |
73
+ |-------|-------------|
74
+ | `relay_url` | WebSocket URL of the relay server |
75
+ | `machine_id` | UUID assigned when you register the machine |
76
+ | `token` | Machine authentication token |
77
+
78
+ ## How it works
79
+
80
+ The agent daemon maintains a persistent WebSocket connection to the Remote AI Maestro relay server. When a user opens a sandbox terminal in their browser, the relay forwards the request to the agent, which creates a tmux-backed PTY session on your machine. All terminal input/output is streamed in real time through the relay.
81
+
82
+ AI agents (Claude Code, Codex, etc.) are discovered by probing your PATH at startup. Users can invoke any discovered agent from the browser terminal.
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ # Install in editable mode with test dependencies
88
+ pip install -e ".[test]"
89
+
90
+ # Run tests
91
+ pytest
92
+ ```
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "remotedev-agent"
3
+ version = "1.3.0"
4
+ description = "Agent daemon for Remote AI Maestro — manages PTY sessions and AI agent subprocesses on remote machines"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "Shaun", email = "scwj1210@gmail.com" },
10
+ ]
11
+ keywords = ["remote", "ai", "agent", "terminal", "pty", "tmux", "claude", "codex"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: MacOS",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries",
24
+ "Topic :: System :: Shells",
25
+ "Topic :: Terminals :: Terminal Emulators/X Terminals",
26
+ ]
27
+ dependencies = [
28
+ "websockets>=13.0",
29
+ "click>=8.0",
30
+ "ptyprocess>=0.7.0",
31
+ "watchdog>=3.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/shaunchew/remote-ai-maestro"
36
+ Repository = "https://github.com/shaunchew/remote-ai-maestro"
37
+ Documentation = "https://github.com/shaunchew/remote-ai-maestro#readme"
38
+ Issues = "https://github.com/shaunchew/remote-ai-maestro/issues"
39
+
40
+ [project.scripts]
41
+ remotedev-agent = "remotedev_agent.cli:cli"
42
+
43
+ [project.optional-dependencies]
44
+ test = [
45
+ "pytest>=8.0",
46
+ "pytest-asyncio>=0.23",
47
+ "pytest-mock>=3.12",
48
+ ]
49
+
50
+ [build-system]
51
+ requires = ["setuptools>=68.0"]
52
+ build-backend = "setuptools.build_meta"
53
+
54
+ [tool.setuptools.packages.find]
55
+ include = ["remotedev_agent*"]
56
+
57
+ [tool.pytest.ini_options]
58
+ asyncio_mode = "auto"
59
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """Remote AI Maestro agent daemon."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,265 @@
1
+ import asyncio
2
+ import os
3
+
4
+ import click
5
+
6
+ from remotedev_agent.config import AgentConfig
7
+
8
+
9
+ def _package_version() -> str:
10
+ """Resolve the installed package version. Falls back to the literal in
11
+ pyproject.toml when running from a source checkout without metadata."""
12
+ try:
13
+ from importlib.metadata import version as _v
14
+ return _v("remotedev-agent")
15
+ except Exception:
16
+ return "unknown"
17
+
18
+
19
+ @click.group()
20
+ @click.version_option(version=_package_version(), prog_name="remotedev-agent")
21
+ def cli():
22
+ """Remote AI Maestro Agent — connects your machine to Remote AI Maestro."""
23
+
24
+
25
+ @cli.command()
26
+ @click.option("--relay-url", prompt="Relay URL", help="WebSocket relay URL (e.g. ws://localhost:8001)")
27
+ @click.option("--machine-id", prompt="Machine ID", help="Machine UUID from dashboard")
28
+ @click.option("--token", prompt="Machine token", hide_input=True, help="Machine registration token")
29
+ def setup(relay_url: str, machine_id: str, token: str):
30
+ """Configure the agent with relay connection details."""
31
+ config = AgentConfig(relay_url=relay_url, machine_id=machine_id, token=token)
32
+ config.save()
33
+ click.echo("Config saved.")
34
+ click.echo("To run hidden in the background (recommended): remotedev-agent install-service")
35
+ click.echo("Or quick-detach for one-off: remotedev-agent start --detach")
36
+
37
+
38
+ @cli.command()
39
+ @click.option("--relay-url", default=None)
40
+ @click.option("--machine-id", default=None)
41
+ @click.option("--token", default=None)
42
+ @click.option(
43
+ "--no-verify", is_flag=True,
44
+ help="Skip the post-config connectivity smoke check.",
45
+ )
46
+ def configure(relay_url: str | None, machine_id: str | None, token: str | None, no_verify: bool):
47
+ """Non-interactive configuration (for scripting/EC2 user-data).
48
+
49
+ By default, runs a 10-second connectivity check against the relay after
50
+ saving — surfaces token mismatches or network problems immediately
51
+ instead of letting the daemon retry-loop in silence. Pass `--no-verify`
52
+ to skip (e.g. when configuring offline before bringing the network up).
53
+ """
54
+ if not all([relay_url, machine_id, token]):
55
+ click.echo("All of --relay-url, --machine-id, --token are required.", err=True)
56
+ raise SystemExit(1)
57
+ config = AgentConfig(relay_url=relay_url, machine_id=machine_id, token=token)
58
+ config.save()
59
+ click.echo("Config saved.")
60
+ if no_verify:
61
+ return
62
+ # Smoke check — informational. We don't exit non-zero on failure because
63
+ # configure is a config-write op (the user may be configuring on one
64
+ # network and connecting from another). The output is enough to catch
65
+ # the common cases without blocking the install.
66
+ from remotedev_agent.diagnostics import probe_ws_connect, summarize, probe_tcp_v4, probe_tcp_v6
67
+ click.echo("Verifying connectivity (10s)...")
68
+ async def _check():
69
+ v4 = await probe_tcp_v4(relay_url)
70
+ v6 = await probe_tcp_v6(relay_url)
71
+ ws = await probe_ws_connect(relay_url, machine_id, token)
72
+ return [v4, v6, ws]
73
+ try:
74
+ probes = asyncio.run(_check())
75
+ except Exception as e:
76
+ click.echo(f" (smoke check skipped: {e})")
77
+ return
78
+ click.echo(summarize(probes))
79
+
80
+
81
+ @cli.command()
82
+ def diagnose():
83
+ """Run a full self-diagnostic against the configured relay.
84
+
85
+ Output is designed to be pasted into a bug report. Probes:
86
+ - DNS A/AAAA records
87
+ - raw TCP connect over IPv4 and IPv6 (isolates blackholed-v6 networks)
88
+ - full WebSocket handshake against /ws/agent/{id} with the saved token
89
+ (distinguishes auth failures from network problems)
90
+ """
91
+ from remotedev_agent.diagnostics import run_full_diagnose, format_report, summarize
92
+ try:
93
+ config = AgentConfig.load()
94
+ except FileNotFoundError as e:
95
+ click.echo(str(e), err=True)
96
+ raise SystemExit(1)
97
+ click.echo(f"Diagnosing connectivity for machine {config.machine_id}")
98
+ click.echo(f"Relay URL: {config.relay_url}\n")
99
+ probes = asyncio.run(run_full_diagnose(config))
100
+ click.echo(format_report(probes))
101
+ click.echo("")
102
+ click.echo(summarize(probes))
103
+ # Exit non-zero if the WS probe failed — makes diagnose usable in scripts
104
+ # ("if remotedev-agent diagnose; then ... fi").
105
+ if not probes[-1].ok:
106
+ raise SystemExit(2)
107
+
108
+
109
+ @cli.command()
110
+ @click.option(
111
+ "--detach", "-d", is_flag=True,
112
+ help="Run in the background, detached from the terminal. Logs go to ~/.remotedev/agent.log.",
113
+ )
114
+ def start(detach: bool):
115
+ """Start the agent daemon. Use --detach to run in the background."""
116
+ from remotedev_agent import process_manager
117
+
118
+ try:
119
+ config = AgentConfig.load()
120
+ except FileNotFoundError as e:
121
+ click.echo(str(e), err=True)
122
+ click.echo("Run 'remotedev-agent setup' or 'remotedev-agent configure ...' first.", err=True)
123
+ raise SystemExit(1)
124
+
125
+ def _run():
126
+ # Imported post-fork to avoid copying daemon-side resources (websockets,
127
+ # asyncio, ptyprocess) into the short-lived parent.
128
+ from remotedev_agent.daemon import AgentDaemon
129
+ daemon = AgentDaemon(config)
130
+ asyncio.run(daemon.run())
131
+
132
+ if not detach:
133
+ click.echo(f"Starting agent for machine {config.machine_id}...")
134
+ _run()
135
+ return
136
+
137
+ try:
138
+ pid = process_manager.detach_and_run(_run)
139
+ except RuntimeError as e:
140
+ click.echo(str(e), err=True)
141
+ raise SystemExit(1)
142
+ click.echo(f"Agent running in background (PID {pid}).")
143
+ click.echo(f"Logs: {process_manager.LOG_FILE}")
144
+ click.echo("View: remotedev-agent logs -f")
145
+ click.echo("Stop: remotedev-agent stop")
146
+ click.echo("Status: remotedev-agent status")
147
+
148
+
149
+ @cli.command("stop")
150
+ def cmd_stop():
151
+ """Stop the detached agent (no effect on tmux sandbox sessions)."""
152
+ from remotedev_agent import process_manager
153
+ svc = process_manager.service_status()
154
+ if svc["running"]:
155
+ click.echo(
156
+ f"Agent is running as a {svc['kind']} service, not in detached mode.\n"
157
+ "Stop the service with 'remotedev-agent uninstall-service' "
158
+ f"(or '{'launchctl unload ~/Library/LaunchAgents/' + process_manager.LAUNCHD_LABEL + '.plist' if svc['kind'] == 'launchd' else 'systemctl --user stop ' + process_manager.SYSTEMD_UNIT}').",
159
+ err=True,
160
+ )
161
+ raise SystemExit(1)
162
+ ok, message = process_manager.stop()
163
+ click.echo(message)
164
+ # Stale-PID cleanup is a successful no-op for callers, not a failure.
165
+ if not ok and "stale" not in message.lower() and "not running" not in message.lower():
166
+ raise SystemExit(1)
167
+
168
+
169
+ @cli.command("status")
170
+ def cmd_status():
171
+ """Show whether the agent is running (detached or as a service)."""
172
+ from remotedev_agent import process_manager
173
+ info = process_manager.status()
174
+ if info["service_running"]:
175
+ click.echo(f"Agent is running as a {info['service_kind']} service.")
176
+ elif info["detached_running"]:
177
+ click.echo(f"Agent is running in detached mode (PID {info['pid']}).")
178
+ elif info["pid"]:
179
+ click.echo(f"Agent is NOT running (stale PID {info['pid']} in PID file).")
180
+ else:
181
+ click.echo("Agent is NOT running.")
182
+ if info["service_kind"]:
183
+ click.echo(f"Service: {info['service_kind']} ({'running' if info['service_running'] else 'stopped/not loaded'})")
184
+ click.echo(f"PID file: {info['pid_file']}")
185
+ click.echo(f"Log file: {info['log_file']}")
186
+
187
+
188
+ @cli.command()
189
+ @click.option("--follow", "-f", is_flag=True, help="Stream new log lines as they arrive.")
190
+ @click.option("--lines", "-n", type=click.IntRange(min=1), default=200, show_default=True, help="Number of recent lines to print.")
191
+ def logs(follow: bool, lines: int):
192
+ """View agent logs without affecting any running session."""
193
+ from remotedev_agent import process_manager
194
+ process_manager.tail_log(lines=lines, follow=follow)
195
+
196
+
197
+ @cli.command()
198
+ def restart():
199
+ """Restart the agent (works for both detached and service mode)."""
200
+ import subprocess
201
+ import sys
202
+ from remotedev_agent import process_manager
203
+ svc = process_manager.service_status()
204
+ if svc["kind"] == "launchd":
205
+ # `kickstart -k` is the atomic stop+restart on macOS; falls back to
206
+ # legacy stop/start if the modern domain syntax isn't supported.
207
+ uid = os.getuid() if hasattr(os, "getuid") else None
208
+ target = f"gui/{uid}/{process_manager.LAUNCHD_LABEL}" if uid is not None else process_manager.LAUNCHD_LABEL
209
+ res = subprocess.run(["launchctl", "kickstart", "-k", target], capture_output=True, text=True)
210
+ if res.returncode == 0:
211
+ click.echo("Service restarted (launchd).")
212
+ return
213
+ stop_res = subprocess.run(["launchctl", "stop", process_manager.LAUNCHD_LABEL], capture_output=True, text=True)
214
+ start_res = subprocess.run(["launchctl", "start", process_manager.LAUNCHD_LABEL], capture_output=True, text=True)
215
+ if start_res.returncode == 0:
216
+ click.echo("Service restarted (launchd, fallback path).")
217
+ else:
218
+ click.echo(f"Failed to restart launchd service: {start_res.stderr.strip() or stop_res.stderr.strip() or res.stderr.strip()}", err=True)
219
+ raise SystemExit(1)
220
+ return
221
+ if svc["kind"] == "systemd":
222
+ # If the unit exists at all, prefer systemctl restart — even if the
223
+ # unit is currently inactive/failed, restart will bring it up cleanly
224
+ # and avoid forking a competing detached agent.
225
+ subprocess.run(["systemctl", "--user", "restart", process_manager.SYSTEMD_UNIT], check=True)
226
+ click.echo("Service restarted (systemd).")
227
+ return
228
+ process_manager.stop()
229
+ subprocess.run([sys.executable, "-m", "remotedev_agent.cli", "start", "--detach"], check=False)
230
+
231
+
232
+ @cli.command("install-service")
233
+ def install_service():
234
+ """Install as a background service (launchd on macOS, systemd on Linux)."""
235
+ import platform
236
+ system = platform.system()
237
+ if system == "Darwin":
238
+ from remotedev_agent.service.macos import install
239
+ install()
240
+ elif system == "Linux":
241
+ from remotedev_agent.service.linux import install
242
+ install()
243
+ else:
244
+ click.echo(f"Unsupported platform: {system}", err=True)
245
+ raise SystemExit(1)
246
+
247
+
248
+ @cli.command("uninstall-service")
249
+ def uninstall_service():
250
+ """Uninstall the background service."""
251
+ import platform
252
+ system = platform.system()
253
+ if system == "Darwin":
254
+ from remotedev_agent.service.macos import uninstall
255
+ uninstall()
256
+ elif system == "Linux":
257
+ from remotedev_agent.service.linux import uninstall
258
+ uninstall()
259
+ else:
260
+ click.echo(f"Unsupported platform: {system}", err=True)
261
+ raise SystemExit(1)
262
+
263
+
264
+ if __name__ == "__main__":
265
+ cli()
@@ -0,0 +1,37 @@
1
+ import json
2
+ import os
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ CONFIG_DIR = Path.home() / ".remotedev"
7
+ CONFIG_FILE = CONFIG_DIR / "config.json"
8
+
9
+
10
+ @dataclass
11
+ class AgentConfig:
12
+ relay_url: str
13
+ machine_id: str
14
+ token: str
15
+ sandboxes_root: str = ""
16
+
17
+ def __post_init__(self):
18
+ if not self.sandboxes_root:
19
+ self.sandboxes_root = str(Path.home() / "Documents")
20
+
21
+ def save(self):
22
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
23
+ os.chmod(CONFIG_DIR, 0o700)
24
+ CONFIG_FILE.write_text(json.dumps({
25
+ "relay_url": self.relay_url,
26
+ "machine_id": self.machine_id,
27
+ "token": self.token,
28
+ "sandboxes_root": self.sandboxes_root,
29
+ }, indent=2))
30
+ os.chmod(CONFIG_FILE, 0o600)
31
+
32
+ @classmethod
33
+ def load(cls) -> "AgentConfig":
34
+ if not CONFIG_FILE.exists():
35
+ raise FileNotFoundError(f"Config not found at {CONFIG_FILE}. Run 'remotedev-agent setup' first.")
36
+ data = json.loads(CONFIG_FILE.read_text())
37
+ return cls(**data)