honcho-cli 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/CHANGELOG.md +6 -0
  2. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/PKG-INFO +1 -1
  3. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/pyproject.toml +1 -1
  4. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/session.py +29 -17
  5. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/workspace.py +1 -1
  6. honcho_cli-0.2.1/src/honcho_cli/local/templates/init.sql +2 -0
  7. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/output.py +14 -2
  8. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_commands.py +114 -0
  9. honcho_cli-0.2.0/src/honcho_cli/local/templates/init.sql +0 -1
  10. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/.gitignore +0 -0
  11. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/README.md +0 -0
  12. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/scripts/generate_cli_docs.py +0 -0
  13. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/__init__.py +0 -0
  14. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/_help.py +0 -0
  15. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/branding.py +0 -0
  16. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/__init__.py +0 -0
  17. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/conclusion.py +0 -0
  18. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/config_cmd.py +0 -0
  19. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/message.py +0 -0
  20. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/peer.py +0 -0
  21. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/scope.py +0 -0
  22. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/setup.py +0 -0
  23. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/stack.py +0 -0
  24. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/common.py +0 -0
  25. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/config.py +0 -0
  26. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/__init__.py +0 -0
  27. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/docker.py +0 -0
  28. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/env.py +0 -0
  29. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/health.py +0 -0
  30. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/profile.py +0 -0
  31. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/setup.py +0 -0
  32. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/templates/__init__.py +0 -0
  33. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/templates/docker-compose.yml +0 -0
  34. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/main.py +0 -0
  35. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/oauth.py +0 -0
  36. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/recall.py +0 -0
  37. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/update_check.py +0 -0
  38. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/validation.py +0 -0
  39. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/__init__.py +0 -0
  40. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/conftest.py +0 -0
  41. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_common.py +0 -0
  42. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_conclusion_attribution.py +0 -0
  43. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_config.py +0 -0
  44. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_local.py +0 -0
  45. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_oauth.py +0 -0
  46. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_output.py +0 -0
  47. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_recall.py +0 -0
  48. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_setup.py +0 -0
  49. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_start.py +0 -0
  50. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_validation.py +0 -0
  51. {honcho_cli-0.2.0 → honcho_cli-0.2.1}/uv.lock +0 -0
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.1] - 2026-10-20
11
+
12
+ ### Fixed
13
+
14
+ - `honcho session summaries`, `honcho session inspect`, `honcho session context` and `honcho workspace queue-status` emit nested SDK objects (summaries, context messages, per-session queue status) as JSON objects instead of Python repr strings like `"content='...' message_id='...'"` (#1324)
15
+
10
16
  ## [0.2.0] - 2026-09-21
11
17
 
12
18
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: honcho-cli
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A terminal for Honcho — memory that reasons.
5
5
  Project-URL: Homepage, https://github.com/plastic-labs/honcho
6
6
  Project-URL: Repository, https://github.com/plastic-labs/honcho
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "honcho-cli"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "A terminal for Honcho — memory that reasons."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -27,6 +27,23 @@ app = typer.Typer(cls=HonchoTyperGroup, help="List, inspect, view, create, delet
27
27
  add_common_options(app)
28
28
 
29
29
 
30
+ def _summary_to_dict(summary) -> dict | None:
31
+ """Serialize an SDK ``Summary`` model, which ``json.dumps`` would otherwise stringify."""
32
+ return summary.model_dump(mode="json") if summary is not None else None
33
+
34
+
35
+ def _message_to_dict(m) -> dict:
36
+ """Serialize an SDK ``Message`` to the CLI's message shape."""
37
+ return {
38
+ "id": m.id,
39
+ "peer_id": m.peer_id,
40
+ "content": m.content,
41
+ "token_count": m.token_count,
42
+ "metadata": m.metadata,
43
+ "created_at": str(m.created_at),
44
+ }
45
+
46
+
30
47
  def _get_session_id(session_id: str | None) -> str:
31
48
 
32
49
  config = get_resolved_config()
@@ -132,8 +149,8 @@ def inspect(
132
149
  "peers": [{"id": p.id} for p in peers],
133
150
  "message_count": msg_page.total,
134
151
  "summaries": {
135
- "short": summaries.short_summary if hasattr(summaries, "short_summary") else None,
136
- "long": summaries.long_summary if hasattr(summaries, "long_summary") else None,
152
+ "short": _summary_to_dict(summaries.short_summary),
153
+ "long": _summary_to_dict(summaries.long_summary),
137
154
  },
138
155
  "configuration": _config_to_dict(sess_config) if sess_config else None,
139
156
  }
@@ -334,17 +351,7 @@ def view(
334
351
  if not reverse:
335
352
  msgs = list(reversed(msgs))
336
353
 
337
- items = [
338
- {
339
- "id": m.id,
340
- "peer_id": m.peer_id,
341
- "content": m.content,
342
- "token_count": m.token_count,
343
- "metadata": m.metadata,
344
- "created_at": str(m.created_at),
345
- }
346
- for m in msgs
347
- ]
354
+ items = [_message_to_dict(m) for m in msgs]
348
355
  except Exception as e:
349
356
  _handle_error(e, "session", sid)
350
357
  raise # unreachable: _handle_error always exits
@@ -394,8 +401,13 @@ def context(
394
401
 
395
402
  try:
396
403
  ctx = sess.context(tokens=tokens, summary=summary)
397
- result = ctx.__dict__ if hasattr(ctx, "__dict__") else ctx
398
- print_result(result)
404
+ print_result({
405
+ "session_id": ctx.session_id,
406
+ "summary": _summary_to_dict(ctx.summary),
407
+ "peer_representation": ctx.peer_representation,
408
+ "peer_card": ctx.peer_card,
409
+ "messages": [_message_to_dict(m) for m in ctx.messages],
410
+ })
399
411
  except Exception as e:
400
412
  _handle_error(e, "session", sid)
401
413
 
@@ -418,8 +430,8 @@ def summaries(
418
430
  s = sess.summaries()
419
431
  result = {
420
432
  "session_id": sid,
421
- "short_summary": s.short_summary if hasattr(s, "short_summary") else None,
422
- "long_summary": s.long_summary if hasattr(s, "long_summary") else None,
433
+ "short_summary": _summary_to_dict(s.short_summary),
434
+ "long_summary": _summary_to_dict(s.long_summary),
423
435
  }
424
436
  print_result(result)
425
437
  except Exception as e:
@@ -323,7 +323,7 @@ def queue_status(
323
323
 
324
324
  try:
325
325
  result = client.queue_status(observer=observer, sender=sender, session=config.session_id or None)
326
- print_result(result.__dict__ if hasattr(result, "__dict__") else result)
326
+ print_result(result.model_dump(mode="json"))
327
327
  except Exception as e:
328
328
  _handle_error(e, "queue", "status")
329
329
 
@@ -0,0 +1,2 @@
1
+ CREATE EXTENSION IF NOT EXISTS vector;
2
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
@@ -11,6 +11,7 @@ import sys
11
11
  from datetime import datetime, timezone
12
12
  from typing import Any
13
13
 
14
+ from pydantic import BaseModel
14
15
  from rich.console import Console
15
16
  from rich.table import Table
16
17
  from rich.text import Text
@@ -41,9 +42,20 @@ def use_json() -> bool:
41
42
  return _force_json or os.environ.get("HONCHO_JSON", "").lower() in ("1", "true") or not is_tty()
42
43
 
43
44
 
45
+ def _json_default(obj: Any) -> Any:
46
+ """Fallback for ``json.dumps``: dump pydantic models as objects, stringify the rest.
47
+
48
+ A bare ``default=str`` turns an SDK model into its repr
49
+ (``content='...' message_id='...'``) instead of a nested object.
50
+ """
51
+ if isinstance(obj, BaseModel):
52
+ return obj.model_dump(mode="json")
53
+ return str(obj)
54
+
55
+
44
56
  def print_json(data: Any) -> None:
45
57
  """Print a single JSON value to stdout."""
46
- print(json.dumps(data, indent=2, default=str))
58
+ print(json.dumps(data, indent=2, default=_json_default))
47
59
 
48
60
 
49
61
  def print_table(columns: list[str], rows: list[list[str]], title: str | None = None) -> None:
@@ -76,7 +88,7 @@ def print_result(data: Any, columns: list[str] | None = None, title: str | None
76
88
  table.add_column("Field", style="bold")
77
89
  table.add_column("Value")
78
90
  for k, v in data.items():
79
- val = json.dumps(v, default=str) if isinstance(v, (dict, list)) else str(v)
91
+ val = json.dumps(v, default=_json_default) if isinstance(v, (dict, list)) else str(v)
80
92
  table.add_row(k, val)
81
93
  stdout_console.print(table)
82
94
  else:
@@ -10,9 +10,12 @@ from __future__ import annotations
10
10
  import json
11
11
  import os
12
12
  from contextlib import ExitStack, contextmanager
13
+ from datetime import datetime, timezone
13
14
  from unittest.mock import MagicMock, patch
14
15
 
15
16
  import pytest
17
+ from honcho import Message, SessionContext, SessionSummaries, Summary
18
+ from honcho.api_types import QueueStatusResponse, SessionQueueStatus
16
19
  from typer.testing import CliRunner
17
20
 
18
21
  from honcho_cli.commands.session import _next_page_command
@@ -86,6 +89,16 @@ def _patch_view(session: MagicMock, *, peer_id: str = ""):
86
89
  )
87
90
 
88
91
 
92
+ def _summary(kind: str) -> Summary:
93
+ return Summary(
94
+ content=f"{kind} summary",
95
+ message_id="m1",
96
+ summary_type=f"honcho_chat_summary_{kind}",
97
+ created_at="2026-01-01T00:00:00Z",
98
+ token_count=3,
99
+ )
100
+
101
+
89
102
  @contextmanager
90
103
  def _nested(*managers):
91
104
  with ExitStack() as stack:
@@ -553,6 +566,107 @@ class TestJsonContract:
553
566
  assert payload[-1]["id"] == "m149"
554
567
  assert session.messages.call_args.kwargs["size"] == 100
555
568
 
569
+ def test_session_summaries_json_nests_summary_objects(self, cfg, runner):
570
+ """Summaries are SDK models; they were emitted as their Python repr string."""
571
+ cfg.write_text(json.dumps({"apiKey": "k", "environmentUrl": "http://localhost:8000"}))
572
+ sess = MagicMock()
573
+ sess.summaries.return_value = SessionSummaries(
574
+ id="sess1", short_summary=_summary("short"), long_summary=None
575
+ )
576
+ client = MagicMock()
577
+ client.session.return_value = sess
578
+ config = MagicMock(session_id="sess1", workspace_id="ws1")
579
+ with patch("honcho_cli.commands.session.get_client", return_value=(client, config)):
580
+ result = runner.invoke(app, ["session", "summaries", "sess1", "-w", "ws1", "--json"])
581
+ assert result.exit_code == 0, result.stderr
582
+ assert json.loads(result.stdout) == {
583
+ "session_id": "sess1",
584
+ "short_summary": _summary("short").model_dump(),
585
+ "long_summary": None,
586
+ }
587
+
588
+ def test_session_inspect_json_nests_summary_objects(self, cfg, runner):
589
+ cfg.write_text(json.dumps({"apiKey": "k", "environmentUrl": "http://localhost:8000"}))
590
+ sess = MagicMock()
591
+ sess.peers.return_value = []
592
+ sess.messages.return_value = _fake_page([], total=0)
593
+ sess.summaries.return_value = SessionSummaries(
594
+ id="sess1", short_summary=_summary("short"), long_summary=_summary("long")
595
+ )
596
+ sess.get_configuration.return_value = None
597
+ client = MagicMock()
598
+ client.session.return_value = sess
599
+ config = MagicMock(session_id="sess1", workspace_id="ws1")
600
+ with patch("honcho_cli.commands.session.get_client", return_value=(client, config)):
601
+ result = runner.invoke(app, ["session", "inspect", "sess1", "-w", "ws1", "--json"])
602
+ assert result.exit_code == 0, result.stderr
603
+ assert json.loads(result.stdout)["summaries"] == {
604
+ "short": _summary("short").model_dump(),
605
+ "long": _summary("long").model_dump(),
606
+ }
607
+
608
+ def test_session_context_json_nests_summary_and_messages(self, cfg, runner):
609
+ cfg.write_text(json.dumps({"apiKey": "k", "environmentUrl": "http://localhost:8000"}))
610
+ message = Message(
611
+ id="m1",
612
+ content="hello",
613
+ peer_id="alice",
614
+ session_id="sess1",
615
+ workspace_id="ws1",
616
+ metadata={},
617
+ created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
618
+ token_count=1,
619
+ )
620
+ sess = MagicMock()
621
+ sess.context.return_value = SessionContext(
622
+ session_id="sess1", messages=[message], summary=_summary("short")
623
+ )
624
+ client = MagicMock()
625
+ client.session.return_value = sess
626
+ config = MagicMock(session_id="sess1", workspace_id="ws1")
627
+ with patch("honcho_cli.commands.session.get_client", return_value=(client, config)):
628
+ result = runner.invoke(app, ["session", "context", "sess1", "-w", "ws1", "--json"])
629
+ assert result.exit_code == 0, result.stderr
630
+ assert json.loads(result.stdout) == {
631
+ "session_id": "sess1",
632
+ "summary": _summary("short").model_dump(),
633
+ "peer_representation": None,
634
+ "peer_card": None,
635
+ "messages": [{
636
+ "id": "m1",
637
+ "peer_id": "alice",
638
+ "content": "hello",
639
+ "token_count": 1,
640
+ "metadata": {},
641
+ "created_at": "2026-01-01 00:00:00+00:00",
642
+ }],
643
+ }
644
+
645
+ def test_workspace_queue_status_json_nests_session_status(self, cfg, runner):
646
+ cfg.write_text(json.dumps({
647
+ "apiKey": "k",
648
+ "environmentUrl": "http://localhost:8000",
649
+ "workspace_id": "ws1",
650
+ }))
651
+ counts = {
652
+ "total_work_units": 2,
653
+ "completed_work_units": 1,
654
+ "in_progress_work_units": 0,
655
+ "pending_work_units": 1,
656
+ }
657
+ client = MagicMock()
658
+ client.queue_status.return_value = QueueStatusResponse(
659
+ **counts, sessions={"sess1": SessionQueueStatus(session_id="sess1", **counts)}
660
+ )
661
+ config = MagicMock(workspace_id="ws1", session_id="")
662
+ with patch("honcho_cli.commands.workspace.get_client", return_value=(client, config)):
663
+ result = runner.invoke(app, ["workspace", "queue-status", "-w", "ws1", "--json"])
664
+ assert result.exit_code == 0, result.stderr
665
+ assert json.loads(result.stdout) == {
666
+ **counts,
667
+ "sessions": {"sess1": {"session_id": "sess1", **counts}},
668
+ }
669
+
556
670
 
557
671
  # --------------------------------------------------------------------------- #
558
672
  # 4. Exit codes on error
@@ -1 +0,0 @@
1
- CREATE EXTENSION IF NOT EXISTS vector;
File without changes
File without changes
File without changes
File without changes
File without changes