mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
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:
parent
91885a32b3
commit
f3c27e30eb
6 changed files with 148 additions and 83 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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})`")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue