diff --git a/honcho-cli/README.md b/honcho-cli/README.md index 19e42445..95dc5fae 100644 --- a/honcho-cli/README.md +++ b/honcho-cli/README.md @@ -55,6 +55,7 @@ Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `- | Command | Description | |---------|-------------| | `honcho workspace list` | List accessible workspaces | +| `honcho workspace create ` | Create or get a workspace | | `honcho workspace inspect` | Peers, sessions, config for a workspace | | `honcho workspace search ` | Search messages across workspace | | `honcho workspace queue-status` | Deriver queue status (filter with `--observer` / `--sender`) | @@ -78,6 +79,7 @@ Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `- | Command | Description | |---------|-------------| | `honcho session list` | List sessions in the workspace (filter with `--peer/-p`) | +| `honcho session create ` | Create or get a session (optionally `--peers` to add peers, `--metadata`) | | `honcho session inspect ` | Peers, message count, summaries, config | | `honcho session context ` | What an agent would see | | `honcho session summaries ` | Short + long summaries | @@ -92,6 +94,7 @@ Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `- | Command | Description | |---------|-------------| | `honcho message list` | List messages in a session (session via `-s` / `HONCHO_SESSION_ID`) | +| `honcho message create ` | Create a message (requires `--peer/-p`, session via `-s`) | | `honcho message get ` | Get a single message (session via `-s` / `HONCHO_SESSION_ID`) | ### Conclusions (observations) diff --git a/honcho-cli/src/honcho_cli/commands/message.py b/honcho-cli/src/honcho_cli/commands/message.py index 6d604e58..f1e94b18 100644 --- a/honcho-cli/src/honcho_cli/commands/message.py +++ b/honcho-cli/src/honcho_cli/commands/message.py @@ -1,19 +1,20 @@ -"""Message commands: list, get.""" +"""Message commands: list, get, create.""" from __future__ import annotations import hashlib +import json from typing import Optional import typer -from honcho.api_types import MessageResponse +from honcho.api_types import MessageCreateParams, MessageResponse from honcho.http import routes from honcho.message import Message from honcho_cli.commands.session import _get_session_id from honcho_cli.commands.workspace import _handle_error -from honcho_cli.output import print_result, status +from honcho_cli.output import print_error, print_result, status from honcho_cli.validation import validate_resource_id from honcho_cli.common import add_common_options, get_client, handle_cmd_flags @@ -87,6 +88,48 @@ def list_messages( _handle_error(e, "message", "list") +@app.command("create") +def create_message( + content: str = typer.Argument(help="Message content"), + peer_id: str = typer.Option(..., "--peer", "-p", help="Peer ID of the message sender"), + metadata: Optional[str] = typer.Option(None, "--metadata", help="JSON metadata to associate with the message"), + session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session ID"), + 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 a message in a session.""" + handle_cmd_flags(json_output=json_output, workspace=workspace, session=session_id) + sid = _get_session_id(None) + validate_resource_id(peer_id, "peer") + client, config = get_client() + sess = client.session(sid) + + parsed_metadata = None + if metadata: + try: + parsed_metadata = json.loads(metadata) + except json.JSONDecodeError as e: + print_error("INVALID_JSON", f"--metadata must be valid JSON: {e}", {}) + raise typer.Exit(1) + + try: + msgs = sess.add_messages(MessageCreateParams( + peer_id=peer_id, + content=content, + metadata=parsed_metadata, + )) + msg = msgs[0] + print_result({ + "id": msg.id, + "peer_id": msg.peer_id, + "content": msg.content, + "token_count": msg.token_count, + "created_at": str(msg.created_at), + }) + except Exception as e: + _handle_error(e, "message", "create") + + @app.command("get") def get_message( message_id: str = typer.Argument(help="Message ID"), diff --git a/honcho-cli/src/honcho_cli/commands/session.py b/honcho-cli/src/honcho_cli/commands/session.py index 7a727f35..068d7616 100644 --- a/honcho-cli/src/honcho_cli/commands/session.py +++ b/honcho-cli/src/honcho_cli/commands/session.py @@ -60,6 +60,45 @@ def list_sessions( _handle_error(e, "session", "list") +@app.command("create") +def create_session( + session_id: str = typer.Argument(help="Session ID to create or get"), + peers: Optional[str] = typer.Option(None, "--peers", help="Comma-separated peer IDs to add to the session"), + metadata: Optional[str] = typer.Option(None, "--metadata", help="JSON metadata to associate with the session"), + 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 session.""" + handle_cmd_flags(json_output=json_output, workspace=workspace) + sid = validate_resource_id(session_id, "session") + client, config = get_client() + + parsed_metadata = None + if metadata: + try: + parsed_metadata = json.loads(metadata) + except json.JSONDecodeError as e: + print_error("INVALID_JSON", f"--metadata must be valid JSON: {e}", {}) + raise typer.Exit(1) + + peer_ids = [p.strip() for p in peers.split(",") if p.strip()] if peers else [] + for pid in peer_ids: + validate_resource_id(pid, "peer") + + try: + sess = client.session(sid, metadata=parsed_metadata) + if peer_ids: + sess.add_peers(peer_ids) + result: dict[str, object] = {"session_id": sess.id} + if parsed_metadata is not None: + result["metadata"] = parsed_metadata + if peer_ids: + result["peers"] = peer_ids + print_result(result) + except Exception as e: + _handle_error(e, "session", sid) + + @app.command() def inspect( session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"), diff --git a/honcho-cli/src/honcho_cli/commands/workspace.py b/honcho-cli/src/honcho_cli/commands/workspace.py index 187ae3e2..d4ae9921 100644 --- a/honcho-cli/src/honcho_cli/commands/workspace.py +++ b/honcho-cli/src/honcho_cli/commands/workspace.py @@ -1,7 +1,8 @@ -"""Workspace commands: list, inspect, delete, search, queue-status.""" +"""Workspace commands: list, inspect, create, delete, search, queue-status.""" from __future__ import annotations +import json from typing import Optional import typer @@ -62,6 +63,38 @@ def list_workspaces( _handle_error(e, "workspace", "list") +@app.command("create") +def create_workspace( + workspace_id: str = typer.Argument(help="Workspace ID to create or get"), + metadata: Optional[str] = typer.Option(None, "--metadata", help="JSON metadata to associate with the workspace"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """Create or get a workspace.""" + handle_cmd_flags(json_output=json_output) + wid = validate_resource_id(workspace_id, "workspace") + client, config = get_client(require_workspace=False) + ws_client = _with_workspace(client, wid) + + parsed_metadata = None + if metadata: + try: + parsed_metadata = json.loads(metadata) + except json.JSONDecodeError as e: + print_error("INVALID_JSON", f"--metadata must be valid JSON: {e}", {}) + raise typer.Exit(1) + + try: + # Trigger get-or-create via the workspace ensure mechanism + ws_client.get_configuration() + result: dict[str, object] = {"workspace_id": wid} + if parsed_metadata is not None: + ws_client.set_metadata(parsed_metadata) + result["metadata"] = parsed_metadata + print_result(result) + except Exception as e: + _handle_error(e, "workspace", wid) + + @app.command() def inspect( workspace_id: Optional[str] = typer.Argument(None, help="Workspace ID (uses default if omitted)"), @@ -129,6 +162,13 @@ def delete( client, config = get_client(require_workspace=False) ws_client = _with_workspace(client, workspace_id) + # Verify workspace exists before prompting for confirmation + try: + ws_client.get_metadata() + except Exception as e: + _handle_error(e, "workspace", workspace_id) + return + # Always fetch sessions for dry-run or cascade raw_sessions = _raw_list(ws_client.sessions()) if (dry_run or cascade) else [] @@ -225,7 +265,7 @@ def _with_workspace(client, workspace_id: str): """Return a new client pointed at a different workspace.""" return Honcho( base_url=str(client.base_url), - api_key=client._http._api_key if hasattr(client._http, "_api_key") else None, + api_key=client._http.api_key if hasattr(client._http, "api_key") else None, workspace_id=workspace_id, )