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.
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/CHANGELOG.md +6 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/PKG-INFO +1 -1
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/pyproject.toml +1 -1
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/session.py +29 -17
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/workspace.py +1 -1
- honcho_cli-0.2.1/src/honcho_cli/local/templates/init.sql +2 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/output.py +14 -2
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_commands.py +114 -0
- honcho_cli-0.2.0/src/honcho_cli/local/templates/init.sql +0 -1
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/.gitignore +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/README.md +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/scripts/generate_cli_docs.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/__init__.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/_help.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/branding.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/__init__.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/conclusion.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/config_cmd.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/message.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/peer.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/scope.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/setup.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/commands/stack.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/common.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/config.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/__init__.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/docker.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/env.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/health.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/profile.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/setup.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/templates/__init__.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/local/templates/docker-compose.yml +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/main.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/oauth.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/recall.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/update_check.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/src/honcho_cli/validation.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/__init__.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/conftest.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_common.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_conclusion_attribution.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_config.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_local.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_oauth.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_output.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_recall.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_setup.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_start.py +0 -0
- {honcho_cli-0.2.0 → honcho_cli-0.2.1}/tests/test_validation.py +0 -0
- {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
|
|
@@ -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
|
|
136
|
-
"long": summaries.long_summary
|
|
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
|
-
|
|
398
|
-
|
|
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
|
|
422
|
-
"long_summary": s.long_summary
|
|
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.
|
|
326
|
+
print_result(result.model_dump(mode="json"))
|
|
327
327
|
except Exception as e:
|
|
328
328
|
_handle_error(e, "queue", "status")
|
|
329
329
|
|
|
@@ -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=
|
|
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=
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|