diff --git a/honcho-cli/src/honcho_cli/commands/conclusion.py b/honcho-cli/src/honcho_cli/commands/conclusion.py index b216821d..7bb24d5e 100644 --- a/honcho-cli/src/honcho_cli/commands/conclusion.py +++ b/honcho-cli/src/honcho_cli/commands/conclusion.py @@ -21,10 +21,15 @@ add_common_options(app) def list_conclusions( observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"), observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"), + 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: """List conclusions.""" + 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) client, config = get_client() if not observer: @@ -33,13 +38,13 @@ def list_conclusions( print_error("NO_PEER", "Observer peer ID required. Use --observer or set default peer.") raise typer.Exit(1) - peer = client.peer(observer) + p = client.peer(observer) try: if observed: - scope = peer.conclusions_of(observed) + scope = p.conclusions_of(observed) else: - scope = peer.conclusions + scope = p.conclusions conclusions = list(scope.list()) items = [ @@ -63,10 +68,15 @@ def search( observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"), observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"), top_k: int = typer.Option(10, help="Max results"), + 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: """Semantic search over conclusions.""" + 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) client, config = get_client() if not observer: @@ -75,13 +85,13 @@ def search( print_error("NO_PEER", "Observer peer ID required. Use --observer or set default peer.") raise typer.Exit(1) - peer = client.peer(observer) + p = client.peer(observer) try: if observed: - scope = peer.conclusions_of(observed) + scope = p.conclusions_of(observed) else: - scope = peer.conclusions + scope = p.conclusions results = scope.query(query, top_k=top_k) items = [ @@ -104,11 +114,16 @@ def create( content: str = typer.Argument(help="Conclusion content or JSON payload"), observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"), observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"), - session_id: Optional[str] = typer.Option(None, "--session", help="Session context"), + session_id: Optional[str] = typer.Option(None, "--session", "-s", help="Session context"), + 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: """Create a conclusion.""" + 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) client, config = get_client() if not observer: @@ -124,13 +139,13 @@ def create( except (json.JSONDecodeError, AttributeError): pass - peer = client.peer(observer) + p = client.peer(observer) try: if observed: - scope = peer.conclusions_of(observed) + scope = p.conclusions_of(observed) else: - scope = peer.conclusions + scope = p.conclusions result = scope.create(content, session_id=session_id) print_result({ @@ -150,10 +165,15 @@ def delete( observer: Optional[str] = typer.Option(None, "--observer", help="Observer peer ID"), observed: Optional[str] = typer.Option(None, "--observed", help="Observed peer ID"), yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"), + 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: """Delete a conclusion.""" + 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) validate_resource_id(conclusion_id, "conclusion") client, config = get_client() @@ -166,13 +186,13 @@ def delete( if not yes: typer.confirm(f"Delete conclusion '{conclusion_id}'?", abort=True) - peer = client.peer(observer) + p = client.peer(observer) try: if observed: - scope = peer.conclusions_of(observed) + scope = p.conclusions_of(observed) else: - scope = peer.conclusions + scope = p.conclusions scope.delete(conclusion_id) status(f"Conclusion '{conclusion_id}' deleted") diff --git a/honcho-cli/src/honcho_cli/commands/key.py b/honcho-cli/src/honcho_cli/commands/key.py index 828b1374..b025a1d5 100644 --- a/honcho-cli/src/honcho_cli/commands/key.py +++ b/honcho-cli/src/honcho_cli/commands/key.py @@ -47,13 +47,17 @@ def generate( admin: bool = typer.Option(False, "--admin", help="Generate admin key"), expires: str = typer.Option("90d", "--expires", help="Expiry duration (e.g., 30d, 24h)"), no_expire: bool = typer.Option(False, "--no-expire", help="No expiration (use with caution)"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), ) -> None: """Generate a scoped JWT key. Requires an admin JWT in config. Generated keys are scoped down from admin. """ + from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_resolved_config + handle_cmd_flags(json_output=json_output) + config = get_resolved_config() if not config.api_key: diff --git a/honcho-cli/src/honcho_cli/commands/message.py b/honcho-cli/src/honcho_cli/commands/message.py index 66a5e4a6..5b561374 100644 --- a/honcho-cli/src/honcho_cli/commands/message.py +++ b/honcho-cli/src/honcho_cli/commands/message.py @@ -2,12 +2,13 @@ from __future__ import annotations +import hashlib from typing import Optional import typer from honcho_cli.commands.workspace import _handle_error -from honcho_cli.output import print_result +from honcho_cli.output import print_result, status from honcho_cli.validation import validate_resource_id from honcho_cli.common import add_common_options @@ -21,34 +22,62 @@ def list_messages( session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"), last: int = typer.Option(20, "--last", help="Number of recent messages"), reverse: bool = typer.Option(False, "--reverse", help="Reverse order"), + brief: bool = typer.Option(False, "--brief", help="Show only IDs, peer, token count, and created_at (no content)"), + 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 messages in a session.""" from honcho_cli.commands.session import _get_session_id + 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() - session = client.session(sid) + sess = client.session(sid) try: - msgs = list(session.messages()) + msgs = list(sess.messages()) if not reverse: msgs = msgs[-last:] else: msgs = msgs[:last] - items = [ - { - "id": m.id, - "peer_id": m.peer_id, - "content": m.content, - "token_count": m.token_count, - "metadata": m.metadata, - "created_at": str(m.created_at), - } - for m in msgs - ] - print_result(items, columns=["id", "peer_id", "content", "created_at"], title="Messages") + # Detect duplicate content + content_hashes: dict[str, list[str]] = {} + for m in msgs: + h = hashlib.md5(m.content.encode()).hexdigest() + content_hashes.setdefault(h, []).append(m.id) + dupes = {h: ids for h, ids in content_hashes.items() if len(ids) > 1} + if dupes: + dupe_count = sum(len(ids) - 1 for ids in dupes.values()) + status(f"Warning: {dupe_count} duplicate message(s) detected (identical content, different IDs)") + + if brief: + items = [ + { + "id": m.id, + "peer_id": m.peer_id, + "token_count": m.token_count, + "created_at": str(m.created_at), + } + for m in msgs + ] + print_result(items, columns=["id", "peer_id", "token_count", "created_at"], title="Messages") + else: + items = [ + { + "id": m.id, + "peer_id": m.peer_id, + "content": m.content, + "token_count": m.token_count, + "metadata": m.metadata, + "created_at": str(m.created_at), + } + for m in msgs + ] + print_result(items, columns=["id", "peer_id", "content", "created_at"], title="Messages") except Exception as e: _handle_error(e, "message", "list") @@ -56,20 +85,24 @@ def list_messages( @app.command("get") def get_message( message_id: str = typer.Argument(help="Message ID"), - session_id: Optional[str] = typer.Option(None, "--session", help="Session ID"), + 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: """Get a single message by ID.""" from honcho_cli.commands.session import _get_session_id + from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_client + handle_cmd_flags(json_output=json_output, workspace=workspace) validate_resource_id(message_id, "message") sid = _get_session_id(session_id) client, config = get_client() try: # Use raw HTTP to get a single message - session = client.session(sid) - msgs = list(session.messages()) + sess = client.session(sid) + msgs = list(sess.messages()) msg = next((m for m in msgs if m.id == message_id), None) if msg is None: diff --git a/honcho-cli/src/honcho_cli/commands/peer.py b/honcho-cli/src/honcho_cli/commands/peer.py index 42d8facc..6da2e800 100644 --- a/honcho-cli/src/honcho_cli/commands/peer.py +++ b/honcho-cli/src/honcho_cli/commands/peer.py @@ -30,22 +30,28 @@ def _get_peer_id(peer_id: str | None) -> str: @app.command("list") -def list_peers() -> None: +def list_peers( + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: """List all peers in the workspace.""" + from honcho_cli.commands.workspace import _compact_config, _raw_list + from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_client + handle_cmd_flags(json_output=json_output, workspace=workspace) client, config = get_client() try: - peers = list(client.peers()) + raw_peers = _raw_list(client.peers()) items = [ { "id": p.id, - "metadata": getattr(p, "metadata", {}), - "configuration": _config_to_dict(p.configuration) if getattr(p, "configuration", None) else None, - "created_at": str(getattr(p, "created_at", "")), + "metadata": p.metadata, + "configuration": _compact_config(_config_to_dict(p.configuration)) if p.configuration else None, + "created_at": str(p.created_at), } - for p in peers + for p in raw_peers ] print_result(items, columns=["id", "metadata", "created_at"], title="Peers") except Exception as e: @@ -55,18 +61,23 @@ def list_peers() -> None: @app.command() def inspect( 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: """Inspect a peer: card, session count, recent conclusions.""" + 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() - peer = client.peer(pid) + p = client.peer(pid) try: - card = peer.get_card() - sessions = list(peer.sessions()) - conclusions = list(peer.conclusions.list()) + card = p.get_card() + sessions = list(p.sessions()) + conclusions = list(p.conclusions.list()) result = { "id": pid, @@ -88,16 +99,21 @@ def inspect( def card( peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"), target: Optional[str] = typer.Option(None, help="Target peer for relationship card"), + 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 raw peer card content.""" + 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() - peer = client.peer(pid) + p = client.peer(pid) try: - result = peer.get_card(target=target) + result = p.get_card(target=target) print_result({"peer_id": pid, "target": target, "card": result}) except Exception as e: _handle_error(e, "peer", pid) @@ -109,16 +125,21 @@ def chat( peer_id: Optional[str] = typer.Option(None, help="Peer ID (uses default if omitted)"), target: Optional[str] = typer.Option(None, help="Target peer for perspective"), session: Optional[str] = typer.Option(None, help="Session context"), + 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: """Query the dialectic about 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() - peer = client.peer(pid) + p = client.peer(pid) try: - response = peer.chat(query, target=target, session=session) + response = p.chat(query, target=target, session=session) print_result({"peer_id": pid, "query": query, "response": response}) except Exception as e: _handle_error(e, "peer", pid) @@ -129,16 +150,21 @@ def search( query: str = typer.Argument(help="Search query"), peer_id: Optional[str] = typer.Option(None, help="Peer 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"), + peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), ) -> None: """Search a peer's messages.""" + 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() - peer = client.peer(pid) + p = client.peer(pid) try: - results = peer.search(query, limit=limit) + results = p.search(query, limit=limit) items = [ { "id": m.id, diff --git a/honcho-cli/src/honcho_cli/commands/session.py b/honcho-cli/src/honcho_cli/commands/session.py index cacc954d..205c4c94 100644 --- a/honcho-cli/src/honcho_cli/commands/session.py +++ b/honcho-cli/src/honcho_cli/commands/session.py @@ -31,28 +31,34 @@ def _get_session_id(session_id: str | None) -> str: @app.command("list") def list_sessions( - peer_id: Optional[str] = typer.Option(None, "--peer", help="Filter by peer"), + peer_id: Optional[str] = typer.Option(None, "--peer", "-p", help="Filter by peer"), + 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 sessions in the workspace.""" + from honcho_cli.commands.workspace import _raw_list + 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) client, config = get_client() try: if peer_id: peer = client.peer(peer_id) - sessions = list(peer.sessions()) + raw_sessions = _raw_list(peer.sessions()) else: - sessions = list(client.sessions()) + raw_sessions = _raw_list(client.sessions()) items = [ { "id": s.id, - "is_active": getattr(s, "is_active", None), - "metadata": getattr(s, "metadata", {}), - "created_at": str(getattr(s, "created_at", "")), + "is_active": s.is_active, + "metadata": s.metadata, + "created_at": str(s.created_at), } - for s in sessions + for s in raw_sessions ] print_result(items, columns=["id", "is_active", "metadata", "created_at"], title="Sessions") except Exception as e: @@ -62,20 +68,28 @@ def list_sessions( @app.command() def inspect( 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: """Inspect a session: peers, message count, summaries, config.""" + 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() - session = client.session(sid) + sess = client.session(sid) try: - peers = session.peers() - messages = list(session.messages()) - summaries = session.summaries() - sess_config = session.get_configuration() + peers = sess.peers() + messages = list(sess.messages()) + summaries = sess.summaries() + sess_config = sess.get_configuration() + from honcho_cli.commands.workspace import _compact_config + + raw_config = _config_to_dict(sess_config) if sess_config else None result = { "id": sid, "peers": [{"id": p.id} for p in peers], @@ -84,7 +98,7 @@ def inspect( "short": summaries.short_summary if hasattr(summaries, "short_summary") else None, "long": summaries.long_summary if hasattr(summaries, "long_summary") else None, }, - "configuration": _config_to_dict(sess_config) if sess_config else None, + "configuration": _compact_config(raw_config) if isinstance(raw_config, dict) else raw_config, } print_result(result) except Exception as e: @@ -96,16 +110,21 @@ def messages( session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"), last: int = typer.Option(20, "--last", help="Number of recent messages"), reverse: bool = typer.Option(False, "--reverse", help="Reverse order (oldest first)"), + 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 recent 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() - session = client.session(sid) + sess = client.session(sid) try: - msgs = list(session.messages()) + msgs = list(sess.messages()) if not reverse: msgs = msgs[-last:] else: @@ -131,16 +150,21 @@ def context( session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"), tokens: Optional[int] = typer.Option(None, help="Token budget"), summary: bool = typer.Option(True, help="Include summary"), + 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 session context (what an agent would see).""" + 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() - session = client.session(sid) + sess = client.session(sid) try: - ctx = session.context(tokens=tokens, summary=summary) + ctx = sess.context(tokens=tokens, summary=summary) result = ctx.__dict__ if hasattr(ctx, "__dict__") else ctx print_result(result) except Exception as e: @@ -150,16 +174,21 @@ def context( @app.command() def summaries( 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 session summaries (short + long).""" + 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() - session = client.session(sid) + sess = client.session(sid) try: - s = session.summaries() + s = sess.summaries() result = { "session_id": sid, "short_summary": s.short_summary if hasattr(s, "short_summary") else None, diff --git a/honcho-cli/src/honcho_cli/commands/workspace.py b/honcho-cli/src/honcho_cli/commands/workspace.py index 859ad6a2..679af2c8 100644 --- a/honcho-cli/src/honcho_cli/commands/workspace.py +++ b/honcho-cli/src/honcho_cli/commands/workspace.py @@ -1,4 +1,4 @@ -"""Workspace commands: inspect, delete, search, queue-status.""" +"""Workspace commands: list, inspect, delete, search, queue-status.""" from __future__ import annotations @@ -26,13 +26,55 @@ def _get_workspace_id(workspace_id: str | None) -> str: return validate_resource_id(wid, "workspace") +def _raw_list(page) -> list: + """Collect all raw API response items across all pages of a SyncPage.""" + items = list(page._raw_items) + while page.has_next_page(): + page = page.get_next_page() + if page is None: + break + items.extend(page._raw_items) + return items + + +def _compact_config(config_dict: dict) -> dict | str: + """Return '(defaults)' if all config values are None, else the dict.""" + if all(v is None for v in config_dict.values()): + return "(defaults)" + return config_dict + + +@app.command("list") +def list_workspaces( + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), +) -> None: + """List all accessible workspaces.""" + from honcho_cli.common import handle_cmd_flags + from honcho_cli.main import get_client + + handle_cmd_flags(json_output=json_output) + client, config = get_client() + + try: + workspaces = list(client.workspaces()) + items = [{"id": w} for w in workspaces] + print_result(items, columns=["id"], title="Workspaces") + except Exception as e: + _handle_error(e, "workspace", "list") + + @app.command() def inspect( workspace_id: Optional[str] = typer.Argument(None, help="Workspace 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: """Inspect a workspace: peers, sessions, config.""" + from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_client + handle_cmd_flags(json_output=json_output, workspace=workspace) + wid = _get_workspace_id(workspace_id) client, config = get_client() @@ -44,17 +86,24 @@ def inspect( ws_config = client.get_configuration() ws_metadata = client.get_metadata() - peers = list(client.peers()) - sessions = list(client.sessions()) + # Use raw API response objects to get all fields (created_at, is_active) + raw_peers = _raw_list(client.peers()) + raw_sessions = _raw_list(client.sessions()) result = { "workspace_id": wid, "metadata": ws_metadata, - "configuration": _config_to_dict(ws_config), - "peer_count": len(peers), - "session_count": len(sessions), - "peers": [{"id": p.id, "metadata": getattr(p, "metadata", {})} for p in peers[:20]], - "sessions": [{"id": s.id, "is_active": getattr(s, "is_active", None), "metadata": getattr(s, "metadata", {})} for s in sessions[:20]], + "configuration": _compact_config(_config_to_dict(ws_config)), + "peer_count": len(raw_peers), + "session_count": len(raw_sessions), + "peers": [ + {"id": p.id, "metadata": p.metadata, "created_at": str(p.created_at)} + for p in raw_peers[:20] + ], + "sessions": [ + {"id": s.id, "is_active": s.is_active, "metadata": s.metadata, "created_at": str(s.created_at)} + for s in raw_sessions[:20] + ], } print_result(result) except Exception as e: @@ -66,10 +115,14 @@ def delete( workspace_id: str = typer.Argument(help="Workspace ID to delete"), yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"), 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.""" + from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_client + handle_cmd_flags(json_output=json_output) + validate_resource_id(workspace_id, "workspace") client, config = get_client() client = _with_workspace(client, workspace_id) @@ -100,11 +153,16 @@ def delete( def search( query: str = typer.Argument(help="Search query"), workspace_id: Optional[str] = typer.Option(None, help="Workspace ID"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), limit: int = typer.Option(10, help="Max results"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), ) -> None: """Search messages across workspace.""" + from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_client + handle_cmd_flags(json_output=json_output, workspace=workspace) + wid = _get_workspace_id(workspace_id) client, config = get_client() @@ -128,13 +186,18 @@ def search( @app.command("queue-status") def queue_status( workspace_id: Optional[str] = typer.Option(None, help="Workspace ID"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"), observer: Optional[str] = typer.Option(None, help="Filter by observer peer"), sender: Optional[str] = typer.Option(None, help="Filter by sender peer"), session: Optional[str] = typer.Option(None, help="Filter by session"), + json_output: bool = typer.Option(False, "--json", help="Force JSON output"), ) -> None: """Get queue processing status.""" + from honcho_cli.common import handle_cmd_flags from honcho_cli.main import get_client + handle_cmd_flags(json_output=json_output, workspace=workspace) + _get_workspace_id(workspace_id) client, config = get_client() diff --git a/honcho-cli/src/honcho_cli/common.py b/honcho-cli/src/honcho_cli/common.py index 4b940d8f..e0e67f87 100644 --- a/honcho-cli/src/honcho_cli/common.py +++ b/honcho-cli/src/honcho_cli/common.py @@ -1,4 +1,11 @@ -"""Shared callback for subcommand groups to accept global-style flags.""" +"""Shared callback and command-level flag helpers. + +Flags like --json, -w, -p, -s work in TWO positions: + 1. Group-level (before subcommand): honcho workspace --json list + 2. Command-level (after subcommand): honcho workspace list --json -w granola + +Both positions are idempotent — if set at group level, the command-level is a no-op. +""" from __future__ import annotations @@ -9,10 +16,28 @@ import typer from honcho_cli.output import set_json_mode, set_quiet_mode +def handle_cmd_flags( + json_output: bool = False, + workspace: str | None = None, + peer: str | None = None, + session: str | None = None, +) -> None: + """Apply command-level flags. Idempotent if already set by group callback.""" + if json_output: + set_json_mode(True) + + from honcho_cli.main import _global_overrides + + if workspace: + _global_overrides["workspace"] = workspace + if peer: + _global_overrides["peer"] = peer + if session: + _global_overrides["session"] = session + + def add_common_options(app: typer.Typer) -> None: """Add a callback to a sub-app that accepts --json, --quiet, -w, -p, -s.""" - # Allow flags like --json after subcommands (e.g., `honcho workspace inspect granola --json`) - app.info.context_settings = {"allow_interspersed_args": True} @app.callback(invoke_without_command=True) def _callback( @@ -28,7 +53,6 @@ def add_common_options(app: typer.Typer) -> None: if quiet: set_quiet_mode(True) - # Import here to avoid circular imports from honcho_cli.main import _global_overrides if workspace: