246 lines
9.2 KiB
Python
Executable File
246 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Small Zo Skill wrapper around the official Honcho CLI."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
def default_user_peer() -> str:
|
|
for name in ("HONCHO_USER_ID", "HONCHO_PEER_ID", "ZO_USER", "ZO_USERNAME", "USER"):
|
|
value = os.environ.get(name)
|
|
if value and value not in {"root", "zo"}:
|
|
return value
|
|
return "user"
|
|
|
|
|
|
DEFAULT_USER = default_user_peer()
|
|
DEFAULT_ASSISTANT = os.environ.get("HONCHO_ASSISTANT_ID", "assistant")
|
|
DEFAULT_SESSION = os.environ.get("HONCHO_SESSION_ID", "default")
|
|
DEFAULT_WORKSPACE = os.environ.get("HONCHO_WORKSPACE_ID", "default")
|
|
|
|
|
|
def fail(message: str, code: int = 1):
|
|
print(f"ERROR: {message}", file=sys.stderr)
|
|
raise SystemExit(code)
|
|
|
|
|
|
def ensure_cli() -> str:
|
|
binary = shutil.which("honcho") or str(Path.home() / ".local/bin/honcho")
|
|
if Path(binary).exists():
|
|
return binary
|
|
if not shutil.which("uv"):
|
|
fail("Honcho CLI is missing and `uv` is not installed. Install uv, then run: uv tool install honcho-cli")
|
|
subprocess.run(["uv", "tool", "install", "honcho-cli"], check=True)
|
|
binary = shutil.which("honcho") or str(Path.home() / ".local/bin/honcho")
|
|
if not Path(binary).exists():
|
|
fail("Honcho CLI installed but `honcho` was not found. Try opening a new shell or add ~/.local/bin to PATH.")
|
|
return binary
|
|
|
|
|
|
def require_key():
|
|
if not os.environ.get("HONCHO_API_KEY"):
|
|
fail("HONCHO_API_KEY is missing. Add it in Zo Settings > Advanced > Secrets.", 3)
|
|
|
|
|
|
def env() -> dict[str, str]:
|
|
out = os.environ.copy()
|
|
out["PATH"] = f"{Path.home() / '.local/bin'}:{out.get('PATH', '')}"
|
|
out.setdefault("HONCHO_JSON", "1")
|
|
return out
|
|
|
|
|
|
def scope(args, include_session: bool = True) -> list[str]:
|
|
flags = ["--workspace", args.workspace or DEFAULT_WORKSPACE]
|
|
if getattr(args, "user", None):
|
|
flags += ["--peer", args.user]
|
|
if include_session and getattr(args, "session", None):
|
|
flags += ["--session", args.session]
|
|
return flags
|
|
|
|
|
|
def run_honcho(args_list: list[str], *, capture: bool = False, input_text: str | None = None, allow_doctor_partial: bool = False):
|
|
require_key()
|
|
binary = ensure_cli()
|
|
cmd = [binary, *args_list]
|
|
if "--json" not in cmd:
|
|
cmd.append("--json")
|
|
result = subprocess.run(
|
|
cmd,
|
|
input=input_text,
|
|
text=True,
|
|
capture_output=capture,
|
|
env=env(),
|
|
)
|
|
if result.returncode != 0:
|
|
if allow_doctor_partial and args_list and args_list[0] == "doctor":
|
|
text = result.stdout or result.stderr or ""
|
|
try:
|
|
data = json.loads(text)
|
|
checks = data.get("checks", [])
|
|
critical_failures = [c for c in checks if not c.get("ok") and c.get("check") != "Config file"]
|
|
if not critical_failures:
|
|
return text
|
|
except Exception:
|
|
pass
|
|
message = (result.stderr or result.stdout or "Honcho command failed").strip()
|
|
fail(message, result.returncode)
|
|
return result.stdout if capture else ""
|
|
|
|
|
|
def parse_messages(args) -> list[dict[str, str]]:
|
|
raw = args.text
|
|
if not raw and not sys.stdin.isatty():
|
|
raw = sys.stdin.read().strip()
|
|
if not raw:
|
|
fail("Provide message text, JSON, or pipe JSON into stdin.")
|
|
try:
|
|
data = json.loads(raw)
|
|
if isinstance(data, dict):
|
|
data = [data]
|
|
if isinstance(data, list):
|
|
messages = []
|
|
for item in data:
|
|
if not isinstance(item, dict) or "content" not in item:
|
|
fail("JSON messages must be objects with at least a content field.")
|
|
messages.append({"role": str(item.get("role", args.role)), "content": str(item["content"])})
|
|
return messages
|
|
except json.JSONDecodeError:
|
|
pass
|
|
return [{"role": args.role, "content": raw}]
|
|
|
|
|
|
def peer_for_role(role: str, args) -> str:
|
|
normalized = role.lower()
|
|
if normalized in {"assistant", "ai", "bot"}:
|
|
return args.assistant or DEFAULT_ASSISTANT
|
|
return args.user or DEFAULT_USER
|
|
|
|
|
|
def cmd_save(args):
|
|
session = args.session or DEFAULT_SESSION
|
|
workspace = args.workspace or DEFAULT_WORKSPACE
|
|
messages = parse_messages(args)
|
|
run_honcho(["session", "create", session, "--peers", f"{args.user or DEFAULT_USER},{args.assistant or DEFAULT_ASSISTANT}", "--workspace", workspace], capture=True)
|
|
for item in messages:
|
|
peer = peer_for_role(item["role"], args)
|
|
metadata = json.dumps({"role": item["role"], "source": "zo-skill:honcho-memory"})
|
|
run_honcho(["message", "create", item["content"], "--peer", peer, "--session", session, "--workspace", workspace, "--metadata", metadata], capture=True)
|
|
print(f"Saved {len(messages)} message(s) to Honcho session '{session}'.")
|
|
|
|
|
|
def cmd_ask(args):
|
|
output = run_honcho(["peer", "chat", args.question, *scope(args), "--reasoning", args.reasoning], capture=True)
|
|
print(output.strip())
|
|
|
|
|
|
def cmd_context(args):
|
|
command = ["session", "context", args.session or DEFAULT_SESSION, "--workspace", args.workspace or DEFAULT_WORKSPACE]
|
|
if args.tokens:
|
|
command += ["--tokens", str(args.tokens)]
|
|
if not args.summary:
|
|
command.append("--no-summary")
|
|
output = run_honcho(command, capture=True)
|
|
print(output.strip())
|
|
|
|
|
|
def cmd_messages(args):
|
|
command = ["message", "list", args.session or DEFAULT_SESSION, "--workspace", args.workspace or DEFAULT_WORKSPACE, "--last", str(args.limit)]
|
|
if args.user:
|
|
command += ["--peer", args.user]
|
|
output = run_honcho(command, capture=True)
|
|
print(output.strip())
|
|
|
|
|
|
def cmd_search(args):
|
|
output = run_honcho(["conclusion", "search", args.query, "--observer", args.user or DEFAULT_USER, "--workspace", args.workspace or DEFAULT_WORKSPACE, "--top-k", str(args.limit)], capture=True)
|
|
print(output.strip())
|
|
|
|
|
|
def cmd_doctor(args):
|
|
output = run_honcho(["doctor"], capture=True, allow_doctor_partial=True)
|
|
print(output.strip())
|
|
|
|
|
|
def cmd_test(args):
|
|
test_session = args.session
|
|
if not test_session or test_session == DEFAULT_SESSION:
|
|
test_session = f"honcho-memory-test-{int(time.time())}"
|
|
content = f"Honcho CLI skill test at {int(time.time())}"
|
|
save_args = argparse.Namespace(
|
|
text=content,
|
|
role="user",
|
|
user=args.user or DEFAULT_USER,
|
|
assistant=args.assistant or DEFAULT_ASSISTANT,
|
|
session=test_session,
|
|
workspace=args.workspace or DEFAULT_WORKSPACE,
|
|
)
|
|
cmd_save(save_args)
|
|
output = run_honcho(["message", "list", test_session, "--workspace", args.workspace or DEFAULT_WORKSPACE, "--last", "5"], capture=True)
|
|
if content not in output:
|
|
fail("Test message was saved but not found in message list.")
|
|
print(f"OK: Honcho CLI saved and listed a test message in session '{test_session}'.")
|
|
|
|
|
|
def add_common(parser):
|
|
parser.add_argument("--user", default=DEFAULT_USER, help="User peer ID")
|
|
parser.add_argument("--assistant", default=DEFAULT_ASSISTANT, help="Assistant peer ID")
|
|
parser.add_argument("--session", default=DEFAULT_SESSION, help="Session ID")
|
|
parser.add_argument("--workspace", default=DEFAULT_WORKSPACE, help="Workspace ID")
|
|
|
|
|
|
def build_parser():
|
|
parser = argparse.ArgumentParser(description="Simple Honcho memory CLI wrapper")
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
save = sub.add_parser("save", help="Save one message or JSON messages to Honcho")
|
|
save.add_argument("text", nargs="?", help="Message text or JSON messages")
|
|
save.add_argument("--role", default="user", choices=["user", "assistant", "system"], help="Role for plain text input")
|
|
add_common(save)
|
|
save.set_defaults(func=cmd_save)
|
|
|
|
ask = sub.add_parser("ask", help="Ask Honcho about remembered context")
|
|
ask.add_argument("question")
|
|
ask.add_argument("--reasoning", default="medium", choices=["minimal", "low", "medium", "high", "max"])
|
|
add_common(ask)
|
|
ask.set_defaults(func=cmd_ask)
|
|
|
|
context = sub.add_parser("context", help="Print an LLM-ready context window as JSON")
|
|
context.add_argument("--tokens", type=int, default=0)
|
|
context.add_argument("--summary", action=argparse.BooleanOptionalAction, default=True)
|
|
add_common(context)
|
|
context.set_defaults(func=cmd_context)
|
|
|
|
messages = sub.add_parser("messages", help="List recent session messages")
|
|
messages.add_argument("--limit", type=int, default=20)
|
|
add_common(messages)
|
|
messages.set_defaults(func=cmd_messages)
|
|
|
|
search = sub.add_parser("search", help="Search Honcho conclusions for the user peer")
|
|
search.add_argument("query")
|
|
search.add_argument("--limit", type=int, default=10)
|
|
add_common(search)
|
|
search.set_defaults(func=cmd_search)
|
|
|
|
doctor = sub.add_parser("doctor", help="Check Honcho CLI connectivity")
|
|
doctor.add_argument("--workspace", default=DEFAULT_WORKSPACE)
|
|
doctor.set_defaults(func=cmd_doctor)
|
|
|
|
test = sub.add_parser("test", help="Save and list a tiny memory to verify setup")
|
|
add_common(test)
|
|
test.set_defaults(func=cmd_test)
|
|
|
|
return parser
|
|
|
|
|
|
def main():
|
|
args = build_parser().parse_args()
|
|
args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|