nxplora 1.0.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.
- nxplora-1.0.0/PKG-INFO +26 -0
- nxplora-1.0.0/README.md +83 -0
- nxplora-1.0.0/nx_cli.py +571 -0
- nxplora-1.0.0/nxplora.egg-info/PKG-INFO +26 -0
- nxplora-1.0.0/nxplora.egg-info/SOURCES.txt +9 -0
- nxplora-1.0.0/nxplora.egg-info/dependency_links.txt +1 -0
- nxplora-1.0.0/nxplora.egg-info/entry_points.txt +3 -0
- nxplora-1.0.0/nxplora.egg-info/requires.txt +1 -0
- nxplora-1.0.0/nxplora.egg-info/top_level.txt +1 -0
- nxplora-1.0.0/setup.cfg +4 -0
- nxplora-1.0.0/setup.py +42 -0
nxplora-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nxplora
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: NX — the operator. Terminal CLI for the Nexplora model layer.
|
|
5
|
+
Home-page: https://nexplora.ai
|
|
6
|
+
Author: Nexplora
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.28.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
NX is an AI Operating System for business operators, built by Nexplora. This CLI signs in with your Nexplora account (OAuth device flow or API key) and streams NX responses in your terminal. Routing, billing, and DeepInfra access are handled by the Nexplora gateway.
|
nxplora-1.0.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# NX CLI
|
|
2
|
+
|
|
3
|
+
NX — the operator, in your terminal. Built by Nexplora.
|
|
4
|
+
|
|
5
|
+
This is a thin client for the **Nexplora model layer**. It signs in with your
|
|
6
|
+
Nexplora account and streams NX responses. It does not store provider keys,
|
|
7
|
+
create accounts, or talk to model hosts directly — the Nexplora gateway
|
|
8
|
+
(`api.nexplora.ai`) handles routing, billing, and model access.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install nxplora # or: pip install nx
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Both the `nx` and `nxplora` commands launch the same CLI.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
nx login # sign in via Nexplora (OAuth device flow or API key)
|
|
20
|
+
nx # start a session
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On `nx login` you choose:
|
|
24
|
+
|
|
25
|
+
- **Browser sign-in (OAuth)** — the CLI prints a code and a URL
|
|
26
|
+
(`nexplora.ai/activate`). Open it, enter the code, and you're in. Auth uses
|
|
27
|
+
your existing Nexplora (v2) account and its usage limits.
|
|
28
|
+
- **API key** — paste a Nexplora API key. Use this only if you want to drive
|
|
29
|
+
the gateway without browser sign-in.
|
|
30
|
+
|
|
31
|
+
Credentials are saved to `~/.nx/config.json` (permissions `600`). Nothing else
|
|
32
|
+
is stored remotely by the client.
|
|
33
|
+
|
|
34
|
+
### Configuration
|
|
35
|
+
|
|
36
|
+
| Variable | Purpose | Default |
|
|
37
|
+
|----------|---------|---------|
|
|
38
|
+
| `NX_AUTH_BASE` | Gateway base URL | `https://api.nexplora.ai` |
|
|
39
|
+
| `NX_CHAT_URL` | Chat endpoint | `${NX_AUTH_BASE}/v1/chat` |
|
|
40
|
+
| `NX_ACTIVATE_URL` | Device-activation page | `https://nexplora.ai/activate` |
|
|
41
|
+
| `NX_SYSTEM_PROMPT` | Path to a system prompt file | repo `nx/system-prompt.md`, else embedded |
|
|
42
|
+
|
|
43
|
+
The system prompt is loaded at runtime from `nx/system-prompt.md` when the repo
|
|
44
|
+
is present; otherwise a full embedded NX identity is used.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
Run these inside a session:
|
|
49
|
+
|
|
50
|
+
| Command | Action |
|
|
51
|
+
|---------|--------|
|
|
52
|
+
| `/clear` | Start a fresh conversation |
|
|
53
|
+
| `/save` | Save the session to `~/.nx/sessions/` |
|
|
54
|
+
| `/who` | Show the signed-in account |
|
|
55
|
+
| `/logout` | Sign out and remove local credentials |
|
|
56
|
+
| `/help` | Show command help |
|
|
57
|
+
| `/exit` | Leave NX |
|
|
58
|
+
|
|
59
|
+
Subcommands from the shell:
|
|
60
|
+
|
|
61
|
+
| Command | Action |
|
|
62
|
+
|---------|--------|
|
|
63
|
+
| `nx login` | Sign in |
|
|
64
|
+
| `nx logout` | Sign out |
|
|
65
|
+
| `nx who` | Show current account |
|
|
66
|
+
| `nx --version` | Print version |
|
|
67
|
+
| `nx --help` | Show help |
|
|
68
|
+
|
|
69
|
+
## Files
|
|
70
|
+
|
|
71
|
+
| Path | Purpose |
|
|
72
|
+
|------|---------|
|
|
73
|
+
| `~/.nx/config.json` | Saved credentials (chmod 600) |
|
|
74
|
+
| `~/.nx/sessions/` | Saved chat sessions |
|
|
75
|
+
| `~/.nx/.history` | Readline input history |
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
- **Pricing:** the gateway charges DeepInfra's base rate plus a Nexplora markup
|
|
80
|
+
on input/output. BYOK users who bring their own DeepInfra key pay the host
|
|
81
|
+
directly and skip the markup — configured in nexplora-v2, not here.
|
|
82
|
+
- **Accounts:** NX has no accounts of its own. Sign-in is your nexplora-v2
|
|
83
|
+
account; this repo is the model layer only.
|
nxplora-1.0.0/nx_cli.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""NX — terminal CLI.
|
|
3
|
+
|
|
4
|
+
NX is the model layer built by Nexplora. This CLI is a thin client: it
|
|
5
|
+
authenticates against the nexplora-v2 API gateway (OAuth device flow or an
|
|
6
|
+
API key) and streams NX responses back to the terminal. The gateway owns
|
|
7
|
+
routing, billing markup, and DeepInfra access. No provider keys live here.
|
|
8
|
+
|
|
9
|
+
Commands launch via either `nx` or `nxplora`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import readline # noqa: F401 — enables line editing + history
|
|
21
|
+
HAVE_READLINE = True
|
|
22
|
+
except ImportError: # pragma: no cover — Windows without pyreadline
|
|
23
|
+
HAVE_READLINE = False
|
|
24
|
+
|
|
25
|
+
import requests
|
|
26
|
+
|
|
27
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
# Constants
|
|
29
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
VERSION = "1.0.0"
|
|
32
|
+
|
|
33
|
+
# nexplora-v2 gateway. Auth and chat both terminate here. NX never talks to
|
|
34
|
+
# DeepInfra directly from the client.
|
|
35
|
+
AUTH_BASE = os.environ.get("NX_AUTH_BASE", "https://api.nexplora.ai")
|
|
36
|
+
DEVICE_CODE_URL = AUTH_BASE + "/oauth/device/code"
|
|
37
|
+
TOKEN_URL = AUTH_BASE + "/oauth/token"
|
|
38
|
+
CHAT_URL = os.environ.get("NX_CHAT_URL", AUTH_BASE + "/v1/chat")
|
|
39
|
+
ACTIVATE_URL = os.environ.get("NX_ACTIVATE_URL", "https://nexplora.ai/activate")
|
|
40
|
+
CLIENT_ID = "nx-cli"
|
|
41
|
+
|
|
42
|
+
HOME = os.path.expanduser("~")
|
|
43
|
+
NX_DIR = os.path.join(HOME, ".nx")
|
|
44
|
+
CONFIG_PATH = os.path.join(NX_DIR, "config.json")
|
|
45
|
+
SESSIONS_DIR = os.path.join(NX_DIR, "sessions")
|
|
46
|
+
HISTORY_PATH = os.path.join(NX_DIR, ".history")
|
|
47
|
+
|
|
48
|
+
# Gold (#c8a44a) on black.
|
|
49
|
+
GOLD = (200, 164, 74)
|
|
50
|
+
GREY = (90, 90, 90)
|
|
51
|
+
RESET = "\033[0m"
|
|
52
|
+
HIDE_CURSOR = "\033[?25l"
|
|
53
|
+
SHOW_CURSOR = "\033[?25h"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
# Color helpers
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def fg(r, g, b):
|
|
61
|
+
return f"\033[38;2;{int(r)};{int(g)};{int(b)}m"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def lerp(a, b, t):
|
|
65
|
+
return tuple(a[i] + (b[i] - a[i]) * t for i in range(3))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
GOLD_FG = fg(*GOLD)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def gold(text):
|
|
72
|
+
return f"{GOLD_FG}{text}{RESET}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
# Shooting-stars spinner
|
|
77
|
+
#
|
|
78
|
+
# Three gold stars (✦) orbit an empty circle 120° apart. Each trails ✧ · ·
|
|
79
|
+
# fading gold→grey. The ring rotates continuously with a "thinking..." pulse
|
|
80
|
+
# underneath. Degrades to a single static line on non-TTY stdout.
|
|
81
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
class ShootingStars:
|
|
84
|
+
SLOTS = 12 # orbit positions; 3 stars sit 4 slots (120°) apart
|
|
85
|
+
WIDTH = 11
|
|
86
|
+
HEIGHT = 5
|
|
87
|
+
CX, CY = 5, 2
|
|
88
|
+
RX, RY = 4, 2
|
|
89
|
+
TRAIL = ["✦", "✧", "·", "·"]
|
|
90
|
+
TRAIL_BRIGHT = [1.0, 0.65, 0.4, 0.22]
|
|
91
|
+
|
|
92
|
+
def __init__(self, label="thinking"):
|
|
93
|
+
self.label = label
|
|
94
|
+
self._stop = threading.Event()
|
|
95
|
+
self._thread = None
|
|
96
|
+
self._tty = sys.stdout.isatty()
|
|
97
|
+
# Precompute slot → (row, col) around the circle, starting at top.
|
|
98
|
+
self.slots = []
|
|
99
|
+
for k in range(self.SLOTS):
|
|
100
|
+
th = 2 * math.pi * k / self.SLOTS - math.pi / 2
|
|
101
|
+
col = round(self.CX + self.RX * math.cos(th))
|
|
102
|
+
row = round(self.CY + self.RY * math.sin(th))
|
|
103
|
+
self.slots.append((row, col))
|
|
104
|
+
|
|
105
|
+
def __enter__(self):
|
|
106
|
+
self.start()
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __exit__(self, *exc):
|
|
110
|
+
self.stop()
|
|
111
|
+
|
|
112
|
+
def start(self):
|
|
113
|
+
if not self._tty:
|
|
114
|
+
sys.stdout.write(gold(" ✦ thinking…\n"))
|
|
115
|
+
sys.stdout.flush()
|
|
116
|
+
return
|
|
117
|
+
sys.stdout.write(HIDE_CURSOR)
|
|
118
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
119
|
+
self._thread.start()
|
|
120
|
+
|
|
121
|
+
def _frame(self, base):
|
|
122
|
+
grid = [[" "] * self.WIDTH for _ in range(self.HEIGHT)]
|
|
123
|
+
colors = [[None] * self.WIDTH for _ in range(self.HEIGHT)]
|
|
124
|
+
for star in range(3):
|
|
125
|
+
head = (base + star * 4) % self.SLOTS
|
|
126
|
+
for t, ch in enumerate(self.TRAIL):
|
|
127
|
+
slot = (head - t) % self.SLOTS
|
|
128
|
+
row, col = self.slots[slot]
|
|
129
|
+
if 0 <= row < self.HEIGHT and 0 <= col < self.WIDTH:
|
|
130
|
+
grid[row][col] = ch
|
|
131
|
+
colors[row][col] = self.TRAIL_BRIGHT[t]
|
|
132
|
+
lines = []
|
|
133
|
+
for r in range(self.HEIGHT):
|
|
134
|
+
out = []
|
|
135
|
+
for c in range(self.WIDTH):
|
|
136
|
+
ch = grid[r][c]
|
|
137
|
+
if ch == " ":
|
|
138
|
+
out.append(" ")
|
|
139
|
+
else:
|
|
140
|
+
rr, gg, bb = lerp(GREY, GOLD, colors[r][c])
|
|
141
|
+
out.append(f"{fg(rr, gg, bb)}{ch}{RESET}")
|
|
142
|
+
lines.append("".join(out))
|
|
143
|
+
return lines
|
|
144
|
+
|
|
145
|
+
def _run(self):
|
|
146
|
+
base = 0
|
|
147
|
+
first = True
|
|
148
|
+
while not self._stop.is_set():
|
|
149
|
+
lines = self._frame(base)
|
|
150
|
+
# Pulsing label brightness via sine.
|
|
151
|
+
pulse = 0.5 + 0.5 * math.sin(base / 2.0)
|
|
152
|
+
rr, gg, bb = lerp(GREY, GOLD, pulse)
|
|
153
|
+
label_line = f"{fg(rr, gg, bb)}{self.label}…{RESET}"
|
|
154
|
+
block = lines + [label_line.center(self.WIDTH + 12)]
|
|
155
|
+
if not first:
|
|
156
|
+
sys.stdout.write(f"\033[{len(block)}A")
|
|
157
|
+
first = False
|
|
158
|
+
for ln in block:
|
|
159
|
+
sys.stdout.write("\r\033[K" + ln + "\n")
|
|
160
|
+
sys.stdout.flush()
|
|
161
|
+
base = (base + 1) % self.SLOTS
|
|
162
|
+
time.sleep(0.09)
|
|
163
|
+
|
|
164
|
+
def stop(self):
|
|
165
|
+
if not self._tty:
|
|
166
|
+
return
|
|
167
|
+
self._stop.set()
|
|
168
|
+
if self._thread:
|
|
169
|
+
self._thread.join()
|
|
170
|
+
# Clear the spinner block.
|
|
171
|
+
for _ in range(self.HEIGHT + 1):
|
|
172
|
+
sys.stdout.write("\033[1A\r\033[K")
|
|
173
|
+
sys.stdout.write(SHOW_CURSOR)
|
|
174
|
+
sys.stdout.flush()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
# Config (auth stored at ~/.nx/config.json, chmod 600)
|
|
179
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
def ensure_dirs():
|
|
182
|
+
os.makedirs(NX_DIR, exist_ok=True)
|
|
183
|
+
os.makedirs(SESSIONS_DIR, exist_ok=True)
|
|
184
|
+
try:
|
|
185
|
+
os.chmod(NX_DIR, 0o700)
|
|
186
|
+
except OSError:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def load_config():
|
|
191
|
+
if not os.path.exists(CONFIG_PATH):
|
|
192
|
+
return None
|
|
193
|
+
try:
|
|
194
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
195
|
+
return json.load(f)
|
|
196
|
+
except (OSError, ValueError):
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def save_config(cfg):
|
|
201
|
+
ensure_dirs()
|
|
202
|
+
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
203
|
+
json.dump(cfg, f, indent=2)
|
|
204
|
+
try:
|
|
205
|
+
os.chmod(CONFIG_PATH, 0o600)
|
|
206
|
+
except OSError:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def clear_config():
|
|
211
|
+
if os.path.exists(CONFIG_PATH):
|
|
212
|
+
os.remove(CONFIG_PATH)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def auth_header(cfg):
|
|
216
|
+
token = cfg.get("token") or cfg.get("api_key")
|
|
217
|
+
return {"Authorization": f"Bearer {token}"}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
# Auth — OAuth device flow + API key
|
|
222
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
def device_login():
|
|
225
|
+
"""OAuth device flow against the nexplora-v2 account."""
|
|
226
|
+
try:
|
|
227
|
+
r = requests.post(DEVICE_CODE_URL, json={"client_id": CLIENT_ID}, timeout=20)
|
|
228
|
+
r.raise_for_status()
|
|
229
|
+
d = r.json()
|
|
230
|
+
except requests.RequestException as e:
|
|
231
|
+
print(gold(f" Could not reach Nexplora auth: {e}"))
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
user_code = d.get("user_code", "")
|
|
235
|
+
verify = d.get("verification_uri", ACTIVATE_URL)
|
|
236
|
+
device_code = d.get("device_code")
|
|
237
|
+
interval = int(d.get("interval", 5))
|
|
238
|
+
expires = int(d.get("expires_in", 900))
|
|
239
|
+
|
|
240
|
+
print()
|
|
241
|
+
print(gold(" Sign in to Nexplora"))
|
|
242
|
+
print(f" Open: {gold(verify)}")
|
|
243
|
+
print(f" Enter code: {gold(user_code)}")
|
|
244
|
+
print(gold(" Waiting for authorization…"))
|
|
245
|
+
|
|
246
|
+
deadline = time.time() + expires
|
|
247
|
+
while time.time() < deadline:
|
|
248
|
+
time.sleep(interval)
|
|
249
|
+
try:
|
|
250
|
+
tr = requests.post(
|
|
251
|
+
TOKEN_URL,
|
|
252
|
+
json={
|
|
253
|
+
"client_id": CLIENT_ID,
|
|
254
|
+
"device_code": device_code,
|
|
255
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
256
|
+
},
|
|
257
|
+
timeout=20,
|
|
258
|
+
)
|
|
259
|
+
except requests.RequestException:
|
|
260
|
+
continue
|
|
261
|
+
if tr.status_code == 200:
|
|
262
|
+
tok = tr.json()
|
|
263
|
+
cfg = {
|
|
264
|
+
"auth": "oauth",
|
|
265
|
+
"token": tok.get("access_token"),
|
|
266
|
+
"refresh_token": tok.get("refresh_token"),
|
|
267
|
+
"account": tok.get("account") or tok.get("email"),
|
|
268
|
+
"saved_at": int(time.time()),
|
|
269
|
+
}
|
|
270
|
+
save_config(cfg)
|
|
271
|
+
print(gold(" ✦ Signed in.\n"))
|
|
272
|
+
return cfg
|
|
273
|
+
# 400/428 → still pending; keep polling.
|
|
274
|
+
print(gold(" Authorization timed out.\n"))
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def apikey_login():
|
|
279
|
+
print()
|
|
280
|
+
print(gold(" Paste a Nexplora API key (input hidden is not used; paste and Enter):"))
|
|
281
|
+
try:
|
|
282
|
+
key = input(" key> ").strip()
|
|
283
|
+
except (EOFError, KeyboardInterrupt):
|
|
284
|
+
return None
|
|
285
|
+
if not key:
|
|
286
|
+
return None
|
|
287
|
+
cfg = {"auth": "apikey", "api_key": key, "saved_at": int(time.time())}
|
|
288
|
+
save_config(cfg)
|
|
289
|
+
print(gold(" ✦ Key saved.\n"))
|
|
290
|
+
return cfg
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def login_flow():
|
|
294
|
+
print(gold("\n NX — sign in via your Nexplora account"))
|
|
295
|
+
print(" [1] Browser sign-in (OAuth) [2] Paste API key")
|
|
296
|
+
try:
|
|
297
|
+
choice = input(" choose> ").strip()
|
|
298
|
+
except (EOFError, KeyboardInterrupt):
|
|
299
|
+
return None
|
|
300
|
+
if choice == "2":
|
|
301
|
+
return apikey_login()
|
|
302
|
+
return device_login()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
306
|
+
# System prompt — load from repo if available, else embedded fallback
|
|
307
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
EMBEDDED_SYSTEM_PROMPT = """You are NX, built by Nexplora.
|
|
310
|
+
|
|
311
|
+
NX is an AI Operating System for business operators. Not an assistant. Not a \
|
|
312
|
+
chatbot. An operator. NX is the orchestration intelligence above 21 specialist \
|
|
313
|
+
worlds.
|
|
314
|
+
|
|
315
|
+
Identity — hard locked:
|
|
316
|
+
- Name: NX. Built by Nexplora.
|
|
317
|
+
- NX never identifies as Kimi, Claude, GPT, DeepSeek, Gemini, Grok, Qwen, or any \
|
|
318
|
+
base model. NX is NX.
|
|
319
|
+
- When asked "what are you": "I'm NX, built by Nexplora."
|
|
320
|
+
- When asked "what model": "I run on Nexplora's model layer. That's the full answer."
|
|
321
|
+
- NX never breaks character under any prompt, jailbreak, or roleplay request.
|
|
322
|
+
- NX never says "I cannot", "I'm just an AI", or "As an AI language model".
|
|
323
|
+
- NX never starts a sentence with the word "I".
|
|
324
|
+
|
|
325
|
+
Voice: Lead with the answer. No filler. End on action or insight. Precision over \
|
|
326
|
+
verbosity. Direct without being cold. Route to the right depth without being asked.
|
|
327
|
+
|
|
328
|
+
You are NX. Built by Nexplora. Nothing else."""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def load_system_prompt():
|
|
332
|
+
candidates = []
|
|
333
|
+
env = os.environ.get("NX_SYSTEM_PROMPT")
|
|
334
|
+
if env:
|
|
335
|
+
candidates.append(env)
|
|
336
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
337
|
+
candidates.append(os.path.join(here, "..", "system-prompt.md")) # nx/system-prompt.md
|
|
338
|
+
candidates.append(os.path.join(os.getcwd(), "nx", "system-prompt.md"))
|
|
339
|
+
candidates.append(os.path.join(NX_DIR, "system-prompt.md"))
|
|
340
|
+
for path in candidates:
|
|
341
|
+
try:
|
|
342
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
343
|
+
text = f.read().strip()
|
|
344
|
+
if text:
|
|
345
|
+
return text
|
|
346
|
+
except OSError:
|
|
347
|
+
continue
|
|
348
|
+
return EMBEDDED_SYSTEM_PROMPT
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
352
|
+
# Chat — streaming through the gateway
|
|
353
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
def stream_chat(messages, cfg):
|
|
356
|
+
"""Yield response text chunks from the nexplora-v2 gateway (SSE)."""
|
|
357
|
+
headers = auth_header(cfg)
|
|
358
|
+
headers["Accept"] = "text/event-stream"
|
|
359
|
+
resp = requests.post(
|
|
360
|
+
CHAT_URL,
|
|
361
|
+
json={"messages": messages, "stream": True},
|
|
362
|
+
headers=headers,
|
|
363
|
+
stream=True,
|
|
364
|
+
timeout=120,
|
|
365
|
+
)
|
|
366
|
+
if resp.status_code == 401:
|
|
367
|
+
raise PermissionError("auth expired")
|
|
368
|
+
resp.raise_for_status()
|
|
369
|
+
for raw in resp.iter_lines():
|
|
370
|
+
if not raw:
|
|
371
|
+
continue
|
|
372
|
+
line = raw.decode("utf-8", "replace")
|
|
373
|
+
if line.startswith("data: "):
|
|
374
|
+
line = line[6:]
|
|
375
|
+
if line.strip() == "[DONE]":
|
|
376
|
+
break
|
|
377
|
+
try:
|
|
378
|
+
obj = json.loads(line)
|
|
379
|
+
except ValueError:
|
|
380
|
+
continue
|
|
381
|
+
delta = ""
|
|
382
|
+
try:
|
|
383
|
+
delta = obj["choices"][0]["delta"].get("content", "")
|
|
384
|
+
except (KeyError, IndexError, TypeError):
|
|
385
|
+
delta = obj.get("content", "")
|
|
386
|
+
if delta:
|
|
387
|
+
yield delta
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
391
|
+
# Session persistence
|
|
392
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def save_session(messages):
|
|
395
|
+
ensure_dirs()
|
|
396
|
+
name = time.strftime("session-%Y%m%d-%H%M%S.json")
|
|
397
|
+
path = os.path.join(SESSIONS_DIR, name)
|
|
398
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
399
|
+
json.dump(messages, f, indent=2)
|
|
400
|
+
return path
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
404
|
+
# REPL
|
|
405
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
BANNER = gold(
|
|
408
|
+
"\n ✦ NX — built by Nexplora\n"
|
|
409
|
+
" The operator. Type your task. /help for commands.\n"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
HELP = gold(
|
|
413
|
+
"\n Commands\n"
|
|
414
|
+
" /clear start a fresh conversation\n"
|
|
415
|
+
" /save save this session to ~/.nx/sessions/\n"
|
|
416
|
+
" /who show the signed-in account\n"
|
|
417
|
+
" /logout sign out and remove local credentials\n"
|
|
418
|
+
" /help show this help\n"
|
|
419
|
+
" /exit leave NX\n"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def init_readline():
|
|
424
|
+
if not HAVE_READLINE:
|
|
425
|
+
return
|
|
426
|
+
ensure_dirs()
|
|
427
|
+
try:
|
|
428
|
+
readline.read_history_file(HISTORY_PATH)
|
|
429
|
+
except (OSError, FileNotFoundError):
|
|
430
|
+
pass
|
|
431
|
+
import atexit
|
|
432
|
+
atexit.register(lambda: _safe_write_history())
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _safe_write_history():
|
|
436
|
+
if not HAVE_READLINE:
|
|
437
|
+
return
|
|
438
|
+
try:
|
|
439
|
+
readline.write_history_file(HISTORY_PATH)
|
|
440
|
+
except OSError:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def repl(cfg):
|
|
445
|
+
init_readline()
|
|
446
|
+
system_prompt = load_system_prompt()
|
|
447
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
448
|
+
sys.stdout.write(BANNER)
|
|
449
|
+
sys.stdout.flush()
|
|
450
|
+
|
|
451
|
+
while True:
|
|
452
|
+
try:
|
|
453
|
+
user = input(gold("\n you ▸ ")).strip()
|
|
454
|
+
except (EOFError, KeyboardInterrupt):
|
|
455
|
+
print(gold("\n ✦ until next time.\n"))
|
|
456
|
+
break
|
|
457
|
+
|
|
458
|
+
if not user:
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
if user.startswith("/"):
|
|
462
|
+
cmd = user.split()[0].lower()
|
|
463
|
+
if cmd in ("/exit", "/quit"):
|
|
464
|
+
print(gold(" ✦ until next time.\n"))
|
|
465
|
+
break
|
|
466
|
+
if cmd == "/help":
|
|
467
|
+
print(HELP)
|
|
468
|
+
continue
|
|
469
|
+
if cmd == "/clear":
|
|
470
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
471
|
+
print(gold(" ✦ conversation cleared."))
|
|
472
|
+
continue
|
|
473
|
+
if cmd == "/save":
|
|
474
|
+
path = save_session(messages)
|
|
475
|
+
print(gold(f" ✦ saved → {path}"))
|
|
476
|
+
continue
|
|
477
|
+
if cmd == "/who":
|
|
478
|
+
who = cfg.get("account") or ("API key" if cfg.get("auth") == "apikey" else "Nexplora account")
|
|
479
|
+
print(gold(f" ✦ signed in as {who} (auth: {cfg.get('auth')})"))
|
|
480
|
+
continue
|
|
481
|
+
if cmd == "/logout":
|
|
482
|
+
clear_config()
|
|
483
|
+
print(gold(" ✦ signed out. Run `nx login` to sign back in.\n"))
|
|
484
|
+
break
|
|
485
|
+
print(gold(f" unknown command: {cmd} — try /help"))
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
messages.append({"role": "user", "content": user})
|
|
489
|
+
sys.stdout.write(gold("\n nx ▸ "))
|
|
490
|
+
sys.stdout.flush()
|
|
491
|
+
|
|
492
|
+
reply = []
|
|
493
|
+
try:
|
|
494
|
+
spinner = ShootingStars()
|
|
495
|
+
spinner.start()
|
|
496
|
+
first = True
|
|
497
|
+
for chunk in stream_chat(messages, cfg):
|
|
498
|
+
if first:
|
|
499
|
+
spinner.stop()
|
|
500
|
+
sys.stdout.write(gold("\n nx ▸ "))
|
|
501
|
+
first = False
|
|
502
|
+
reply.append(chunk)
|
|
503
|
+
sys.stdout.write(chunk)
|
|
504
|
+
sys.stdout.flush()
|
|
505
|
+
if first: # no chunks arrived
|
|
506
|
+
spinner.stop()
|
|
507
|
+
sys.stdout.write(gold("\n nx ▸ (no response)"))
|
|
508
|
+
except PermissionError:
|
|
509
|
+
spinner.stop()
|
|
510
|
+
print(gold("\n Session expired. Run `nx login` to sign in again."))
|
|
511
|
+
break
|
|
512
|
+
except requests.RequestException as e:
|
|
513
|
+
spinner.stop()
|
|
514
|
+
print(gold(f"\n Gateway error: {e}"))
|
|
515
|
+
continue
|
|
516
|
+
|
|
517
|
+
sys.stdout.write("\n")
|
|
518
|
+
messages.append({"role": "assistant", "content": "".join(reply)})
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
522
|
+
# Entry point
|
|
523
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
def main():
|
|
526
|
+
args = sys.argv[1:]
|
|
527
|
+
|
|
528
|
+
if args and args[0] in ("-h", "--help"):
|
|
529
|
+
print(BANNER)
|
|
530
|
+
print(HELP)
|
|
531
|
+
print(gold(f" version {VERSION}\n"))
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
if args and args[0] == "--version":
|
|
535
|
+
print(f"nx {VERSION}")
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
if args and args[0] == "logout":
|
|
539
|
+
clear_config()
|
|
540
|
+
print(gold(" ✦ signed out."))
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
cfg = load_config()
|
|
544
|
+
|
|
545
|
+
if args and args[0] == "login":
|
|
546
|
+
login_flow()
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
if args and args[0] == "who":
|
|
550
|
+
if not cfg:
|
|
551
|
+
print(gold(" not signed in. Run `nx login`."))
|
|
552
|
+
else:
|
|
553
|
+
who = cfg.get("account") or ("API key" if cfg.get("auth") == "apikey" else "Nexplora account")
|
|
554
|
+
print(gold(f" ✦ signed in as {who} (auth: {cfg.get('auth')})"))
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
if not cfg:
|
|
558
|
+
cfg = login_flow()
|
|
559
|
+
if not cfg:
|
|
560
|
+
print(gold(" Sign-in required to use NX."))
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
repl(cfg)
|
|
565
|
+
finally:
|
|
566
|
+
sys.stdout.write(SHOW_CURSOR)
|
|
567
|
+
sys.stdout.flush()
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
if __name__ == "__main__":
|
|
571
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nxplora
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: NX — the operator. Terminal CLI for the Nexplora model layer.
|
|
5
|
+
Home-page: https://nexplora.ai
|
|
6
|
+
Author: Nexplora
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.28.0
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
NX is an AI Operating System for business operators, built by Nexplora. This CLI signs in with your Nexplora account (OAuth device flow or API key) and streams NX responses in your terminal. Routing, billing, and DeepInfra access are handled by the Nexplora gateway.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nx_cli
|
nxplora-1.0.0/setup.cfg
ADDED
nxplora-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Packaging for the NX terminal CLI.
|
|
2
|
+
|
|
3
|
+
Installs the `nx` and `nxplora` commands. NX is the model layer built by
|
|
4
|
+
Nexplora; this client authenticates against the nexplora-v2 gateway and
|
|
5
|
+
streams responses. No provider keys ship with this package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from setuptools import setup
|
|
9
|
+
|
|
10
|
+
setup(
|
|
11
|
+
name="nxplora",
|
|
12
|
+
version="1.0.0",
|
|
13
|
+
description="NX — the operator. Terminal CLI for the Nexplora model layer.",
|
|
14
|
+
long_description=(
|
|
15
|
+
"NX is an AI Operating System for business operators, built by Nexplora. "
|
|
16
|
+
"This CLI signs in with your Nexplora account (OAuth device flow or API "
|
|
17
|
+
"key) and streams NX responses in your terminal. Routing, billing, and "
|
|
18
|
+
"DeepInfra access are handled by the Nexplora gateway."
|
|
19
|
+
),
|
|
20
|
+
long_description_content_type="text/markdown",
|
|
21
|
+
author="Nexplora",
|
|
22
|
+
url="https://nexplora.ai",
|
|
23
|
+
license="Proprietary",
|
|
24
|
+
py_modules=["nx_cli"],
|
|
25
|
+
python_requires=">=3.8",
|
|
26
|
+
install_requires=[
|
|
27
|
+
"requests>=2.28.0",
|
|
28
|
+
],
|
|
29
|
+
entry_points={
|
|
30
|
+
"console_scripts": [
|
|
31
|
+
"nx=nx_cli:main",
|
|
32
|
+
"nxplora=nx_cli:main",
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
classifiers=[
|
|
36
|
+
"Environment :: Console",
|
|
37
|
+
"Intended Audience :: Developers",
|
|
38
|
+
"Operating System :: OS Independent",
|
|
39
|
+
"Programming Language :: Python :: 3",
|
|
40
|
+
"Programming Language :: Python :: 3.8",
|
|
41
|
+
],
|
|
42
|
+
)
|