diff --git a/honcho-cli/README.md b/honcho-cli/README.md index 57372ee9..6dd375ee 100644 --- a/honcho-cli/README.md +++ b/honcho-cli/README.md @@ -27,6 +27,14 @@ honcho peer list honcho peer inspect honcho session messages --last 20 +# Peer management +honcho peer create +honcho peer create --observe-me --metadata '{"role": "user"}' +honcho peer get-metadata +honcho peer set-metadata --metadata '{"role": "user"}' +honcho peer representation +honcho peer representation --search-query "preferences" --max-conclusions 20 + # Debug honcho peer card honcho conclusion search "topic" --observer diff --git a/honcho-cli/src/honcho_cli/commands/peer.py b/honcho-cli/src/honcho_cli/commands/peer.py index 6da2e800..91fcbb23 100644 --- a/honcho-cli/src/honcho_cli/commands/peer.py +++ b/honcho-cli/src/honcho_cli/commands/peer.py @@ -1,7 +1,8 @@ -"""Peer commands: list, inspect, card, chat, search.""" +"""Peer commands: list, inspect, card, chat, search, create, metadata, representation.""" from __future__ import annotations +import json from typing import Optional import typer @@ -177,3 +178,130 @@ def search( print_result(items, columns=["id", "session_id", "content", "created_at"], title=f"Peer search: {query}") except Exception as e: _handle_error(e, "peer", pid) + + +@app.command("create") +def create_peer( + peer_id: str = typer.Argument(help="Peer ID to create or get"), + observe_me: Optional[bool] = typer.Option(None, "--observe-me/--no-observe-me", help="Whether Honcho will form a representation of this peer"), + metadata: Optional[str] = typer.Option(None, "--metadata", help="JSON metadata to associate with the peer"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Create or get a peer.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + from honcho.api_types import PeerConfig + + handle_cmd_flags(json_output=json_output, workspace=workspace) + pid = validate_resource_id(peer_id, "peer") + client, config = get_client() + + parsed_metadata = None + if metadata: + try: + parsed_metadata = json.loads(metadata) + except json.JSONDecodeError as e: + from honcho_cli.output import print_error + print_error("INVALID_JSON", f"--metadata must be valid JSON: {e}", {}) + raise typer.Exit(1) + + peer_config = PeerConfig(observe_me=observe_me) if observe_me is not None else None + + try: + p = client.peer(pid, configuration=peer_config, metadata=parsed_metadata) + result = { + "peer_id": p.id, + "metadata": parsed_metadata, + "configuration": {"observe_me": observe_me} if observe_me is not None else None, + } + print_result(result) + except Exception as e: + _handle_error(e, "peer", pid) + + +@app.command("get-metadata") +def get_metadata( + peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Get metadata for a peer.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer) + pid = _get_peer_id(peer_id) + client, config = get_client() + p = client.peer(pid) + + try: + result = p.get_metadata() + print_result({"peer_id": pid, "metadata": result}) + except Exception as e: + _handle_error(e, "peer", pid) + + +@app.command("set-metadata") +def set_metadata( + peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"), + metadata: str = typer.Option(..., "--metadata", "-m", help="JSON metadata to set (e.g. '{\"key\": \"value\"}')"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Set metadata for a peer.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer) + pid = _get_peer_id(peer_id) + client, config = get_client() + + try: + parsed = json.loads(metadata) + except json.JSONDecodeError as e: + from honcho_cli.output import print_error + print_error("INVALID_JSON", f"metadata must be valid JSON: {e}", {}) + raise typer.Exit(1) + + p = client.peer(pid) + + try: + p.set_metadata(parsed) + print_result({"peer_id": pid, "metadata": parsed}) + except Exception as e: + _handle_error(e, "peer", pid) + + +@app.command() +def representation( + peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"), + target: Optional[str] = typer.Option(None, help="Target peer to get representation about"), + session: Optional[str] = typer.Option(None, help="Scope representation to a session"), + search_query: Optional[str] = typer.Option(None, help="Semantic search query to filter conclusions"), + max_conclusions: Optional[int] = typer.Option(None, help="Maximum number of conclusions to include"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Get the formatted representation for a peer.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer) + pid = _get_peer_id(peer_id) + client, config = get_client() + p = client.peer(pid) + + try: + result = p.representation( + target=target, + session=session, + search_query=search_query, + max_conclusions=max_conclusions, + ) + print_result({"peer_id": pid, "target": target, "representation": result}) + except Exception as e: + _handle_error(e, "peer", pid) diff --git a/honcho-cli/src/honcho_cli/commands/session.py b/honcho-cli/src/honcho_cli/commands/session.py index 205c4c94..047d3b4a 100644 --- a/honcho-cli/src/honcho_cli/commands/session.py +++ b/honcho-cli/src/honcho_cli/commands/session.py @@ -1,8 +1,9 @@ -"""Session commands: list, inspect, messages, context, summaries.""" +"""Session commands: list, inspect, messages, context, summaries, peers, search, representation, metadata.""" from __future__ import annotations -from typing import Optional +import json +from typing import List, Optional import typer @@ -197,3 +198,223 @@ def summaries( print_result(result) except Exception as e: _handle_error(e, "session", sid) + + +@app.command() +def delete( + session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"), + yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Delete a session and all its data. Destructive — requires --yes or interactive confirm.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session) + sid = _get_session_id(session_id) + client, config = get_client() + + if not yes: + typer.confirm(f"Delete session '{sid}' and all its messages, conclusions, and queue items?", abort=True) + + sess = client.session(sid) + + try: + sess.delete() + status(f"Session '{sid}' deleted") + print_result({"deleted": sid}) + except Exception as e: + _handle_error(e, "session", sid) + + +@app.command("peers") +def session_peers( + session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """List peers in a session.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session) + sid = _get_session_id(session_id) + client, config = get_client() + sess = client.session(sid) + + try: + peers = sess.peers() + items = [{"id": p.id} for p in peers] + print_result(items, columns=["id"], title=f"Session peers ({sid})") + except Exception as e: + _handle_error(e, "session", sid) + + +@app.command("add-peers") +def add_peers( + peer_ids: List[str] = typer.Argument(help="Peer IDs to add to the session"), + session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session ID (uses default if omitted)"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Add peers to a session.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session_id) + sid = _get_session_id(session_id) + client, config = get_client() + sess = client.session(sid) + + try: + sess.add_peers(peer_ids) + print_result({"session_id": sid, "added_peers": peer_ids}) + except Exception as e: + _handle_error(e, "session", sid) + + +@app.command("remove-peers") +def remove_peers( + peer_ids: List[str] = typer.Argument(help="Peer IDs to remove from the session"), + session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session ID (uses default if omitted)"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Remove peers from a session.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session_id) + sid = _get_session_id(session_id) + client, config = get_client() + sess = client.session(sid) + + try: + sess.remove_peers(peer_ids) + print_result({"session_id": sid, "removed_peers": peer_ids}) + except Exception as e: + _handle_error(e, "session", sid) + + +@app.command() +def search( + query: str = typer.Argument(help="Search query"), + session_id: Optional[str] = typer.Option(None, help="Session ID (uses default if omitted)"), + limit: int = typer.Option(10, help="Max results"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Search messages in a session.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session) + sid = _get_session_id(session_id) + client, config = get_client() + sess = client.session(sid) + + try: + results = sess.search(query, limit=limit) + items = [ + { + "id": m.id, + "peer_id": m.peer_id, + "content": m.content[:200], + "created_at": str(m.created_at), + } + for m in results + ] + print_result(items, columns=["id", "peer_id", "content", "created_at"], title=f"Session search: {query}") + except Exception as e: + _handle_error(e, "session", sid) + + +@app.command() +def representation( + peer_id: str = typer.Argument(help="Peer ID to get representation for"), + session_id: Optional[str] = typer.Option(None, help="Session ID (uses default if omitted)"), + target: Optional[str] = typer.Option(None, help="Target peer (what peer_id knows about target)"), + search_query: Optional[str] = typer.Option(None, help="Semantic search query to filter conclusions"), + max_conclusions: Optional[int] = typer.Option(None, help="Maximum number of conclusions to include"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Get the representation of a peer within a session.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session) + sid = _get_session_id(session_id) + client, config = get_client() + sess = client.session(sid) + + try: + result = sess.representation( + peer_id, + target=target, + search_query=search_query, + max_conclusions=max_conclusions, + ) + print_result({"session_id": sid, "peer_id": peer_id, "target": target, "representation": result}) + except Exception as e: + _handle_error(e, "session", sid) + + +@app.command("get-metadata") +def get_metadata( + session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Get metadata for a session.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session) + sid = _get_session_id(session_id) + client, config = get_client() + sess = client.session(sid) + + try: + result = sess.get_metadata() + print_result({"session_id": sid, "metadata": result}) + except Exception as e: + _handle_error(e, "session", sid) + + +@app.command("set-metadata") +def set_metadata( + metadata: str = typer.Argument(help="JSON metadata to set (e.g. '{\"key\": \"value\"}')"), + session_id: Optional[str] = typer.Option(None, help="Session ID (uses default if omitted)"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Set metadata for a session.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session) + sid = _get_session_id(session_id) + client, config = get_client() + + try: + parsed = json.loads(metadata) + except json.JSONDecodeError as e: + from honcho_cli.output import print_error + print_error("INVALID_JSON", f"metadata must be valid JSON: {e}", {}) + raise typer.Exit(1) + + sess = client.session(sid) + + try: + sess.set_metadata(parsed) + print_result({"session_id": sid, "metadata": parsed}) + except Exception as e: + _handle_error(e, "session", sid) diff --git a/honcho-cli/src/honcho_cli/commands/workspace.py b/honcho-cli/src/honcho_cli/commands/workspace.py index 679af2c8..ef759203 100644 --- a/honcho-cli/src/honcho_cli/commands/workspace.py +++ b/honcho-cli/src/honcho_cli/commands/workspace.py @@ -113,11 +113,16 @@ def inspect( @app.command() def delete( workspace_id: str = typer.Argument(help="Workspace ID to delete"), - yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"), + yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt (for scripted/agent use)"), + cascade: bool = typer.Option(False, "--cascade", help="Delete all sessions before deleting the workspace"), dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"), json_output: bool = typer.Option(False, "--json", help="Force JSON output"), ) -> None: - """Delete a workspace. Destructive — requires --yes or interactive confirm.""" + """Delete a workspace. Use --dry-run first to see what will be deleted. + + Requires --yes to skip confirmation, or will prompt interactively. + If sessions exist, requires --cascade to delete them first. + """ from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_client @@ -125,26 +130,44 @@ def delete( validate_resource_id(workspace_id, "workspace") client, config = get_client() - client = _with_workspace(client, workspace_id) + ws_client = _with_workspace(client, workspace_id) + + # Always fetch sessions for dry-run or cascade + raw_sessions = _raw_list(ws_client.sessions()) if (dry_run or cascade) else [] if dry_run: - sessions = list(client.sessions()) - peers = list(client.peers()) print_result({ "dry_run": True, "workspace_id": workspace_id, - "sessions_to_delete": len(sessions), - "peers_to_delete": len(peers), + "sessions_to_delete": len(raw_sessions), + "session_ids": [s.id for s in raw_sessions], + "warning": "This action cannot be undone.", }) return if not yes: - typer.confirm(f"Delete workspace '{workspace_id}' and all its data?", abort=True) + if cascade and raw_sessions: + typer.confirm( + f"Delete workspace '{workspace_id}' and {len(raw_sessions)} session(s)? This cannot be undone.", + abort=True, + ) + else: + typer.confirm(f"Delete workspace '{workspace_id}'? This cannot be undone.", abort=True) try: - client.delete_workspace(workspace_id) - status(f"Workspace '{workspace_id}' deleted") - print_result({"deleted": workspace_id}) + deleted_sessions = [] + if cascade and raw_sessions: + for s in raw_sessions: + ws_client.session(s.id).delete() + deleted_sessions.append(s.id) + status(f"Deleted session '{s.id}'") + + ws_client.delete_workspace(workspace_id) + status(f"Workspace '{workspace_id}' deletion accepted (processing in background)") + result = {"deleted_workspace": workspace_id, "status": "accepted"} + if cascade: + result["deleted_sessions"] = deleted_sessions + print_result(result) except Exception as e: _handle_error(e, "workspace", workspace_id)