mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
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:
parent
b172e03c20
commit
f76899facf
5 changed files with 162 additions and 89 deletions
|
|
@ -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).")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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`)。
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue