feat(sessions): wire html + prompt-only formats into 'sessions export'

Salvage follow-up integrating PR #30481 (@simplast) and PR #57683
(@catbearlove1-lang) into the unified export surface:

- --format html: standalone self-contained HTML transcript (single
  session or multi-session with sidebar), works with all shared filters
  and --redact; requires a file output path.
- --only user-prompts: prompt-only export (jsonl records or md sections)
  via the shared session_export renderer; the separate export-prompts
  subcommand from the original PR is subsumed by this flag.
- AUTHOR_MAP entries for both contributors; docs EN + zh-Hans.
This commit is contained in:
teknium1 2026-07-07 13:14:29 -07:00 committed by Teknium
parent b172e03c20
commit f76899facf
5 changed files with 162 additions and 89 deletions

View file

@ -13543,10 +13543,18 @@ def main():
)
sessions_export.add_argument(
"--format",
choices=["jsonl", "md", "qmd"],
choices=["jsonl", "md", "qmd", "html"],
default="jsonl",
help="Export format (default: jsonl)",
)
sessions_export.add_argument(
"--only",
choices=["user-prompts"],
help=(
"Export only a filtered view (user-prompts: one prompt record "
"per line for jsonl, headed sections for md)"
),
)
sessions_export.add_argument(
"--session-id", help="Session ID or unique prefix to export"
)
@ -13788,6 +13796,99 @@ def main():
return redact_session_data(data)
def _collect_sessions():
"""Resolve --session-id / filters / bare export into a list
of redacted session dicts, or None after printing an error."""
if args.session_id:
resolved = db.resolve_session_id(args.session_id)
data = _redact(db.export_session(resolved)) if resolved else None
if not data:
print(f"Session '{args.session_id}' not found.")
return None
return [data]
if filters:
candidates = db.list_prune_candidates(**filters)
if args.dry_run:
print(
f"Would export {len(candidates)} session(s) "
f"({describe_filters(filters)})."
)
for row in candidates[:100]:
print(f" {row.get('id')} {row.get('source', '')}")
if len(candidates) > 100:
print(f" ... {len(candidates) - 100} more")
return None
return [
s
for s in (
_redact(db.export_session(row["id"])) for row in candidates
)
if s
]
if args.dry_run:
print("--dry-run requires at least one filter.")
return None
return [_redact(s) for s in db.export_all(source=None)]
# Prompt-only export (--only user-prompts): one prompt record per
# line (jsonl) or headed sections (md). Delegates rendering to
# hermes_cli.session_export.
if getattr(args, "only", None):
if args.format not in ("jsonl", "md"):
print("--only user-prompts supports --format jsonl or md.")
return
from hermes_cli.session_export import (
export_record_count,
render_sessions_export,
)
sessions = _collect_sessions()
if sessions is None:
db.close()
return
rendered = render_sessions_export(
sessions,
fmt="markdown" if args.format == "md" else "jsonl",
only=args.only,
)
if not args.output or args.output == "-":
sys.stdout.write(rendered)
db.close()
return
with open(args.output, "w", encoding="utf-8") as f:
f.write(rendered)
count, noun = export_record_count(sessions, only=args.only)
suffix = "" if count == 1 else "s"
print(f"Exported {count} {noun}{suffix} to {args.output}")
db.close()
return
# Standalone HTML export: one self-contained file (single session
# or multi-session with sidebar navigation).
if args.format == "html":
if not args.output or args.output == "-":
print("HTML export requires an output file path.")
return
from hermes_cli.session_export_html import (
generate_html_export,
generate_multi_session_html_export,
)
sessions = _collect_sessions()
if sessions is None:
db.close()
return
if len(sessions) == 1:
content = generate_html_export(sessions[0])
else:
content = generate_multi_session_html_export(sessions)
with open(args.output, "w", encoding="utf-8") as f:
f.write(content)
suffix = "" if len(sessions) == 1 else "s"
print(f"Exported {len(sessions)} session{suffix} to {args.output} (HTML)")
db.close()
return
if args.format == "jsonl":
if not args.output:
print("JSONL export requires an output path (use - for stdout).")

View file

@ -839,6 +839,9 @@ AUTHOR_MAP = {
"27719690+Mirac1eSky@users.noreply.github.com": "Mirac1eSky",
"web3blind@users.noreply.github.com": "web3blind",
"264741654+web3blind@users.noreply.github.com": "web3blind",
# Sessions export format cluster (July 2026): HTML + prompt-only salvages
"840004959@qq.com": "simplast",
"catbearlove1@gmail.com": "catbearlove1-lang",
"julia@alexland.us": "alexg0bot",
"christian@scheid.tech": "scheidti",
# Moonshot schema anyOf+enum salvage (May 2026)

View file

@ -188,7 +188,7 @@ def test_sessions_export_cli_prompt_only_markdown_file(monkeypatch, capsys, tmp_
"--session-id",
"sess",
"--format",
"markdown",
"md",
"--only",
"user-prompts",
],
@ -203,13 +203,13 @@ def test_sessions_export_cli_prompt_only_markdown_file(monkeypatch, capsys, tmp_
assert "I will inspect the auth middleware." not in content
def test_sessions_export_rejects_full_session_markdown(monkeypatch, capsys):
def test_sessions_export_only_rejects_unsupported_format(monkeypatch, capsys):
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def export_all(self, source=None):
return [_sample_session()]
raise AssertionError("should refuse before touching the DB")
def close(self):
pass
@ -218,92 +218,9 @@ def test_sessions_export_rejects_full_session_markdown(monkeypatch, capsys):
monkeypatch.setattr(
sys,
"argv",
["hermes", "sessions", "export", "-", "--format", "markdown"],
["hermes", "sessions", "export", "-", "--format", "html", "--only", "user-prompts"],
)
main_mod.main()
assert (
"Error: --format markdown currently requires --only user-prompts."
in capsys.readouterr().out
)
def test_sessions_export_prompts_cli_stdout(monkeypatch, capsys):
import hermes_cli.main as main_mod
import hermes_state
captured = {}
class FakeDB:
def resolve_session_id(self, session_id):
captured["resolved_from"] = session_id
return "sess-123"
def export_session(self, session_id):
captured["exported"] = session_id
return _sample_session()
def close(self):
captured["closed"] = True
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys,
"argv",
["hermes", "sessions", "export-prompts", "sess"],
)
main_mod.main()
output = capsys.readouterr().out
records = [json.loads(line) for line in output.splitlines()]
assert [record["text"] for record in records] == [
"Why is login broken?",
"Only show me the prompts.",
]
assert captured == {
"resolved_from": "sess",
"exported": "sess-123",
"closed": True,
}
def test_sessions_export_prompts_cli_markdown_file(monkeypatch, capsys, tmp_path):
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def resolve_session_id(self, _session_id):
return "sess-123"
def export_session(self, _session_id):
return _sample_session()
def close(self):
pass
output_path = tmp_path / "prompts.md"
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys,
"argv",
[
"hermes",
"sessions",
"export-prompts",
"sess",
"--format",
"md",
"--output",
str(output_path),
],
)
main_mod.main()
assert f"Exported 2 prompts to {output_path}" in capsys.readouterr().out
content = output_path.read_text(encoding="utf-8")
assert "# User prompts for session sess-123" in content
assert "Why is login broken?" in content
assert "I will inspect the auth middleware." not in content
assert "--only user-prompts supports --format jsonl or md." in capsys.readouterr().out

View file

@ -312,6 +312,32 @@ Exported files contain one JSON object per line with full session metadata and a
`export` accepts the same filters as `prune` / `archive``--older-than` / `--newer-than` / `--before` / `--after` (durations like `5h`/`2d`/`1w`, bare days, or ISO timestamps), `--source`, `--title`, `--model`, `--provider`, `--cwd`, `--min-messages` / `--max-messages`, `--min-tokens` / `--max-tokens`, `--min-cost` / `--max-cost`, `--min-tool-calls` / `--max-tool-calls`, `--user`, `--chat-id`, `--chat-type`, `--branch`, and `--end-reason`. Add `--dry-run` to preview which sessions match without writing anything. Note: bulk filters match *ended* sessions; unfiltered `export` dumps everything, including active ones.
### Export Sessions to HTML
`--format html` writes a single self-contained HTML file — no remote dependencies — with styled message bubbles, collapsible tool output, and (for multi-session exports) a sidebar to switch between sessions:
```bash
# One session as a standalone HTML page
hermes sessions export --format html --session-id 20250305_091523_a1b2c3d4 transcript.html
# All Telegram sessions from the last week in one file, secrets redacted
hermes sessions export --format html --newer-than 1w --source telegram --redact archive.html
```
### Export Only Your Prompts
`--only user-prompts` exports just the prompts you wrote — no assistant replies, tool output, or system context. Useful for building prompt libraries or reviewing what you asked:
```bash
# One JSONL record per prompt (session id, index, timestamp, text)
hermes sessions export prompts.jsonl --session-id 20250305_091523_a1b2c3d4 --only user-prompts
# Markdown, straight to stdout
hermes sessions export - --session-id 20250305_091523_a1b2c3d4 --only user-prompts --format md
```
Works with `--format jsonl` (default) or `md`, honors the same filters for bulk export, and combines with `--redact`.
### Export Sessions to Markdown/QMD
Pass `--format md` or `--format qmd` when you want a readable, file-based archive before hiding or deleting old sessions. Markdown/QMD exports write one file per session into a directory (default: `~/.hermes/session-exports`).

View file

@ -289,6 +289,32 @@ hermes sessions export backup.jsonl --redact
`export` 接受与 `prune` / `archive` 相同的过滤器 — `--older-than` / `--newer-than` / `--before` / `--after`(时长如 `5h`/`2d`/`1w`、纯数字天数或 ISO 时间戳)、`--source``--title``--model``--provider``--cwd``--min-messages` / `--max-messages``--min-tokens` / `--max-tokens``--min-cost` / `--max-cost``--min-tool-calls` / `--max-tool-calls``--user``--chat-id``--chat-type``--branch``--end-reason`。加 `--dry-run` 可预览匹配的 session 而不写入任何内容。注意:带过滤器的批量导出只匹配*已结束*的 session不带过滤器的 `export` 会导出所有 session包括活跃的
### 导出 Session 为 HTML
`--format html` 生成一个完全独立的 HTML 文件 — 无远程依赖 — 带样式化的消息气泡、可折叠的工具输出,多 session 导出时还带侧边栏导航:
```bash
# 将一个 session 导出为独立 HTML 页面
hermes sessions export --format html --session-id 20250305_091523_a1b2c3d4 transcript.html
# 将最近一周的所有 Telegram session 导出到一个文件,并脱敏
hermes sessions export --format html --newer-than 1w --source telegram --redact archive.html
```
### 只导出你的 Prompt
`--only user-prompts` 只导出你写的 prompt — 不含助手回复、工具输出或系统上下文。适合构建 prompt 库或回顾你问过什么:
```bash
# 每个 prompt 一条 JSONL 记录session id、序号、时间戳、文本
hermes sessions export prompts.jsonl --session-id 20250305_091523_a1b2c3d4 --only user-prompts
# Markdown 格式,直接输出到 stdout
hermes sessions export - --session-id 20250305_091523_a1b2c3d4 --only user-prompts --format md
```
支持 `--format jsonl`(默认)或 `md`,批量导出时同样支持全部过滤器,也可与 `--redact` 组合。
### 导出 Session 为 Markdown/QMD
当你想在隐藏或删除旧 session 之前保留一份可读的文件归档时,传入 `--format md``--format qmd`。Markdown/QMD 导出会为每个 session 写入一个文件到目录中(默认:`~/.hermes/session-exports`)。