nxplora 0.16.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,7 @@
1
+ .env*
2
+ __pycache__/
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .DS_Store
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.5
2
+ Name: nxplora
3
+ Version: 0.16.0
4
+ Summary: NX in your terminal, on your Nexplora account.
5
+ License: Proprietary
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: certifi
8
+ Requires-Dist: textual<2,>=1.0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # nx-cli
12
+
13
+ **Private source. Proprietary. Published to PyPI as `nxplora` only when ready.**
14
+
15
+ The NX terminal client: a thin client of a person's Nexplora account. Each message is a real run on the same engine the
16
+ chat uses, with the account's approvals and billing. It shows what really came back and nothing else.
17
+
18
+ ## Charter
19
+
20
+ | nx-cli OWNS | nx-cli NEVER contains |
21
+ |---|---|
22
+ | Sign-in / enrollment with nx-cloud | Brain logic: routing, prompts, CodeChat harness, council, cognition router, brain core (these stay in private `Nexploraai/NX`) |
23
+ | Terminal UI, streaming, rendering | Model IDs, provider URLs or provider keys |
24
+ | Actions that must run on the operator's own machine (host, computer, phone) — only under nx-cloud-issued, approval-gated commands | Global Nexplora / NX secrets |
25
+ | Its own update channel | Any upload / publish path to a package index other than the gated release job |
26
+
27
+ Every published build must pass a check that none of the private NX modules are present in the wheel.
28
+
29
+ ## What it does
30
+
31
+ - **In a Nexplora Code session's terminal** it is already signed in: the app writes a short-lived session ticket (bound to the
32
+ person, the Operation and that session, one hour, usable only for sending a message there) to a private file, and renews it
33
+ while the page holds the terminal. The person's login is never on that computer. The ticket can't approve anything.
34
+ - **On any other computer** `nx` shows a short link and a code. Approve it in the browser and it's done. The sign-in stays in
35
+ `~/.nx-cli/credentials.json` (0600). `nx logout` ends it. Work runs on the person's Nexplora cloud computer, not this one.
36
+ - **Approvals** are given in the chat. `nx` says what is waiting and where, then follows the run as it goes on.
37
+ - **Colours** are the terminal's own, with no painted background, so it matches a light or dark panel.
38
+
39
+ ```bash
40
+ python -m pip install .
41
+ nx
42
+ ```
43
+
44
+ ## Checks
45
+
46
+ ```bash
47
+ python -m pytest # includes a real local HTTP server (tests/stub_server.py)
48
+ python -m build --wheel && python scripts/check_wheel.py dist/*.whl # the charter's check; a release must pass it
49
+ python scripts/pty_proof.py "$(command -v nx)" /tmp/nx-proof # real pty, reads the screen, fails on any painted background
50
+ ```
51
+
52
+ ## PyPI
53
+
54
+ The `nxplora` project name is owned and all historical releases are yanked (2026-09-29). Existing installs
55
+ run `nx upgrade` → `pip install -U nxplora`, so the first nx-cli release published under `nxplora` becomes
56
+ the migration path for those users. Never delete the PyPI project (name-takeover risk).
@@ -0,0 +1,46 @@
1
+ # nx-cli
2
+
3
+ **Private source. Proprietary. Published to PyPI as `nxplora` only when ready.**
4
+
5
+ The NX terminal client: a thin client of a person's Nexplora account. Each message is a real run on the same engine the
6
+ chat uses, with the account's approvals and billing. It shows what really came back and nothing else.
7
+
8
+ ## Charter
9
+
10
+ | nx-cli OWNS | nx-cli NEVER contains |
11
+ |---|---|
12
+ | Sign-in / enrollment with nx-cloud | Brain logic: routing, prompts, CodeChat harness, council, cognition router, brain core (these stay in private `Nexploraai/NX`) |
13
+ | Terminal UI, streaming, rendering | Model IDs, provider URLs or provider keys |
14
+ | Actions that must run on the operator's own machine (host, computer, phone) — only under nx-cloud-issued, approval-gated commands | Global Nexplora / NX secrets |
15
+ | Its own update channel | Any upload / publish path to a package index other than the gated release job |
16
+
17
+ Every published build must pass a check that none of the private NX modules are present in the wheel.
18
+
19
+ ## What it does
20
+
21
+ - **In a Nexplora Code session's terminal** it is already signed in: the app writes a short-lived session ticket (bound to the
22
+ person, the Operation and that session, one hour, usable only for sending a message there) to a private file, and renews it
23
+ while the page holds the terminal. The person's login is never on that computer. The ticket can't approve anything.
24
+ - **On any other computer** `nx` shows a short link and a code. Approve it in the browser and it's done. The sign-in stays in
25
+ `~/.nx-cli/credentials.json` (0600). `nx logout` ends it. Work runs on the person's Nexplora cloud computer, not this one.
26
+ - **Approvals** are given in the chat. `nx` says what is waiting and where, then follows the run as it goes on.
27
+ - **Colours** are the terminal's own, with no painted background, so it matches a light or dark panel.
28
+
29
+ ```bash
30
+ python -m pip install .
31
+ nx
32
+ ```
33
+
34
+ ## Checks
35
+
36
+ ```bash
37
+ python -m pytest # includes a real local HTTP server (tests/stub_server.py)
38
+ python -m build --wheel && python scripts/check_wheel.py dist/*.whl # the charter's check; a release must pass it
39
+ python scripts/pty_proof.py "$(command -v nx)" /tmp/nx-proof # real pty, reads the screen, fails on any painted background
40
+ ```
41
+
42
+ ## PyPI
43
+
44
+ The `nxplora` project name is owned and all historical releases are yanked (2026-09-29). Existing installs
45
+ run `nx upgrade` → `pip install -U nxplora`, so the first nx-cli release published under `nxplora` becomes
46
+ the migration path for those users. Never delete the PyPI project (name-takeover risk).
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nxplora"
7
+ version = "0.16.0"
8
+ description = "NX in your terminal, on your Nexplora account."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Proprietary" }
12
+ dependencies = ["textual>=1.0,<2", "certifi"]
13
+
14
+ [project.scripts]
15
+ nx = "nxplora_cli.__main__:main"
16
+ nxplora = "nxplora_cli.__main__:main"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/nxplora_cli"]
20
+
21
+ [tool.pytest.ini_options]
22
+ testpaths = ["tests"]
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """The charter's check: a published wheel holds this client and nothing of NX. Exits non-zero, naming what it found.
3
+
4
+ Usage: python scripts/check_wheel.py dist/nxplora-*.whl
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ import sys
10
+ import zipfile
11
+
12
+ ALLOWED_MODULES = {"__init__.py", "__main__.py", "app.py", "auth.py", "events.py", "opening.py", "transport.py"}
13
+ # Never in a client: brain modules, model ids, provider addresses, secret names.
14
+ FORBIDDEN = re.compile(
15
+ r"nx_brain|nx_agent|nx_channel|chat_ui|autonomy_loop|nx_capability|from nx\b|import nx\b"
16
+ r"|claude-[a-z0-9]|gpt-[0-9]|gemini-[0-9]|qwen|deepseek|kimi|moonshot"
17
+ r"|api\.anthropic\.com|api\.openai\.com|generativelanguage\.googleapis|deepinfra|openrouter"
18
+ r"|SUPABASE_|sk-ant-|sk-proj-|BEGIN (?:RSA |EC )?PRIVATE KEY",
19
+ re.IGNORECASE,
20
+ )
21
+
22
+
23
+ def check(path: str) -> list[str]:
24
+ problems: list[str] = []
25
+ with zipfile.ZipFile(path) as wheel:
26
+ for name in wheel.namelist():
27
+ top = name.split("/", 1)[0]
28
+ if top.endswith(".dist-info"):
29
+ continue
30
+ if top != "nxplora_cli" or name.split("/", 1)[1] not in ALLOWED_MODULES:
31
+ problems.append(f"{name}: not part of this client")
32
+ continue
33
+ found = FORBIDDEN.search(wheel.read(name).decode("utf-8", "replace"))
34
+ if found:
35
+ problems.append(f"{name}: contains {found.group(0)!r}")
36
+ return problems
37
+
38
+
39
+ def main() -> int:
40
+ if len(sys.argv) != 2:
41
+ print(__doc__, file=sys.stderr)
42
+ return 2
43
+ problems = check(sys.argv[1])
44
+ for problem in problems:
45
+ print(problem, file=sys.stderr)
46
+ if not problems:
47
+ print(f"{sys.argv[1]}: clean")
48
+ return 1 if problems else 0
49
+
50
+
51
+ if __name__ == "__main__":
52
+ raise SystemExit(main())
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """Run a built `nx` in a real pty against the local stub (tests/stub_server.py), read the screen with a terminal emulator,
3
+ and write it out as text and as a picture in the terminal panel's real palettes. Never touches production.
4
+
5
+ Usage: python scripts/pty_proof.py NX_BINARY OUT_DIR [--term xterm-16color] [--no-send]
6
+ Exits 1 if the app painted any background, or the ticket reached the screen.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import fcntl
12
+ import json
13
+ import os
14
+ import pty
15
+ import select
16
+ import struct
17
+ import sys
18
+ import tempfile
19
+ import termios
20
+ import time
21
+ from pathlib import Path
22
+
23
+ import pyte
24
+ from PIL import Image, ImageDraw, ImageFont
25
+
26
+ sys.path.insert(0, str(Path(__file__).parent.parent / "tests"))
27
+ from stub_server import SESSION, Stub # noqa: E402
28
+
29
+ COLS, ROWS = 96, 26
30
+ # The panel's xterm themes, from lib/code/pty-bridge.ts LIVING_INK_XTERM, and a plain dark one.
31
+ INK = {"default_bg": "#efe9dc", "default": "#2b2620", "black": "#d9d3c6", "red": "#a63a2a", "green": "#4f6b34", "brown": "#9a7413", "blue": "#3f5d7a", "magenta": "#7a466a", "cyan": "#3f6b68", "white": "#4a4438",
32
+ "brightblack": "#6f675a", "brightred": "#c04a34", "brightgreen": "#5f7f40", "brightbrown": "#b68a1a", "brightblue": "#4d6f92", "brightmagenta": "#93567f", "brightcyan": "#4f807c", "brightwhite": "#2b2620"}
33
+ DARK = {"default_bg": "#14161a", "default": "#d7d9dc", "black": "#14161a", "red": "#e06c60", "green": "#8fbf6a", "brown": "#d9b45b", "blue": "#7aa2d6", "magenta": "#c78bd0", "cyan": "#6fc0ba", "white": "#c8cacc",
34
+ "brightblack": "#8a8f96", "brightred": "#ef8378", "brightgreen": "#a5d180", "brightbrown": "#e8c673", "brightblue": "#92b6e6", "brightmagenta": "#d7a2df", "brightcyan": "#86d3cd", "brightwhite": "#ffffff"}
35
+
36
+
37
+ def colour(name: str, palette: dict[str, str]) -> str:
38
+ if name == "default":
39
+ return palette["default"]
40
+ if name in palette:
41
+ return palette[name]
42
+ return f"#{name}" if len(name) == 6 else palette["default"]
43
+
44
+
45
+ def picture(screen: pyte.Screen, palette: dict[str, str], path: Path) -> None:
46
+ font = None
47
+ for candidate in ("/System/Library/Fonts/Menlo.ttc", "/System/Library/Fonts/Monaco.ttf"):
48
+ if os.path.exists(candidate):
49
+ font = ImageFont.truetype(candidate, 15)
50
+ break
51
+ font = font or ImageFont.load_default()
52
+ w, h = 9, 19
53
+ image = Image.new("RGB", (COLS * w, ROWS * h), palette["default_bg"])
54
+ draw = ImageDraw.Draw(image)
55
+ for y in range(ROWS):
56
+ for x in range(COLS):
57
+ cell = screen.buffer[y][x]
58
+ fg, bg = colour(cell.fg, palette), palette["default_bg"] if cell.bg == "default" else colour(cell.bg, palette)
59
+ if cell.reverse:
60
+ fg, bg = bg, fg
61
+ if bg != palette["default_bg"]:
62
+ draw.rectangle([x * w, y * h, (x + 1) * w, (y + 1) * h], fill=bg)
63
+ if cell.data.strip():
64
+ draw.text((x * w, y * h), cell.data, font=font, fill=fg)
65
+ image.save(path)
66
+
67
+
68
+ def main() -> int:
69
+ parser = argparse.ArgumentParser()
70
+ parser.add_argument("nx")
71
+ parser.add_argument("out")
72
+ parser.add_argument("--term", default="xterm-16color")
73
+ parser.add_argument("--no-send", action="store_true")
74
+ parser.add_argument("--name", default="nx")
75
+ args = parser.parse_args()
76
+ out = Path(args.out)
77
+ out.mkdir(parents=True, exist_ok=True)
78
+
79
+ ticket = "nxst_stubpayload.stubsignature"
80
+ stub = Stub(ticket)
81
+ stub.run_scripts = [[
82
+ {"event": "tool_call", "tick": 0, "tool": "run_command", "args": {}, "summary": "Ran npm test"},
83
+ {"event": "assistant_text", "text": "All 12 tests pass."},
84
+ {"event": "approval_required", "id": "a1", "tick": 1, "tool": "git_push", "args": {}, "summary": "Push to main", "danger": "push"},
85
+ {"event": "done", "status": "awaiting_approval"},
86
+ ]]
87
+ home = Path(tempfile.mkdtemp())
88
+ (home / ".nx-pty").mkdir()
89
+ (home / ".nx-cli").mkdir(mode=0o700)
90
+ session = home / ".nx-cli" / "session.json"
91
+ session.write_text(json.dumps({"v": 1, "origin": stub.origin, "session_id": SESSION, "ticket": ticket, "expires_at_ms": int(time.time() * 1000) + 3_600_000}))
92
+ os.chmod(session, 0o600)
93
+
94
+ screen = pyte.Screen(COLS, ROWS)
95
+ stream = pyte.ByteStream(screen)
96
+ raw = bytearray()
97
+ pid, fd = pty.fork()
98
+ if pid == 0:
99
+ env = {"HOME": str(home), "TERM": args.term, "PATH": os.environ["PATH"], "LANG": "C.UTF-8"}
100
+ os.chdir(home)
101
+ os.execve(args.nx, [args.nx], env)
102
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0))
103
+
104
+ def pump(seconds: float) -> None:
105
+ end = time.time() + seconds
106
+ while time.time() < end:
107
+ ready, _, _ = select.select([fd], [], [], 0.1)
108
+ if ready:
109
+ try:
110
+ data = os.read(fd, 65536)
111
+ except OSError:
112
+ return
113
+ raw.extend(data)
114
+ stream.feed(data)
115
+
116
+ pump(5)
117
+ first = out / f"{args.name}-open.txt"
118
+ first.write_text("\n".join(screen.display))
119
+ if not args.no_send:
120
+ for ch in "run the tests":
121
+ os.write(fd, ch.encode())
122
+ pump(0.03)
123
+ os.write(fd, b"\r")
124
+ pump(3)
125
+ (out / f"{args.name}-screen.txt").write_text("\n".join(screen.display))
126
+ picture(screen, INK, out / f"{args.name}-light-panel.png")
127
+ picture(screen, DARK, out / f"{args.name}-dark-panel.png")
128
+
129
+ painted: dict[str, int] = {}
130
+ fgs: dict[str, int] = {}
131
+ for y in range(ROWS):
132
+ for x in range(COLS):
133
+ cell = screen.buffer[y][x]
134
+ if cell.bg != "default":
135
+ painted[cell.bg] = painted.get(cell.bg, 0) + 1
136
+ fgs[cell.fg] = fgs.get(cell.fg, 0) + 1
137
+ os.write(fd, b"\x03")
138
+ pump(1)
139
+ report = {
140
+ "term": args.term,
141
+ "painted_background_cells": painted,
142
+ "foregrounds": fgs,
143
+ "ticket_on_screen": ticket.encode() in bytes(raw),
144
+ "requests": [{"path": r["path"], "body_fields": sorted(r["body"] or {})} for r in stub.requests],
145
+ }
146
+ (out / f"{args.name}-report.json").write_text(json.dumps(report, indent=1))
147
+ print(json.dumps(report))
148
+ print("\n".join(line.rstrip() for line in screen.display))
149
+ stub.close()
150
+ return 1 if painted or report["ticket_on_screen"] else 0
151
+
152
+
153
+ if __name__ == "__main__":
154
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ __version__ = "0.16.0"
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from . import __version__
7
+ from .auth import DEFAULT_ORIGIN, forget_device_login, load_device_login, safe_origin
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser(prog="nx", description="NX in your terminal, on your Nexplora account")
12
+ parser.add_argument("--version", action="version", version=__version__)
13
+ parser.add_argument("--connect", metavar="URL", help="the Nexplora address to use (default: nexplora.ai)")
14
+ parser.add_argument("command", nargs="?", choices=["login", "logout"], help="sign in or out on this computer")
15
+ args = parser.parse_args()
16
+ origin = safe_origin(args.connect) if args.connect else None
17
+ if args.connect and not origin:
18
+ parser.error("that address isn't https, so nx won't send your sign-in to it")
19
+ if args.command == "logout":
20
+ _logout()
21
+ return
22
+ from .app import NxploraApp
23
+
24
+ app = NxploraApp(origin=origin or DEFAULT_ORIGIN, origin_chosen=bool(origin), force_login=args.command == "login")
25
+ app.run(mouse=False)
26
+ if app.exit_note:
27
+ print(app.exit_note)
28
+
29
+
30
+ def _logout() -> None:
31
+ from .transport import Unreachable, revoke_device_session
32
+
33
+ login = load_device_login()
34
+ if login and login.device_session_id:
35
+ try:
36
+ revoke_device_session(login.origin, login.access_token, login.device_session_id)
37
+ except Unreachable:
38
+ print("Signed out here, but Nexplora couldn't be reached. You can end this sign-in in your account.", file=sys.stderr)
39
+ forget_device_login()
40
+ print("Signed out on this computer.")
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()