honcho-cli 0.1.2__tar.gz → 0.1.3__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 (47) hide show
  1. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/.gitignore +3 -3
  2. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/CHANGELOG.md +18 -0
  3. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/PKG-INFO +34 -2
  4. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/README.md +32 -0
  5. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/pyproject.toml +5 -1
  6. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/scripts/generate_cli_docs.py +1 -1
  7. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/__init__.py +1 -1
  8. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/_help.py +2 -1
  9. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/message.py +7 -4
  10. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/session.py +247 -6
  11. honcho_cli-0.1.3/src/honcho_cli/commands/stack.py +400 -0
  12. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/common.py +9 -0
  13. honcho_cli-0.1.3/src/honcho_cli/local/__init__.py +12 -0
  14. honcho_cli-0.1.3/src/honcho_cli/local/docker.py +436 -0
  15. honcho_cli-0.1.3/src/honcho_cli/local/env.py +183 -0
  16. honcho_cli-0.1.3/src/honcho_cli/local/health.py +53 -0
  17. honcho_cli-0.1.3/src/honcho_cli/local/profile.py +157 -0
  18. honcho_cli-0.1.3/src/honcho_cli/local/setup.py +469 -0
  19. honcho_cli-0.1.3/src/honcho_cli/local/templates/__init__.py +1 -0
  20. honcho_cli-0.1.3/src/honcho_cli/local/templates/docker-compose.yml +100 -0
  21. honcho_cli-0.1.3/src/honcho_cli/local/templates/init.sql +1 -0
  22. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/main.py +4 -0
  23. honcho_cli-0.1.3/src/honcho_cli/output.py +235 -0
  24. honcho_cli-0.1.3/tests/test_commands.py +480 -0
  25. honcho_cli-0.1.3/tests/test_local.py +127 -0
  26. honcho_cli-0.1.3/tests/test_output.py +139 -0
  27. honcho_cli-0.1.3/tests/test_setup.py +66 -0
  28. honcho_cli-0.1.3/tests/test_start.py +159 -0
  29. honcho_cli-0.1.2/src/honcho_cli/output.py +0 -104
  30. honcho_cli-0.1.2/tests/test_commands.py +0 -195
  31. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/branding.py +0 -0
  32. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/__init__.py +0 -0
  33. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/conclusion.py +0 -0
  34. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/config_cmd.py +0 -0
  35. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/peer.py +0 -0
  36. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/setup.py +0 -0
  37. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/commands/workspace.py +0 -0
  38. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/config.py +0 -0
  39. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/oauth.py +0 -0
  40. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/src/honcho_cli/validation.py +0 -0
  41. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/tests/__init__.py +0 -0
  42. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/tests/conftest.py +0 -0
  43. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/tests/test_common.py +0 -0
  44. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/tests/test_config.py +0 -0
  45. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/tests/test_oauth.py +0 -0
  46. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/tests/test_validation.py +0 -0
  47. {honcho_cli-0.1.2 → honcho_cli-0.1.3}/uv.lock +0 -0
@@ -6,8 +6,8 @@ api/docker-compose.yml
6
6
  *.db
7
7
  data
8
8
  redis-data
9
- docker-compose.yml
10
- compose.yml
9
+ /docker-compose.yml
10
+ /compose.yml
11
11
 
12
12
 
13
13
 
@@ -195,4 +195,4 @@ lancedb_data/
195
195
  grafana-data/
196
196
 
197
197
  # Claude Code addon stuff
198
- .omc
198
+ .omc
@@ -5,11 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.3] - 2026-08-25
11
+
12
+ ### Added
13
+
14
+ - `honcho start`, `honcho stop`, and `honcho status` — run a personal Honcho stack in Docker (API, deriver, Postgres, Redis). Profiles live under `~/.honcho/profiles/`. First start pins `ghcr.io/plastic-labs/honcho:latest` by digest and copies the image `config.toml`. Optional `--setup basic` / `--setup advanced` wizard writes LLM overrides to `.env` (#1029)
15
+ - `honcho session view` — session transcript table (`--last N`, `--page N --size M`, `--all`, `--reverse`, `--ids`, peer filter via `-p`). Content is shown verbatim, timestamps are normalized to UTC, and the command is read-only: unlike the other session commands it never get-or-creates the session (#1006)
16
+
17
+ ### Fixed
18
+
19
+ - `honcho message list --last N` no longer stops at the first page of 50 — it walks pages to fill the requested window (#1006)
20
+
8
21
  ## [0.1.2] - 2026-07-20
9
22
 
10
23
  ### Added
11
24
 
12
25
  - Device-code OAuth login for managed Honcho servers. `honcho init` now offers browser-based login (RFC 8628 device authorization grant) when the host advertises the device grant in its OAuth authorization-server metadata; tokens are persisted to `~/.honcho/config.json` and auto-refreshed (#891)
26
+ - `HONCHO_CONFIG_DIR` environment variable for pointing the CLI at an alternate config directory (#891)
27
+
28
+ ### Changed
29
+
30
+ - An OAuth grant now records the host it was minted against and is ignored — neither used nor refreshed — when `base_url` points elsewhere, so a staging grant is never sent to production. A live OAuth token takes precedence over a stored `apiKey`, and a dead grant degrades to the saved key with a warning instead of aborting. Device login no longer deletes the shared `apiKey`, which sibling tools read from the same config file (#891)
13
31
 
14
32
  ## [0.1.1] - 2026-06-15
15
33
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: honcho-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
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
@@ -48,6 +48,7 @@ uv tool install honcho-cli
48
48
 
49
49
  ```bash
50
50
  honcho init # confirm/set apiKey + Honcho URL in ~/.honcho/config.json
51
+ honcho start # optional: local API + deriver + Postgres + Redis (Docker)
51
52
  honcho doctor # verify your config + connectivity
52
53
  honcho # show banner + command list
53
54
  ```
@@ -56,6 +57,31 @@ honcho # show banner + command list
56
57
 
57
58
  Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `-s` flags or `HONCHO_*` env vars — not persisted as CLI defaults.
58
59
 
60
+ ### Local stack
61
+
62
+ `honcho start` runs a personal Honcho server on your machine (API, deriver, Postgres, Redis) via Docker. Inference is cloud-side: set `LLM_OPENAI_API_KEY`, `LLM_ANTHROPIC_API_KEY`, or `LLM_GEMINI_API_KEY` (env overrides `config.toml`). Stack files live under `~/.honcho/profiles/local/` and are not committed to a project.
63
+
64
+ On first start, the CLI pulls `ghcr.io/plastic-labs/honcho:latest` and **pins that digest** in `profile.json`, then copies the image's `config.toml.example` to `config.toml` in the same directory. `honcho start` never overwrites `config.toml` after that — including when you re-pin the image. Delete the file yourself if you want a fresh copy from a new image.
65
+
66
+ Pass `--setup basic` or `--setup advanced` for an interactive wizard that writes curated LLM/feature overrides into the profile `.env` (env wins over `config.toml`). TTY only; re-runnable. `basic` asks provider + chat model; `advanced` also covers embeddings, deriver/dialectic models, dreams, and snappy deriver flush. Everything else stays in `config.toml`.
67
+
68
+ `honcho start` does **not** change `environmentUrl` in `~/.honcho/config.json` (that file is shared with plugins). To talk to the local stack for one command:
69
+
70
+ ```bash
71
+ HONCHO_BASE_URL=http://127.0.0.1:8000 honcho workspace list
72
+ ```
73
+
74
+ To make local the default, run `honcho init --base-url http://127.0.0.1:8000`.
75
+
76
+ ```bash
77
+ LLM_OPENAI_API_KEY=sk-... honcho start
78
+ honcho start --setup basic
79
+ honcho start --setup advanced
80
+ honcho status
81
+ honcho stop # keep data
82
+ honcho stop --wipe # also delete volumes
83
+ ```
84
+
59
85
  ## Commands
60
86
 
61
87
  ### Onboarding
@@ -63,6 +89,9 @@ Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `-
63
89
  | Command | Description |
64
90
  |---------|-------------|
65
91
  | `honcho init` | Confirm/set `apiKey` + `environmentUrl` in `~/.honcho/config.json` |
92
+ | `honcho start` | Start a local Honcho stack (API, deriver, Postgres, Redis). Requires Docker and a cloud LLM key. `--setup basic` / `--setup advanced` runs an interactive config wizard (TTY only). Does not change `environmentUrl`. |
93
+ | `honcho stop` | Stop the local stack. `--wipe` also deletes volumes. |
94
+ | `honcho status` | Show every local stack (or `--profile` for one). |
66
95
  | `honcho doctor` | Health check: config, connectivity, workspace, peer, queue |
67
96
 
68
97
  ### Workspaces
@@ -96,6 +125,7 @@ Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `-
96
125
  | `honcho session list` | List sessions in the workspace (filter with `--peer/-p`) |
97
126
  | `honcho session create <id>` | Create or get a session (optionally `--peers` to add peers, `--metadata`) |
98
127
  | `honcho session inspect <id>` | Peers, message count, summaries, config |
128
+ | `honcho session view <id>` | Transcript table (`--last N`, `--page N --size M`, `--all`, `--reverse`, `--ids`, `-p`) |
99
129
  | `honcho session context <id>` | What an agent would see |
100
130
  | `honcho session summaries <id>` | Short + long summaries |
101
131
  | `honcho session peers <id>` / `add-peers` / `remove-peers` | Peer management |
@@ -181,6 +211,8 @@ Precedence (highest first): **flag → env var → config file → default**.
181
211
  | `HONCHO_PEER_ID` | `-p` / `--peer` | Peer scope |
182
212
  | `HONCHO_SESSION_ID` | `-s` / `--session` | Session scope |
183
213
  | `HONCHO_JSON` | `--json` | Force JSON output (`1` / `true`) |
214
+ | `HONCHO_PROFILE` | `--profile` (start/stop/status) | Local stack profile (default: `local`) |
215
+ | `LLM_OPENAI_API_KEY` | — | Provider key for `honcho start` (also `LLM_ANTHROPIC_API_KEY`, `LLM_GEMINI_API_KEY`) |
184
216
 
185
217
  ```bash
186
218
  # Per-command flags
@@ -23,6 +23,7 @@ uv tool install honcho-cli
23
23
 
24
24
  ```bash
25
25
  honcho init # confirm/set apiKey + Honcho URL in ~/.honcho/config.json
26
+ honcho start # optional: local API + deriver + Postgres + Redis (Docker)
26
27
  honcho doctor # verify your config + connectivity
27
28
  honcho # show banner + command list
28
29
  ```
@@ -31,6 +32,31 @@ honcho # show banner + command list
31
32
 
32
33
  Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `-s` flags or `HONCHO_*` env vars — not persisted as CLI defaults.
33
34
 
35
+ ### Local stack
36
+
37
+ `honcho start` runs a personal Honcho server on your machine (API, deriver, Postgres, Redis) via Docker. Inference is cloud-side: set `LLM_OPENAI_API_KEY`, `LLM_ANTHROPIC_API_KEY`, or `LLM_GEMINI_API_KEY` (env overrides `config.toml`). Stack files live under `~/.honcho/profiles/local/` and are not committed to a project.
38
+
39
+ On first start, the CLI pulls `ghcr.io/plastic-labs/honcho:latest` and **pins that digest** in `profile.json`, then copies the image's `config.toml.example` to `config.toml` in the same directory. `honcho start` never overwrites `config.toml` after that — including when you re-pin the image. Delete the file yourself if you want a fresh copy from a new image.
40
+
41
+ Pass `--setup basic` or `--setup advanced` for an interactive wizard that writes curated LLM/feature overrides into the profile `.env` (env wins over `config.toml`). TTY only; re-runnable. `basic` asks provider + chat model; `advanced` also covers embeddings, deriver/dialectic models, dreams, and snappy deriver flush. Everything else stays in `config.toml`.
42
+
43
+ `honcho start` does **not** change `environmentUrl` in `~/.honcho/config.json` (that file is shared with plugins). To talk to the local stack for one command:
44
+
45
+ ```bash
46
+ HONCHO_BASE_URL=http://127.0.0.1:8000 honcho workspace list
47
+ ```
48
+
49
+ To make local the default, run `honcho init --base-url http://127.0.0.1:8000`.
50
+
51
+ ```bash
52
+ LLM_OPENAI_API_KEY=sk-... honcho start
53
+ honcho start --setup basic
54
+ honcho start --setup advanced
55
+ honcho status
56
+ honcho stop # keep data
57
+ honcho stop --wipe # also delete volumes
58
+ ```
59
+
34
60
  ## Commands
35
61
 
36
62
  ### Onboarding
@@ -38,6 +64,9 @@ Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `-
38
64
  | Command | Description |
39
65
  |---------|-------------|
40
66
  | `honcho init` | Confirm/set `apiKey` + `environmentUrl` in `~/.honcho/config.json` |
67
+ | `honcho start` | Start a local Honcho stack (API, deriver, Postgres, Redis). Requires Docker and a cloud LLM key. `--setup basic` / `--setup advanced` runs an interactive config wizard (TTY only). Does not change `environmentUrl`. |
68
+ | `honcho stop` | Stop the local stack. `--wipe` also deletes volumes. |
69
+ | `honcho status` | Show every local stack (or `--profile` for one). |
41
70
  | `honcho doctor` | Health check: config, connectivity, workspace, peer, queue |
42
71
 
43
72
  ### Workspaces
@@ -71,6 +100,7 @@ Per-command scoping (workspace / peer / session) is handled via `-w` / `-p` / `-
71
100
  | `honcho session list` | List sessions in the workspace (filter with `--peer/-p`) |
72
101
  | `honcho session create <id>` | Create or get a session (optionally `--peers` to add peers, `--metadata`) |
73
102
  | `honcho session inspect <id>` | Peers, message count, summaries, config |
103
+ | `honcho session view <id>` | Transcript table (`--last N`, `--page N --size M`, `--all`, `--reverse`, `--ids`, `-p`) |
74
104
  | `honcho session context <id>` | What an agent would see |
75
105
  | `honcho session summaries <id>` | Short + long summaries |
76
106
  | `honcho session peers <id>` / `add-peers` / `remove-peers` | Peer management |
@@ -156,6 +186,8 @@ Precedence (highest first): **flag → env var → config file → default**.
156
186
  | `HONCHO_PEER_ID` | `-p` / `--peer` | Peer scope |
157
187
  | `HONCHO_SESSION_ID` | `-s` / `--session` | Session scope |
158
188
  | `HONCHO_JSON` | `--json` | Force JSON output (`1` / `true`) |
189
+ | `HONCHO_PROFILE` | `--profile` (start/stop/status) | Local stack profile (default: `local`) |
190
+ | `LLM_OPENAI_API_KEY` | — | Provider key for `honcho start` (also `LLM_ANTHROPIC_API_KEY`, `LLM_GEMINI_API_KEY`) |
159
191
 
160
192
  ```bash
161
193
  # Per-command flags
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "honcho-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "A terminal for Honcho — memory that reasons."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -38,6 +38,10 @@ build-backend = "hatchling.build"
38
38
  [tool.hatch.build.targets.wheel]
39
39
  packages = ["src/honcho_cli"]
40
40
 
41
+ [tool.hatch.build.targets.wheel.force-include]
42
+ "src/honcho_cli/local/templates/docker-compose.yml" = "honcho_cli/local/templates/docker-compose.yml"
43
+ "src/honcho_cli/local/templates/init.sql" = "honcho_cli/local/templates/init.sql"
44
+
41
45
  [tool.pytest.ini_options]
42
46
  testpaths = ["tests"]
43
47
 
@@ -230,7 +230,7 @@ def build() -> str:
230
230
  body: list[str] = []
231
231
  for name in sorted(root.commands):
232
232
  body.extend(_render_top(root.commands[name], ["honcho", name]))
233
- return HEADER + "\n".join(body) + "\n"
233
+ return HEADER + "\n".join(body).rstrip("\n") + "\n"
234
234
 
235
235
 
236
236
  def main() -> int:
@@ -1,3 +1,3 @@
1
1
  """Honcho CLI — a terminal for Honcho."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
@@ -67,6 +67,7 @@ def print_welcome(console: Console) -> None:
67
67
 
68
68
  start_rows = [
69
69
  ("honcho init", "configure API key and server URL"),
70
+ ("honcho start", "run a local Honcho stack (Docker)"),
70
71
  ("honcho doctor", "verify connection and workspace health"),
71
72
  ]
72
73
  cmd_rows = [
@@ -76,7 +77,7 @@ def print_welcome(console: Console) -> None:
76
77
  ("workspace", "list · create · search · delete · inspect · queue-status"),
77
78
  ("peer", "list · create · search · inspect · card · chat"),
78
79
  ("", "get-metadata · set-metadata · representation"),
79
- ("session", "list · create · search · delete · inspect · add-peers"),
80
+ ("session", "list · create · search · delete · inspect · view · add-peers"),
80
81
  ("", "context · get-metadata · set-metadata · peers"),
81
82
  ("", "remove-peers · representation · summaries"),
82
83
  ("message", "list · create · get"),
@@ -10,7 +10,7 @@ import typer
10
10
 
11
11
  from honcho.api_types import MessageCreateParams
12
12
 
13
- from honcho_cli.commands.session import _get_session_id
13
+ from honcho_cli.commands.session import _fetch_recent_messages, _get_session_id
14
14
  from honcho_cli.commands.workspace import _handle_error
15
15
  from honcho_cli.output import print_error, print_result, status
16
16
  from honcho_cli.validation import validate_resource_id
@@ -37,15 +37,18 @@ def list_messages(
37
37
 
38
38
  handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer, session=session)
39
39
  sid = _get_session_id(session_id)
40
+ if last < 1:
41
+ print_error("INVALID_FLAGS", "--last must be >= 1", {"last": last})
42
+ raise typer.Exit(1)
40
43
  client, config = get_client()
41
44
  sess = client.session(sid)
42
45
 
43
46
  try:
44
47
  filters = {"peer_id": config.peer_id} if config.peer_id else None
45
- # Fetch newest-first so [:last] always gives the most recent N messages,
46
- # then flip to oldest-at-top / newest-at-bottom for readable display.
48
+ # Fetch newest-first so we always get the most recent N messages, then
49
+ # flip to oldest-at-top / newest-at-bottom for readable display.
47
50
  # --reverse keeps the raw server order (oldest first, descending in table).
48
- msgs = sess.messages(filters=filters, reverse=True).items[:last]
51
+ msgs, _ = _fetch_recent_messages(sess, filters, last)
49
52
  if not reverse:
50
53
  msgs = list(reversed(msgs))
51
54
 
@@ -1,22 +1,29 @@
1
- """Session commands: list, inspect, context, summaries, peers, search, representation, metadata."""
1
+ """Session commands: list, inspect, view, context, summaries, peers, search, representation, metadata."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import shlex
6
7
  from typing import List, Optional
7
8
 
8
9
  import typer
9
10
 
10
- from honcho import HonchoError
11
+ from honcho import HonchoError, Session
11
12
 
12
13
  from honcho_cli.commands.workspace import _config_to_dict, _handle_error, _raw_list
13
- from honcho_cli.output import print_error, print_result, status, use_json
14
+ from honcho_cli.output import print_error, print_result, print_transcript, status, use_json
14
15
  from honcho_cli.validation import validate_resource_id
15
16
 
16
17
  from honcho_cli._help import HonchoTyperGroup
17
- from honcho_cli.common import add_common_options, get_client, get_resolved_config, handle_cmd_flags
18
-
19
- app = typer.Typer(cls=HonchoTyperGroup, help="List, inspect, create, delete, and manage conversation sessions and their peers.")
18
+ from honcho_cli.common import (
19
+ add_common_options,
20
+ get_client,
21
+ get_flag_overrides,
22
+ get_resolved_config,
23
+ handle_cmd_flags,
24
+ )
25
+
26
+ app = typer.Typer(cls=HonchoTyperGroup, help="List, inspect, view, create, delete, and manage conversation sessions and their peers.")
20
27
  add_common_options(app)
21
28
 
22
29
 
@@ -135,6 +142,240 @@ def inspect(
135
142
  _handle_error(e, "session", sid)
136
143
 
137
144
 
145
+ # Server-side ceiling on page size (fastapi-pagination's default ``Params``
146
+ # declares ``size`` as ``Query(50, ge=1, le=100)``).
147
+ MAX_PAGE_SIZE = 100
148
+ DEFAULT_PAGE_SIZE = 50
149
+
150
+
151
+ def _fetch_recent_messages(sess, filters: dict | None, last: int) -> tuple[list, int | None]:
152
+ """Fetch the ``last`` most recent messages, newest first.
153
+
154
+ Walks as many newest-first server pages as it takes to fill the window.
155
+ Returns the messages plus the session's total message count (if reported).
156
+ """
157
+ page = sess.messages(
158
+ filters=filters,
159
+ reverse=True,
160
+ size=min(max(last, 1), MAX_PAGE_SIZE),
161
+ )
162
+ total = page.total
163
+ msgs = list(page.items)
164
+ while len(msgs) < last and page.has_next_page():
165
+ page = page.get_next_page()
166
+ if page is None:
167
+ break
168
+ msgs.extend(page.items)
169
+ return msgs[:last], total
170
+
171
+
172
+ def _next_page_command(
173
+ session_id: str,
174
+ next_page: int,
175
+ size: int,
176
+ *,
177
+ reverse: bool,
178
+ show_ids: bool,
179
+ workspace: str | None,
180
+ peer: str | None,
181
+ ) -> str:
182
+ """Continuation command for the next page, carrying this invocation's scope.
183
+
184
+ Scoping flags are echoed only when passed as flags; anything resolved from
185
+ the environment or config file resolves the same way on the next run. IDs
186
+ are shell-quoted — they may contain spaces and metacharacters, and this
187
+ string is meant to be pasted into a shell.
188
+ """
189
+ parts = [
190
+ "honcho",
191
+ "session",
192
+ "view",
193
+ session_id,
194
+ "--page",
195
+ str(next_page),
196
+ "--size",
197
+ str(size),
198
+ ]
199
+ if reverse:
200
+ parts.append("--reverse")
201
+ if show_ids:
202
+ parts.append("--ids")
203
+ if workspace:
204
+ parts += ["-w", workspace]
205
+ if peer:
206
+ parts += ["-p", peer]
207
+ return shlex.join(parts)
208
+
209
+
210
+ def _fetch_all_messages(sess, filters: dict | None) -> tuple[list, int | None]:
211
+ """Fetch every message in the session, oldest first."""
212
+ page = sess.messages(filters=filters, reverse=False, size=MAX_PAGE_SIZE)
213
+ total = page.total
214
+ msgs = list(page.items)
215
+ while page.has_next_page():
216
+ page = page.get_next_page()
217
+ if page is None:
218
+ break
219
+ msgs.extend(page.items)
220
+ return msgs, total
221
+
222
+
223
+ @app.command()
224
+ def view(
225
+ session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"),
226
+ last: Optional[int] = typer.Option(
227
+ None,
228
+ "--last",
229
+ help=f"Show only the N most recent messages (default when no --page/--all: {DEFAULT_PAGE_SIZE})",
230
+ ),
231
+ page_number: Optional[int] = typer.Option(
232
+ None,
233
+ "--page",
234
+ help="1-indexed page of the full transcript. Use for page 2+.",
235
+ ),
236
+ size: Optional[int] = typer.Option(
237
+ None,
238
+ "--size",
239
+ help=f"Messages per page; requires --page (1-{MAX_PAGE_SIZE}, default: {DEFAULT_PAGE_SIZE})",
240
+ ),
241
+ all_messages: bool = typer.Option(False, "--all", help="Show the full transcript (every page)"),
242
+ reverse: bool = typer.Option(
243
+ False,
244
+ "--reverse",
245
+ help="Newest first (default is chronological: oldest at top)",
246
+ ),
247
+ show_ids: bool = typer.Option(False, "--ids", help="Include message IDs in the transcript"),
248
+ workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
249
+ peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Filter by peer ID"),
250
+ session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"),
251
+ json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
252
+ ) -> None:
253
+ """View a session transcript as a chat log.
254
+
255
+ Modes (pick one):
256
+
257
+ - default / --last N: tail of the conversation (most recent N)
258
+ - --page N [--size M]: page through the full transcript
259
+ - --all: every message
260
+
261
+ Paging follows the requested order: --page 1 starts at the oldest message,
262
+ or the newest with --reverse.
263
+
264
+ Human mode prints a row-delimited table. JSON mode emits the message list
265
+ (same shape as message list).
266
+ """
267
+ handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer, session=session)
268
+ sid = _get_session_id(session_id)
269
+
270
+ # Validate every flag before touching the network.
271
+ modes = sum([
272
+ last is not None,
273
+ page_number is not None,
274
+ all_messages,
275
+ ])
276
+ if modes > 1:
277
+ print_error(
278
+ "INVALID_FLAGS",
279
+ "--last, --page, and --all are mutually exclusive",
280
+ {"last": last, "page": page_number, "all": all_messages},
281
+ )
282
+ raise typer.Exit(1)
283
+
284
+ if page_number is not None and page_number < 1:
285
+ print_error("INVALID_FLAGS", "--page must be >= 1", {"page": page_number})
286
+ raise typer.Exit(1)
287
+ if size is not None and page_number is None:
288
+ print_error("INVALID_FLAGS", "--size only applies with --page", {"size": size})
289
+ raise typer.Exit(1)
290
+ if size is not None and not 1 <= size <= MAX_PAGE_SIZE:
291
+ print_error(
292
+ "INVALID_FLAGS",
293
+ f"--size must be between 1 and {MAX_PAGE_SIZE}",
294
+ {"size": size},
295
+ )
296
+ raise typer.Exit(1)
297
+ if last is not None and last < 1:
298
+ print_error("INVALID_FLAGS", "--last must be >= 1", {"last": last})
299
+ raise typer.Exit(1)
300
+
301
+ # Default: tail of conversation (most recent 50).
302
+ mode = "page" if page_number is not None else ("all" if all_messages else "last")
303
+ tail = last if last is not None else DEFAULT_PAGE_SIZE
304
+ page_size = size if size is not None else DEFAULT_PAGE_SIZE
305
+
306
+ client, config = get_client()
307
+ # Read-only: client.session() is a get-or-create POST, so build the Session directly.
308
+ sess = Session(sid, client)
309
+
310
+ try:
311
+ filters = {"peer_id": config.peer_id} if config.peer_id else None
312
+ page_meta: int | None = None
313
+ pages_meta: int | None = None
314
+
315
+ if mode == "page":
316
+ # Page in the order the caller asked for.
317
+ result_page = sess.messages(
318
+ filters=filters,
319
+ page=page_number,
320
+ size=page_size,
321
+ reverse=reverse,
322
+ )
323
+ msgs = list(result_page.items)
324
+ total = result_page.total
325
+ page_meta = result_page.page if result_page.page is not None else page_number
326
+ pages_meta = result_page.pages
327
+ elif mode == "all":
328
+ msgs, total = _fetch_all_messages(sess, filters)
329
+ if reverse:
330
+ msgs = list(reversed(msgs))
331
+ else:
332
+ # Tail window: fetched newest-first, flipped to chronological unless --reverse.
333
+ msgs, total = _fetch_recent_messages(sess, filters, tail)
334
+ if not reverse:
335
+ msgs = list(reversed(msgs))
336
+
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
+ ]
348
+ except Exception as e:
349
+ _handle_error(e, "session", sid)
350
+ raise # unreachable: _handle_error always exits
351
+
352
+ next_page_hint = None
353
+ if page_meta is not None and pages_meta is not None and page_meta < pages_meta:
354
+ # Effective overrides, not the command-level params: -w/-p also parse at
355
+ # group and top level.
356
+ overrides = get_flag_overrides()
357
+ next_page_hint = _next_page_command(
358
+ sid,
359
+ page_meta + 1,
360
+ page_size,
361
+ reverse=reverse,
362
+ show_ids=show_ids,
363
+ workspace=overrides["workspace"],
364
+ peer=overrides["peer"],
365
+ )
366
+
367
+ # Rendered outside the try: output failures aren't session API errors.
368
+ print_transcript(
369
+ items,
370
+ session_id=sid,
371
+ total=total,
372
+ page=page_meta,
373
+ pages=pages_meta,
374
+ show_ids=show_ids,
375
+ next_page_hint=next_page_hint,
376
+ )
377
+
378
+
138
379
  @app.command()
139
380
  def context(
140
381
  session_id: Optional[str] = typer.Argument(None, help="Session ID (uses default if omitted)"),