Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Community/honcho-memory/DISPLAY.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"icon": "brain",
"tags": ["ai", "memory", "data", "cli"],
"secrets": [
{
"secret_name": "HONCHO_API_KEY",
"description": "Required. Create one at app.honcho.dev › API keys."
},
{
"secret_name": "HONCHO_WORKSPACE_ID",
"description": "Optional. Honcho workspace ID. Defaults to default."
},
{
"secret_name": "HONCHO_USER_ID",
"description": "Optional. User peer ID. Defaults to the Zo Computer username when available, then user."
},
{
"secret_name": "HONCHO_ASSISTANT_ID",
"description": "Optional. Assistant peer ID. Defaults to assistant."
},
{
"secret_name": "HONCHO_SESSION_ID",
"description": "Optional. Default session ID. Defaults to default."
},
{
"secret_name": "HONCHO_BASE_URL",
"description": "Optional. Custom Honcho API URL. Defaults to Honcho production."
}
]
}
82 changes: 82 additions & 0 deletions Community/honcho-memory/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: honcho-memory
description: Long-term memory for AI agents via the official Honcho CLI. Save conversation turns, ask natural-language memory questions, and pull context for prompts.
license: AGPL-3.0
compatibility: Requires uv, honcho-cli, and HONCHO_API_KEY. The wrapper auto-installs honcho-cli with uv if missing.
metadata:
author: plastic-labs
version: "0.4.0"
honcho-cli: "0.1.0+"
---
# Honcho Memory

Use this when an agent needs persistent memory across conversations.

This skill is intentionally thin: `scripts/memory.py` is a small Zo-friendly wrapper around the official `honcho` CLI.

## Setup

1. Get a Honcho API key from [app.honcho.dev](https://app.honcho.dev/api-keys).
2. Add `HONCHO_API_KEY` in [Settings > Advanced](/?t=settings&s=advanced).
3. Optional: add `HONCHO_WORKSPACE_ID`, `HONCHO_USER_ID`, `HONCHO_ASSISTANT_ID`, or `HONCHO_SESSION_ID`.
4. Verify:

```bash
python3 Skills/honcho-memory/scripts/memory.py test
```

## Agent workflow

Before responding, get context when memory would help:

```bash
python3 Skills/honcho-memory/scripts/memory.py context --session chat-1
```

After meaningful exchanges, save the turn:

```bash
python3 Skills/honcho-memory/scripts/memory.py save "User prefers concise tools" --session chat-1
```

Ask Honcho directly:

```bash
python3 Skills/honcho-memory/scripts/memory.py ask "What should I know about the user's preferences?"
```

## Commands

```bash
python3 Skills/honcho-memory/scripts/memory.py save "memory" --session chat-1
python3 Skills/honcho-memory/scripts/memory.py ask "question"
python3 Skills/honcho-memory/scripts/memory.py context --session chat-1 --tokens 2000
python3 Skills/honcho-memory/scripts/memory.py search "topic"
python3 Skills/honcho-memory/scripts/memory.py messages --session chat-1
python3 Skills/honcho-memory/scripts/memory.py doctor
python3 Skills/honcho-memory/scripts/memory.py test
```

For a full exchange, pipe JSON:

```bash
cat messages.json | python3 Skills/honcho-memory/scripts/memory.py save --session chat-1
```

```json
[
{"role": "user", "content": "I'm learning Rust"},
{"role": "assistant", "content": "Nice — Rust rewards careful thinking."}
]
```

## Direct CLI

Use the official CLI directly when you need lower-level control:

```bash
honcho doctor --json
honcho message create "memory" -p "$ZO_USER" -s chat-1 --json
honcho session context chat-1 --json
honcho peer chat "what does the user prefer?" -p "$ZO_USER" --json
```
245 changes: 245 additions & 0 deletions Community/honcho-memory/scripts/memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
#!/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()