neruva-control 0.1.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.
- neruva_control-0.1.0/LICENSE +21 -0
- neruva_control-0.1.0/PKG-INFO +103 -0
- neruva_control-0.1.0/README.md +73 -0
- neruva_control-0.1.0/pyproject.toml +58 -0
- neruva_control-0.1.0/setup.cfg +4 -0
- neruva_control-0.1.0/src/neruva_control/__init__.py +22 -0
- neruva_control-0.1.0/src/neruva_control/_cli.py +163 -0
- neruva_control-0.1.0/src/neruva_control/_config.py +85 -0
- neruva_control-0.1.0/src/neruva_control/_install.py +243 -0
- neruva_control-0.1.0/src/neruva_control/_projects.py +167 -0
- neruva_control-0.1.0/src/neruva_control/_recorder.py +196 -0
- neruva_control-0.1.0/src/neruva_control/_sessions.py +329 -0
- neruva_control-0.1.0/src/neruva_control/daemon.py +503 -0
- neruva_control-0.1.0/src/neruva_control/py.typed +0 -0
- neruva_control-0.1.0/src/neruva_control.egg-info/PKG-INFO +103 -0
- neruva_control-0.1.0/src/neruva_control.egg-info/SOURCES.txt +18 -0
- neruva_control-0.1.0/src/neruva_control.egg-info/dependency_links.txt +1 -0
- neruva_control-0.1.0/src/neruva_control.egg-info/entry_points.txt +3 -0
- neruva_control-0.1.0/src/neruva_control.egg-info/requires.txt +8 -0
- neruva_control-0.1.0/src/neruva_control.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clouthier Simulation Labs
|
|
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,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: neruva-control
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local controller daemon for Neruva Cockpit -- the dashboard for agentic AI. Spawns and tails Claude Code sessions, streams them to your browser at app.neruva.io. No CLI knowledge required after install.
|
|
5
|
+
Author-email: Clouthier Simulation Labs <info@neruva.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://neruva.io/
|
|
8
|
+
Project-URL: Documentation, https://neruva.io/docs/
|
|
9
|
+
Project-URL: Source, https://github.com/CloutSimLabs/neruva
|
|
10
|
+
Keywords: neruva,cockpit,claude,claude-code,agent-memory,agentic-ai,agent-dashboard,ai
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: fastapi>=0.110
|
|
24
|
+
Requires-Dist: uvicorn[standard]>=0.27
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: websockets>=12
|
|
27
|
+
Requires-Dist: platformdirs>=4
|
|
28
|
+
Requires-Dist: tomli>=2; python_version < "3.11"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# neruva-control
|
|
32
|
+
|
|
33
|
+
Local controller daemon for **[Neruva Cockpit](https://app.neruva.io/cockpit)** —
|
|
34
|
+
the dashboard for agentic AI. Spawns and tails Claude Code sessions
|
|
35
|
+
on your machine, streams them to your browser. No CLI knowledge
|
|
36
|
+
required after install.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install neruva-control
|
|
42
|
+
neruva-control-install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The installer:
|
|
46
|
+
|
|
47
|
+
1. Generates a random auth token and stores it at
|
|
48
|
+
`~/.config/neruva/control.token` (mode 0600 on Unix).
|
|
49
|
+
2. Registers a background service so the daemon starts on login
|
|
50
|
+
(launchd on macOS, systemd-user on Linux, Task Scheduler on Windows).
|
|
51
|
+
3. Starts the daemon listening on `127.0.0.1:7331` (loopback only).
|
|
52
|
+
4. Prints a one-time URL like
|
|
53
|
+
`https://app.neruva.io/cockpit#token=<TOKEN>` — open it once and
|
|
54
|
+
the browser remembers your machine.
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
[browser at app.neruva.io]
|
|
60
|
+
↕ WebSocket (loopback :7331, token-authed)
|
|
61
|
+
[neruva-control daemon]
|
|
62
|
+
├─ HTTP/WS server on 127.0.0.1:7331
|
|
63
|
+
├─ Spawns: claude --headless --output-format stream-json
|
|
64
|
+
├─ Tails subprocess stdout, broadcasts events to browser
|
|
65
|
+
└─ Forwards user steering input → subprocess stdin
|
|
66
|
+
↕ HTTPS (existing Api-Key auth)
|
|
67
|
+
[api.neruva.io substrate]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The daemon binds to **127.0.0.1 only** — your sessions never leave
|
|
71
|
+
your machine. The browser at `app.neruva.io` connects to your local
|
|
72
|
+
daemon via a loopback WebSocket. The auth token is the shared secret
|
|
73
|
+
between daemon and browser; only the browser tab you linked has it.
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
| Command | What it does |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `neruva-control-install` | One-shot install (generates token, registers service, prints link URL). |
|
|
80
|
+
| `neruva-control start` | Run the daemon foreground (used by service). |
|
|
81
|
+
| `neruva-control status` | Show install + daemon health. |
|
|
82
|
+
| `neruva-control link` | Print the link URL again (for re-link or new browser). |
|
|
83
|
+
| `neruva-control stop` | Stop the daemon. |
|
|
84
|
+
|
|
85
|
+
## Requirements
|
|
86
|
+
|
|
87
|
+
- Python ≥3.10
|
|
88
|
+
- Claude Code installed and on `$PATH` (the daemon spawns it)
|
|
89
|
+
- A Neruva account at [neruva.io](https://neruva.io) (free tier works)
|
|
90
|
+
|
|
91
|
+
## What gets recorded
|
|
92
|
+
|
|
93
|
+
Every session's events (prompts, tool calls, file edits, shell commands,
|
|
94
|
+
MCP calls) are captured and posted to your Neruva substrate via the
|
|
95
|
+
[Records API](https://neruva.io/docs/). You can recall them anytime
|
|
96
|
+
via the Neruva MCP tools or the Cockpit memory browser.
|
|
97
|
+
|
|
98
|
+
What it **doesn't** record: IDE state, browser tabs, other apps. The
|
|
99
|
+
daemon only sees what flows through Claude Code.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# neruva-control
|
|
2
|
+
|
|
3
|
+
Local controller daemon for **[Neruva Cockpit](https://app.neruva.io/cockpit)** —
|
|
4
|
+
the dashboard for agentic AI. Spawns and tails Claude Code sessions
|
|
5
|
+
on your machine, streams them to your browser. No CLI knowledge
|
|
6
|
+
required after install.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install neruva-control
|
|
12
|
+
neruva-control-install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The installer:
|
|
16
|
+
|
|
17
|
+
1. Generates a random auth token and stores it at
|
|
18
|
+
`~/.config/neruva/control.token` (mode 0600 on Unix).
|
|
19
|
+
2. Registers a background service so the daemon starts on login
|
|
20
|
+
(launchd on macOS, systemd-user on Linux, Task Scheduler on Windows).
|
|
21
|
+
3. Starts the daemon listening on `127.0.0.1:7331` (loopback only).
|
|
22
|
+
4. Prints a one-time URL like
|
|
23
|
+
`https://app.neruva.io/cockpit#token=<TOKEN>` — open it once and
|
|
24
|
+
the browser remembers your machine.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
[browser at app.neruva.io]
|
|
30
|
+
↕ WebSocket (loopback :7331, token-authed)
|
|
31
|
+
[neruva-control daemon]
|
|
32
|
+
├─ HTTP/WS server on 127.0.0.1:7331
|
|
33
|
+
├─ Spawns: claude --headless --output-format stream-json
|
|
34
|
+
├─ Tails subprocess stdout, broadcasts events to browser
|
|
35
|
+
└─ Forwards user steering input → subprocess stdin
|
|
36
|
+
↕ HTTPS (existing Api-Key auth)
|
|
37
|
+
[api.neruva.io substrate]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The daemon binds to **127.0.0.1 only** — your sessions never leave
|
|
41
|
+
your machine. The browser at `app.neruva.io` connects to your local
|
|
42
|
+
daemon via a loopback WebSocket. The auth token is the shared secret
|
|
43
|
+
between daemon and browser; only the browser tab you linked has it.
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
| Command | What it does |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `neruva-control-install` | One-shot install (generates token, registers service, prints link URL). |
|
|
50
|
+
| `neruva-control start` | Run the daemon foreground (used by service). |
|
|
51
|
+
| `neruva-control status` | Show install + daemon health. |
|
|
52
|
+
| `neruva-control link` | Print the link URL again (for re-link or new browser). |
|
|
53
|
+
| `neruva-control stop` | Stop the daemon. |
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- Python ≥3.10
|
|
58
|
+
- Claude Code installed and on `$PATH` (the daemon spawns it)
|
|
59
|
+
- A Neruva account at [neruva.io](https://neruva.io) (free tier works)
|
|
60
|
+
|
|
61
|
+
## What gets recorded
|
|
62
|
+
|
|
63
|
+
Every session's events (prompts, tool calls, file edits, shell commands,
|
|
64
|
+
MCP calls) are captured and posted to your Neruva substrate via the
|
|
65
|
+
[Records API](https://neruva.io/docs/). You can recall them anytime
|
|
66
|
+
via the Neruva MCP tools or the Cockpit memory browser.
|
|
67
|
+
|
|
68
|
+
What it **doesn't** record: IDE state, browser tabs, other apps. The
|
|
69
|
+
daemon only sees what flows through Claude Code.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "neruva-control"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local controller daemon for Neruva Cockpit -- the dashboard for agentic AI. Spawns and tails Claude Code sessions, streams them to your browser at app.neruva.io. No CLI knowledge required after install."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Clouthier Simulation Labs", email = "info@neruva.io" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"neruva",
|
|
17
|
+
"cockpit",
|
|
18
|
+
"claude",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"agent-memory",
|
|
21
|
+
"agentic-ai",
|
|
22
|
+
"agent-dashboard",
|
|
23
|
+
"ai",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Topic :: Software Development :: Libraries",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"fastapi>=0.110",
|
|
38
|
+
"uvicorn[standard]>=0.27",
|
|
39
|
+
"httpx>=0.27",
|
|
40
|
+
"websockets>=12",
|
|
41
|
+
"platformdirs>=4",
|
|
42
|
+
"tomli>=2 ; python_version<'3.11'",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://neruva.io/"
|
|
47
|
+
Documentation = "https://neruva.io/docs/"
|
|
48
|
+
Source = "https://github.com/CloutSimLabs/neruva"
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
neruva-control = "neruva_control._cli:main"
|
|
52
|
+
neruva-control-install = "neruva_control._install:main"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
neruva_control = ["py.typed"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Neruva Control -- the local daemon behind Neruva Cockpit.
|
|
2
|
+
|
|
3
|
+
The browser at app.neruva.io connects to this daemon over a loopback
|
|
4
|
+
WebSocket (localhost:7331) to spawn and tail Claude Code sessions on
|
|
5
|
+
the user's machine. Every session's events are auto-recorded into the
|
|
6
|
+
Neruva substrate so the agent has a brain that survives across runs.
|
|
7
|
+
|
|
8
|
+
Install: ``pip install neruva-control && neruva-control-install``
|
|
9
|
+
|
|
10
|
+
After install, open https://app.neruva.io/cockpit -- the install
|
|
11
|
+
script prints a one-time URL with your machine's auth token.
|
|
12
|
+
|
|
13
|
+
Public surface for library users (rare; most users only need the CLI):
|
|
14
|
+
|
|
15
|
+
from neruva_control import daemon # FastAPI app
|
|
16
|
+
from neruva_control._sessions import SessionManager
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""CLI entry-point for `neruva-control`.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
start -- run the daemon in the foreground (blocks). Used by the OS
|
|
5
|
+
service registration. Power users can run it manually.
|
|
6
|
+
stop -- find a running daemon and SIGTERM it.
|
|
7
|
+
status -- print health: token present? daemon listening? PID?
|
|
8
|
+
link -- print the fragment URL again (for re-link or new browser).
|
|
9
|
+
serve -- alias for start (matches `uvicorn` muscle memory).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import signal
|
|
15
|
+
import socket
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from . import __version__
|
|
21
|
+
from ._config import config_dir, cockpit_link_url, load_token, token_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
PORT = 7331
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_listening(host: str = "127.0.0.1", port: int = PORT, timeout: float = 0.5) -> bool:
|
|
28
|
+
"""True if something is bound to host:port. Used by `status` and by
|
|
29
|
+
install's "is the service already up" check."""
|
|
30
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
31
|
+
s.settimeout(timeout)
|
|
32
|
+
try:
|
|
33
|
+
s.connect((host, port))
|
|
34
|
+
s.close()
|
|
35
|
+
return True
|
|
36
|
+
except OSError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_start(args: argparse.Namespace) -> int:
|
|
41
|
+
from .daemon import serve
|
|
42
|
+
host = args.bind or "127.0.0.1"
|
|
43
|
+
port = args.port or PORT
|
|
44
|
+
if host != "127.0.0.1":
|
|
45
|
+
print(
|
|
46
|
+
f"[neruva-control] WARNING: binding on {host}:{port} -- this exposes the daemon\n"
|
|
47
|
+
f" beyond loopback. The auth token is the only thing protecting the API.\n"
|
|
48
|
+
f" Recommended for remote access: Tailscale Serve over loopback instead.\n",
|
|
49
|
+
file=sys.stderr,
|
|
50
|
+
)
|
|
51
|
+
serve(host=host, port=port)
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cmd_status(_args: argparse.Namespace) -> int:
|
|
56
|
+
has_token = token_path().exists()
|
|
57
|
+
listening = _is_listening()
|
|
58
|
+
print(f"neruva-control {__version__}")
|
|
59
|
+
print(f" config dir : {config_dir()}")
|
|
60
|
+
print(f" token : {'present' if has_token else 'MISSING (run neruva-control-install)'}")
|
|
61
|
+
print(f" daemon : {'listening on 127.0.0.1:' + str(PORT) if listening else 'NOT running'}")
|
|
62
|
+
if has_token:
|
|
63
|
+
try:
|
|
64
|
+
token = load_token()
|
|
65
|
+
print(f" link URL : {cockpit_link_url(token)}")
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return 0 if (has_token and listening) else 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _cmd_link(_args: argparse.Namespace) -> int:
|
|
72
|
+
try:
|
|
73
|
+
token = load_token()
|
|
74
|
+
except FileNotFoundError as e:
|
|
75
|
+
print(str(e), file=sys.stderr)
|
|
76
|
+
return 1
|
|
77
|
+
print()
|
|
78
|
+
print("Open this URL in your browser to link this machine to Cockpit:")
|
|
79
|
+
print()
|
|
80
|
+
print(f" {cockpit_link_url(token)}")
|
|
81
|
+
print()
|
|
82
|
+
print("(Token will be stashed in your browser's localStorage; the URL")
|
|
83
|
+
print(" fragment is stripped after first load. Same machine, same browser.)")
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _cmd_stop(_args: argparse.Namespace) -> int:
|
|
88
|
+
"""Best-effort daemon stop. Reads PID from pidfile if present;
|
|
89
|
+
falls back to telling user to kill it themselves on weird OSes."""
|
|
90
|
+
pidfile = config_dir() / "control.pid"
|
|
91
|
+
if not pidfile.exists():
|
|
92
|
+
print("No pidfile -- daemon not started by neruva-control or already stopped.")
|
|
93
|
+
return 0 if not _is_listening() else 1
|
|
94
|
+
try:
|
|
95
|
+
pid = int(pidfile.read_text().strip())
|
|
96
|
+
except Exception:
|
|
97
|
+
print(f"Bad pidfile at {pidfile}; remove it manually.", file=sys.stderr)
|
|
98
|
+
return 1
|
|
99
|
+
try:
|
|
100
|
+
if sys.platform == "win32":
|
|
101
|
+
import ctypes
|
|
102
|
+
PROCESS_TERMINATE = 1
|
|
103
|
+
handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, pid)
|
|
104
|
+
if handle:
|
|
105
|
+
ctypes.windll.kernel32.TerminateProcess(handle, 0)
|
|
106
|
+
ctypes.windll.kernel32.CloseHandle(handle)
|
|
107
|
+
else:
|
|
108
|
+
import os as _os
|
|
109
|
+
_os.kill(pid, signal.SIGTERM)
|
|
110
|
+
# Wait briefly for socket to free
|
|
111
|
+
for _ in range(20):
|
|
112
|
+
if not _is_listening():
|
|
113
|
+
break
|
|
114
|
+
time.sleep(0.1)
|
|
115
|
+
try: pidfile.unlink()
|
|
116
|
+
except Exception: pass
|
|
117
|
+
print(f"Stopped daemon (pid {pid}).")
|
|
118
|
+
return 0
|
|
119
|
+
except ProcessLookupError:
|
|
120
|
+
print("Daemon process already gone.")
|
|
121
|
+
try: pidfile.unlink()
|
|
122
|
+
except Exception: pass
|
|
123
|
+
return 0
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Failed to stop pid {pid}: {e}", file=sys.stderr)
|
|
126
|
+
return 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main(argv: list[str] | None = None) -> int:
|
|
130
|
+
parser = argparse.ArgumentParser(
|
|
131
|
+
prog="neruva-control",
|
|
132
|
+
description="Local controller daemon for Neruva Cockpit.",
|
|
133
|
+
)
|
|
134
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
135
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
136
|
+
p_start = sub.add_parser("start", help="Run the daemon (foreground; blocks).")
|
|
137
|
+
p_start.add_argument("--bind", default=None,
|
|
138
|
+
help="Bind address (default 127.0.0.1, loopback only). Pass "
|
|
139
|
+
"0.0.0.0 to expose on LAN -- only do this if you trust "
|
|
140
|
+
"your network. For remote access prefer Tailscale Serve.")
|
|
141
|
+
p_start.add_argument("--port", type=int, default=None, help=f"Port (default {PORT})")
|
|
142
|
+
p_serve = sub.add_parser("serve", help="Alias for start.")
|
|
143
|
+
p_serve.add_argument("--bind", default=None)
|
|
144
|
+
p_serve.add_argument("--port", type=int, default=None)
|
|
145
|
+
sub.add_parser("status", help="Show install + daemon status.")
|
|
146
|
+
sub.add_parser("link", help="Print the browser link URL.")
|
|
147
|
+
sub.add_parser("stop", help="Stop the daemon.")
|
|
148
|
+
args = parser.parse_args(argv)
|
|
149
|
+
handler = {
|
|
150
|
+
"start": _cmd_start,
|
|
151
|
+
"serve": _cmd_start,
|
|
152
|
+
"status": _cmd_status,
|
|
153
|
+
"link": _cmd_link,
|
|
154
|
+
"stop": _cmd_stop,
|
|
155
|
+
}.get(args.cmd)
|
|
156
|
+
if not handler:
|
|
157
|
+
parser.print_help()
|
|
158
|
+
return 2
|
|
159
|
+
return handler(args)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
sys.exit(main())
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Config + token I/O for neruva-control.
|
|
2
|
+
|
|
3
|
+
Single source of truth for where files live (cross-platform via
|
|
4
|
+
platformdirs). The token is the shared secret between the daemon
|
|
5
|
+
process and the browser at app.neruva.io -- generated once at
|
|
6
|
+
install, never rotated unless the user explicitly re-installs.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import secrets
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from platformdirs import user_config_dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
CONFIG_APP = "neruva"
|
|
17
|
+
CONFIG_AUTHOR = "neruva"
|
|
18
|
+
|
|
19
|
+
# Default substrate base. Overridable via NERUVA_API_BASE env (or future
|
|
20
|
+
# control.toml setting) for self-hosted / staging.
|
|
21
|
+
DEFAULT_API_BASE = "https://api.neruva.io"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def neruva_api_base() -> str:
|
|
25
|
+
return os.environ.get("NERUVA_API_BASE", DEFAULT_API_BASE)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def neruva_api_key() -> str | None:
|
|
29
|
+
"""User's Neruva API key for posting recorded events. Read from env
|
|
30
|
+
first, then ~/.config/neruva/api.key as a stable per-machine option.
|
|
31
|
+
Returns None if unset -- daemon then runs in 'no-record' mode."""
|
|
32
|
+
k = os.environ.get("NERUVA_API_KEY")
|
|
33
|
+
if k:
|
|
34
|
+
return k.strip()
|
|
35
|
+
p = config_dir() / "api.key"
|
|
36
|
+
if p.exists():
|
|
37
|
+
try:
|
|
38
|
+
return p.read_text(encoding="utf-8").strip() or None
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def config_dir() -> Path:
|
|
45
|
+
p = Path(user_config_dir(CONFIG_APP, CONFIG_AUTHOR))
|
|
46
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
return p
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def token_path() -> Path:
|
|
51
|
+
return config_dir() / "control.token"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def config_path() -> Path:
|
|
55
|
+
return config_dir() / "control.toml"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_token() -> str:
|
|
59
|
+
"""Read the shared token. Raises FileNotFoundError if not installed."""
|
|
60
|
+
p = token_path()
|
|
61
|
+
if not p.exists():
|
|
62
|
+
raise FileNotFoundError(
|
|
63
|
+
f"No token at {p}. Run `neruva-control-install` first."
|
|
64
|
+
)
|
|
65
|
+
return p.read_text(encoding="utf-8").strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def write_token(token: str) -> Path:
|
|
69
|
+
"""Write a new token (overwrite). Returns the path."""
|
|
70
|
+
p = token_path()
|
|
71
|
+
p.write_text(token, encoding="utf-8")
|
|
72
|
+
# chmod 600 on Unix; on Windows ACLs already restrict to user
|
|
73
|
+
if os.name == "posix":
|
|
74
|
+
os.chmod(p, 0o600)
|
|
75
|
+
return p
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def gen_token() -> str:
|
|
79
|
+
return secrets.token_urlsafe(32)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cockpit_link_url(token: str, base: str = "https://app.neruva.io") -> str:
|
|
83
|
+
"""Fragment URL the installer prints. Browser reads #token=..., stashes
|
|
84
|
+
in localStorage, then strips via history.replaceState."""
|
|
85
|
+
return f"{base}/cockpit#token={token}"
|