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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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}"