refactor(management): replace curses TUIs with API-backed ncp subcommands; panel/autostart/install integration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-11 07:25:08 -05:00
parent f4f77c5196
commit 53eaed4c7f
792 changed files with 2782 additions and 2240 deletions
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Backend for `ncp` subcommands (chat/memory/playbook/history).
Not a second CLI — `ncp` is the only entrypoint. This just hits the same REST
API the web UI uses, so the terminal matches the general features. httpx and
argparse only (both already present); no curses, no framework.
"""
import argparse
import json
import sys
import httpx
BASE = __import__("os").environ.get("NEXUS_API", "http://localhost:8000")
def _client():
return httpx.Client(base_url=BASE, timeout=None)
def _die_if_down(exc: Exception):
if isinstance(exc, (httpx.ConnectError, httpx.ConnectTimeout)):
sys.exit(f"Backend not reachable at {BASE}. Start it: ncp start -b")
raise exc
def iter_chunks(lines):
"""Yield ('kind', payload) from raw SSE lines. kind is 'chunk' for reply
text, else the event name ('meta'/'done'/'error'/'title'/...). Pure so the
stream parsing is unit-testable without a live server (see test_nexus_api.py)."""
event = "message"
for line in lines:
if line == "": # blank line ends an event block
event = "message"
continue
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = line[5:].strip()
if event in ("message", ""):
yield "chunk", json.loads(data) # server json-encodes each token
else:
yield event, data
def cmd_chat(args):
body = {"message": " ".join(args.message)}
if args.model:
body["model"] = args.model
try:
with _client() as c, c.stream("POST", "/chat/stream", json=body) as r:
r.raise_for_status()
for kind, payload in iter_chunks(r.iter_lines()):
if kind == "chunk":
sys.stdout.write(payload)
sys.stdout.flush()
elif kind == "escalating":
sys.stderr.write("\n[escalating to Claude…]\n")
elif kind == "error":
sys.exit("\n" + json.loads(payload).get("detail", "chat failed"))
elif kind == "done":
break
print()
except Exception as e:
_die_if_down(e)
def cmd_memory(args):
try:
with _client() as c:
if args.action == "list":
items = c.get("/memory").json()["items"]
if not items:
print("No memory facts.")
return
section = None
for m in items:
if m["section"] != section:
section = m["section"]
print(f"\n## {section}")
print(f" {m['id'][:8]} {m['text']}")
elif args.action == "add":
m = c.post("/memory", json={"text": " ".join(args.rest),
"section": args.section}).json()
print(f"added {m['id'][:8]} to {m['section']}")
elif args.action == "rm":
c.delete(f"/memory/{args.rest[0]}").raise_for_status()
print("deleted")
except Exception as e:
_die_if_down(e)
def cmd_playbook(args):
try:
with _client() as c:
if args.action == "list":
pbs = c.get("/playbooks").json()["playbooks"]
for i, p in enumerate(pbs):
mark = "* " if i == 0 else " " # first = active system prompt
tags = f" [{', '.join(p['tags'])}]" if p["tags"] else ""
print(f"{mark}{p['id'][:8]} {p['title']}{tags}")
elif args.action == "show":
p = c.get(f"/playbooks/{args.rest[0]}").json()
print(f"# {p['title']}\n\nGoal: {p['goal']}\n\n{p['instructions']}")
except Exception as e:
_die_if_down(e)
def cmd_history(args):
try:
with _client() as c:
params = {"q": args.query} if args.query else {}
convs = c.get("/conversations", params=params).json()["conversations"]
for cv in convs[:args.limit]:
title = cv.get("title") or cv.get("preview") or "(untitled)"
print(f" {cv['id'][:8]} {title}")
except Exception as e:
_die_if_down(e)
def main():
p = argparse.ArgumentParser(prog="ncp")
sub = p.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("chat"); c.add_argument("message", nargs="+")
c.add_argument("--model"); c.set_defaults(fn=cmd_chat)
m = sub.add_parser("memory"); m.add_argument("action", choices=["list", "add", "rm"])
m.add_argument("rest", nargs="*"); m.add_argument("--section", default="General")
m.set_defaults(fn=cmd_memory)
pb = sub.add_parser("playbook"); pb.add_argument("action", choices=["list", "show"])
pb.add_argument("rest", nargs="*"); pb.set_defaults(fn=cmd_playbook)
h = sub.add_parser("history"); h.add_argument("query", nargs="?")
h.add_argument("--limit", type=int, default=20); h.set_defaults(fn=cmd_history)
args = p.parse_args()
args.fn(args)
if __name__ == "__main__":
main()