refactor(sessions): fold Markdown/QMD export into 'sessions export --format'

Replaces the parallel export-md subcommand from the salvaged commit with
a --format jsonl|md|qmd flag on the existing export subcommand, so all
session export formats share one surface. Adds AUTHOR_MAP entry.

Salvage follow-up for PR #59542 by @web3blind.
This commit is contained in:
teknium1 2026-07-07 04:26:07 -07:00 committed by Teknium
parent 91885a32b3
commit f3c27e30eb
6 changed files with 148 additions and 83 deletions

View file

@ -13434,46 +13434,54 @@ def main():
)
sessions_export = sessions_subparsers.add_parser(
"export", help="Export sessions to a JSONL file"
"export", help="Export sessions to JSONL, Markdown, or QMD"
)
sessions_export.add_argument(
"output", help="Output JSONL file path (use - for stdout)"
"output",
nargs="?",
help=(
"Output path. JSONL: file path (use - for stdout, required). "
"md/qmd: output directory (default: <hermes home>/session-exports)"
),
)
sessions_export.add_argument(
"--format",
choices=["jsonl", "md", "qmd"],
default="jsonl",
help="Export format (default: jsonl)",
)
sessions_export.add_argument("--source", help="Filter by source")
sessions_export.add_argument("--session-id", help="Export a specific session")
sessions_export_md = sessions_subparsers.add_parser(
"export-md",
help="Export sessions to Markdown/QMD files",
)
sessions_export_md.add_argument(
sessions_export.add_argument(
"--session-id", help="Session ID or unique prefix to export"
)
sessions_export_md.add_argument(
"--older-than", type=int, help="Bulk-export ended sessions older than N days"
sessions_export.add_argument(
"--older-than",
type=int,
help="md/qmd only: bulk-export ended sessions older than N days",
)
sessions_export_md.add_argument("--source", help="Filter bulk export by source")
sessions_export_md.add_argument(
"--dry-run", action="store_true", help="Preview matching sessions without writing files"
sessions_export.add_argument(
"--dry-run",
action="store_true",
help="md/qmd only: preview matching sessions without writing files",
)
sessions_export_md.add_argument(
"--lineage", choices=["single", "logical"], default="single", help="Export one row or compression lineage"
sessions_export.add_argument(
"--lineage",
choices=["single", "logical"],
default="single",
help="md/qmd only: export one row or its compression lineage",
)
sessions_export_md.add_argument(
"--delete-after-verified", action="store_true", help="After verified single-session export, delete that session"
sessions_export.add_argument(
"--delete-after-verified",
action="store_true",
help="md/qmd only: after verified single-session export, delete that session",
)
sessions_export_md.add_argument(
sessions_export.add_argument(
"--yes", "-y", action="store_true", help="Required with --delete-after-verified"
)
sessions_export_md.add_argument(
"--output",
help="Output directory (default: ~/.hermes/session-exports)",
)
sessions_export_md.add_argument(
"--format", choices=["md", "qmd"], default="md", help="Export format"
)
sessions_export_md.add_argument(
"--force", action="store_true", help="Overwrite an existing export file"
sessions_export.add_argument(
"--force",
action="store_true",
help="md/qmd only: overwrite an existing export file",
)
sessions_delete = sessions_subparsers.add_parser(
@ -13751,42 +13759,51 @@ def main():
print(f"{preview:<50} {last_active:<13} {s['source']:<6} {sid}")
elif action == "export":
if args.session_id:
resolved_session_id = db.resolve_session_id(args.session_id)
if not resolved_session_id:
print(f"Session '{args.session_id}' not found.")
if args.format == "jsonl":
if not args.output:
print("JSONL export requires an output path (use - for stdout).")
return
data = db.export_session(resolved_session_id)
if not data:
print(f"Session '{args.session_id}' not found.")
return
line = _json.dumps(data, ensure_ascii=False) + "\n"
if args.output == "-":
if args.session_id:
resolved_session_id = db.resolve_session_id(args.session_id)
if not resolved_session_id:
print(f"Session '{args.session_id}' not found.")
return
data = db.export_session(resolved_session_id)
if not data:
print(f"Session '{args.session_id}' not found.")
return
line = _json.dumps(data, ensure_ascii=False) + "\n"
if args.output == "-":
sys.stdout.write(line)
sys.stdout.write(line)
else:
with open(args.output, "w", encoding="utf-8") as f:
f.write(line)
print(f"Exported 1 session to {args.output}")
else:
with open(args.output, "w", encoding="utf-8") as f:
f.write(line)
print(f"Exported 1 session to {args.output}")
else:
sessions = db.export_all(source=args.source)
if args.output == "-":
sessions = db.export_all(source=args.source)
if args.output == "-":
for s in sessions:
sys.stdout.write(_json.dumps(s, ensure_ascii=False) + "\n")
else:
with open(args.output, "w", encoding="utf-8") as f:
for s in sessions:
f.write(_json.dumps(s, ensure_ascii=False) + "\n")
print(f"Exported {len(sessions)} sessions to {args.output}")
sys.stdout.write(_json.dumps(s, ensure_ascii=False) + "\n")
else:
with open(args.output, "w", encoding="utf-8") as f:
for s in sessions:
f.write(_json.dumps(s, ensure_ascii=False) + "\n")
print(f"Exported {len(sessions)} sessions to {args.output}")
return
elif action == "export-md":
# Markdown / QMD export
from hermes_cli.session_export_md import (
append_manifest_entry,
verify_export_file,
write_session_markdown,
)
if args.output == "-":
print("Markdown/QMD export writes files; stdout (-) is only supported with --format jsonl.")
db.close()
return
output_dir = Path(args.output).expanduser() if args.output else get_hermes_home() / "session-exports"
def _export_one(session_id: str):

View file

@ -15,7 +15,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any
EXPORTER_VERSION = "hermes sessions export-md v1"
EXPORTER_VERSION = "hermes sessions export (md/qmd) v1"
_SHA_LINE_RE = re.compile(r"- SHA256 of exported body: `([0-9a-f]{64})`")

View file

@ -4967,7 +4967,7 @@ class SessionDB:
"""List ended sessions that match read-only export filters.
This helper intentionally does not delete, archive, or mutate rows. It
backs `hermes sessions export-md` bulk exports.
backs `hermes sessions export --format md/qmd` bulk exports.
"""
clauses = ["ended_at IS NOT NULL"]
params: list[Any] = []

View file

@ -838,6 +838,7 @@ AUTHOR_MAP = {
"maks.mir@yahoo.com": "say8hi",
"27719690+Mirac1eSky@users.noreply.github.com": "Mirac1eSky",
"web3blind@users.noreply.github.com": "web3blind",
"264741654+web3blind@users.noreply.github.com": "web3blind",
"julia@alexland.us": "alexg0bot",
"christian@scheid.tech": "scheidti",
# Moonshot schema anyOf+enum salvage (May 2026)

View file

@ -23,10 +23,10 @@ def test_sessions_export_md_writes_single_session(monkeypatch, tmp_path, capsys)
}
def delete_session(self, *args, **kwargs):
raise AssertionError("export-md must not delete sessions")
raise AssertionError("markdown export must not delete sessions")
def prune_sessions(self, *args, **kwargs):
raise AssertionError("export-md must not prune sessions")
raise AssertionError("markdown export must not prune sessions")
def close(self):
captured["closed"] = True
@ -38,10 +38,11 @@ def test_sessions_export_md_writes_single_session(monkeypatch, tmp_path, capsys)
[
"hermes",
"sessions",
"export-md",
"export",
"--format",
"md",
"--session-id",
"20260706_123456",
"--output",
str(tmp_path),
],
)
@ -87,10 +88,11 @@ def test_sessions_export_md_reports_unknown_session(monkeypatch, tmp_path, capsy
[
"hermes",
"sessions",
"export-md",
"export",
"--format",
"md",
"--session-id",
"missing",
"--output",
str(output_dir),
],
)
@ -123,13 +125,12 @@ def test_sessions_export_md_supports_qmd_format(monkeypatch, tmp_path, capsys):
[
"hermes",
"sessions",
"export-md",
"export",
"--session-id",
"s1",
"--output",
str(tmp_path),
"--format",
"qmd",
str(tmp_path),
],
)
@ -139,6 +140,48 @@ def test_sessions_export_md_supports_qmd_format(monkeypatch, tmp_path, capsys):
assert "Exported 1 session" in capsys.readouterr().out
def test_sessions_export_md_rejects_stdout_target(monkeypatch, tmp_path, capsys):
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def resolve_session_id(self, session_id):
raise AssertionError("md export to stdout must be refused before DB access")
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys,
"argv",
["hermes", "sessions", "export", "--format", "md", "--session-id", "s1", "-"],
)
main_mod.main()
assert "only supported with --format jsonl" in capsys.readouterr().out
def test_sessions_export_jsonl_requires_output_path(monkeypatch, capsys):
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def export_all(self, **kwargs):
raise AssertionError("jsonl export without an output path must be refused")
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(sys, "argv", ["hermes", "sessions", "export"])
main_mod.main()
assert "requires an output path" in capsys.readouterr().out
def test_sessions_export_md_bulk_dry_run_lists_candidates(monkeypatch, tmp_path, capsys):
import hermes_cli.main as main_mod
import hermes_state
@ -158,14 +201,15 @@ def test_sessions_export_md_bulk_dry_run_lists_candidates(monkeypatch, tmp_path,
[
"hermes",
"sessions",
"export-md",
"export",
"--format",
"md",
"--older-than",
"30",
"--source",
"cron",
"--output",
str(tmp_path),
"--dry-run",
str(tmp_path),
],
)
@ -193,7 +237,7 @@ def test_sessions_export_md_bulk_requires_filter(monkeypatch, tmp_path, capsys):
monkeypatch.setattr(
sys,
"argv",
["hermes", "sessions", "export-md", "--output", str(tmp_path)],
["hermes", "sessions", "export", "--format", "md", str(tmp_path)],
)
main_mod.main()
@ -222,13 +266,14 @@ def test_sessions_export_md_bulk_writes_manifest(monkeypatch, tmp_path, capsys):
[
"hermes",
"sessions",
"export-md",
"export",
"--format",
"md",
"--older-than",
"90",
"--output",
str(tmp_path),
"--lineage",
"logical",
str(tmp_path),
],
)
@ -257,12 +302,13 @@ def test_sessions_export_md_delete_after_verified_requires_yes(monkeypatch, tmp_
[
"hermes",
"sessions",
"export-md",
"export",
"--format",
"md",
"--session-id",
"s1",
"--output",
str(tmp_path),
"--delete-after-verified",
str(tmp_path),
],
)
@ -298,13 +344,14 @@ def test_sessions_export_md_delete_after_verified_deletes_after_file_check(monke
[
"hermes",
"sessions",
"export-md",
"export",
"--format",
"md",
"--session-id",
"s1",
"--output",
str(tmp_path),
"--delete-after-verified",
"--yes",
str(tmp_path),
],
)

View file

@ -309,26 +309,26 @@ Exported files contain one JSON object per line with full session metadata and a
### Export Sessions to Markdown/QMD
Use Markdown/QMD export when you want a readable, file-based archive before hiding or deleting old sessions.
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`).
```bash
# Export one session to Markdown
hermes sessions export-md --session-id 20250305_091523_a1b2c3d4
hermes sessions export --format md --session-id 20250305_091523_a1b2c3d4
# Export a compression lineage as one logical document
hermes sessions export-md --session-id 20250305_091523_a1b2c3d4 --lineage logical
hermes sessions export --format md --session-id 20250305_091523_a1b2c3d4 --lineage logical
# Preview ended sessions older than 90 days without writing files
hermes sessions export-md --older-than 90 --dry-run
hermes sessions export --format md --older-than 90 --dry-run
# Export ended Telegram sessions older than 30 days to QMD files
hermes sessions export-md --older-than 30 --source telegram --format qmd
hermes sessions export --format qmd --older-than 30 --source telegram
# Only after verification, export and delete one explicitly named session
hermes sessions export-md --session-id 20250305_091523_a1b2c3d4 --delete-after-verified --yes
hermes sessions export --format md --session-id 20250305_091523_a1b2c3d4 --delete-after-verified --yes
```
`export-md` writes one `.md` or `.qmd` file per exported session plus a `manifest.jsonl` with the file path, message count, lineage ids, and SHA-256. Bulk export requires a filter such as `--older-than` or `--source`; a bare bulk export is refused. `--delete-after-verified` is intentionally limited to `--session-id` and requires `--yes`.
Markdown/QMD export writes one `.md` or `.qmd` file per exported session plus a `manifest.jsonl` with the file path, message count, lineage ids, and SHA-256. Bulk export requires a filter such as `--older-than` or `--source`; a bare bulk export is refused. `--delete-after-verified` is intentionally limited to `--session-id` and requires `--yes`.
### Delete a Session