refactor(hermes_cli): inline bind-board workdir sync, compact export/save renderers

This commit is contained in:
Teknium
2026-09-02 20:48:06 -07:00
parent 8d0354df15
commit 469c8f45ab
2 changed files with 34 additions and 82 deletions

View File

@@ -9,9 +9,7 @@ import sys
from hermes_cli import projects_db as pdb
def build_parser(
parent_subparsers: argparse._SubParsersAction,
) -> argparse.ArgumentParser:
def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
"""Attach the ``project`` subcommand tree. Returns the top parser."""
parser = parent_subparsers.add_parser(
"project",
@@ -83,7 +81,6 @@ def projects_command(args: argparse.Namespace) -> int:
file=sys.stderr,
)
return 0
handler = _HANDLERS.get(action)
if handler is None:
print(f"Unknown project action: {action}", file=sys.stderr)
@@ -138,23 +135,14 @@ def _print_project(proj) -> None:
if proj.folders:
print(" folders:")
for f in proj.folders:
mark = " *" if f.is_primary else " "
label = f" ({f.label})" if f.label else ""
print(f" {mark} {f.path}{label}")
print(f" {' *' if f.is_primary else ' '} {f.path}{f' ({f.label})' if f.label else ''}")
@_db_command
def _cmd_create(args, conn) -> int:
pid = pdb.create_project(
conn,
name=args.name,
slug=args.slug,
folders=args.folders,
primary_path=args.primary,
description=args.description,
icon=args.icon,
color=args.color,
board_slug=args.board,
conn, name=args.name, slug=args.slug, folders=args.folders, primary_path=args.primary,
description=args.description, icon=args.icon, color=args.color, board_slug=args.board,
)
if args.use:
pdb.set_active(conn, pid)
@@ -170,16 +158,13 @@ def _cmd_create(args, conn) -> int:
@_db_command
def _cmd_list(args, conn) -> int:
active = pdb.get_active_id(conn)
projs = pdb.list_projects(
conn, include_archived=getattr(args, "include_archived", False)
)
projs = pdb.list_projects(conn, include_archived=getattr(args, "include_archived", False))
if not projs:
print("No projects yet. Create one with `hermes project create <name>`.")
return 0
for p in projs:
marker = "*" if p.id == active else " "
flags = " (archived)" if p.archived else ""
print(f"{marker} {p.slug:<24} {p.name}{flags} [{len(p.folders)} folder(s)]")
print(f"{'*' if p.id == active else ' '} {p.slug:<24} {p.name}{flags} [{len(p.folders)} folder(s)]")
return 0
@@ -214,10 +199,7 @@ def _cmd_rename(args, conn, proj) -> int:
@_with_project
def _cmd_set_primary(args, conn, proj) -> int:
if not pdb.set_primary(conn, proj.id, args.path):
return _err(
f"'{args.path}' is not a folder of {proj.slug}; "
f"add it first with `hermes project add-folder`."
)
return _err(f"'{args.path}' is not a folder of {proj.slug}; add it first with `hermes project add-folder`.")
print(f"Set primary of {proj.slug} -> {args.path}")
return 0
@@ -251,28 +233,22 @@ def _flag_command(op: str, verb: str):
@_with_project
def _cmd_bind_board(args, conn, proj) -> int:
pdb.update_project(conn, proj.id, board_slug=args.board)
if args.board.strip():
print(f"Bound {proj.slug} -> board {args.board}")
_sync_board_default_workdir(proj, args.board)
else:
if not args.board.strip():
print(f"Unbound board from {proj.slug}")
return 0
print(f"Bound {proj.slug} -> board {args.board}")
if proj.primary_path: # best-effort: point the bound board's default_workdir at the primary repo
try:
from hermes_cli import kanban_db as kb
slug = kb._normalize_board_slug(args.board)
if slug and (slug == kb.DEFAULT_BOARD or kb.board_exists(slug)):
kb.write_board_metadata(slug, default_workdir=proj.primary_path)
except Exception:
pass
return 0
def _sync_board_default_workdir(proj, board_slug: str) -> None:
"""Best-effort: point the bound board's default_workdir at the primary repo."""
if not proj.primary_path:
return
try:
from hermes_cli import kanban_db as kb
slug = kb._normalize_board_slug(board_slug)
if slug and (slug == kb.DEFAULT_BOARD or kb.board_exists(slug)):
kb.write_board_metadata(slug, default_workdir=proj.primary_path)
except Exception:
pass
_HANDLERS = {
"create": _cmd_create,
"list": _cmd_list,

View File

@@ -37,12 +37,7 @@ def normalize_export_only(only: Optional[str]) -> Optional[ExportOnly]:
raise ValueError(f"Unsupported session export filter: {only}")
def render_sessions_export(
sessions: Iterable[Dict[str, Any]],
*,
fmt: str = "jsonl",
only: Optional[str] = None,
) -> str:
def render_sessions_export(sessions: Iterable[Dict[str, Any]], *, fmt: str = "jsonl", only: Optional[str] = None) -> str:
"""Render exported sessions in a stable, reusable format.
``fmt=jsonl`` with no filter keeps the legacy shape (one full session object per line);
@@ -66,17 +61,13 @@ def render_sessions_export(
if not session_list:
lines += ["_No user prompts found._", ""]
return _finish_markdown(lines)
heading = lambda session: f"Session: {_heading_text(_session_title_or_id(session))}" # noqa: E731
return _finish_markdown(_render_sessions_markdown(
session_list, "Hermes sessions export",
lambda session: f"Session: {_heading_text(_session_title_or_id(session))}",
lambda session: f"Session: {_heading_text(_session_title_or_id(session))}",
_append_session_messages,
session_list, "Hermes sessions export", heading, heading, _append_session_messages,
))
def export_record_count(
sessions: Iterable[Dict[str, Any]], *, only: Optional[str] = None
) -> Tuple[int, str]:
def export_record_count(sessions: Iterable[Dict[str, Any]], *, only: Optional[str] = None) -> Tuple[int, str]:
"""Return ``(count, noun)`` for status messages after an export."""
session_list = list(sessions)
if normalize_export_only(only) == "user-prompts":
@@ -84,9 +75,7 @@ def export_record_count(
return len(session_list), "session"
def iter_user_prompt_records(
sessions: Iterable[Dict[str, Any]]
) -> Iterator[Dict[str, Any]]:
def iter_user_prompt_records(sessions: Iterable[Dict[str, Any]]) -> Iterator[Dict[str, Any]]:
"""Yield one normalized record for each user-authored prompt."""
for session in sessions:
session_id = str(session.get("id") or session.get("session_id") or "")
@@ -124,33 +113,25 @@ def _render_sessions_markdown(sessions, multi_title, single_heading, multi_headi
return lines
def _append_prompt_records(
lines: List[str], session: Dict[str, Any], *, heading_level: int
) -> None:
def _append_prompt_records(lines: List[str], session: Dict[str, Any], *, heading_level: int) -> None:
prompts = list(iter_user_prompt_records([session]))
if not prompts:
lines += ["_No user prompts found._", ""]
return
marker = "#" * heading_level
for prompt in prompts:
timestamp = prompt.get("created_at") or "timestamp unavailable"
lines.append(f"{marker} {prompt['index']}. {timestamp}")
lines.append(f"{marker} {prompt['index']}. {prompt.get('created_at') or 'timestamp unavailable'}")
if (message_id := prompt.get("message_id")) is not None:
lines += [f"Message ID: `{message_id}`", ""]
lines += [str(prompt.get("text") or ""), ""]
def _append_session_messages(
lines: List[str], session: Dict[str, Any], *, heading_level: int
) -> None:
def _append_session_messages(lines: List[str], session: Dict[str, Any], *, heading_level: int) -> None:
marker = "#" * heading_level
visible_messages = [
message for message in _messages(session) if message.get("role") != "system"
]
visible_messages = [message for message in _messages(session) if message.get("role") != "system"]
if not visible_messages:
lines += ["_No messages found._", ""]
return
for message in visible_messages:
role = str(message.get("role") or "unknown")
timestamp = _format_timestamp(message.get("timestamp"))
@@ -163,9 +144,9 @@ def _append_session_messages(
f"<details><summary>{html_escape(tool_name)}</summary>", "",
_fenced_text(text), "", "</details>", "",
]
continue
label = {"user": "User", "assistant": "Assistant"}.get(role, role.title())
lines += [f"{marker} {label}{suffix}", "", text, ""]
else:
label = {"user": "User", "assistant": "Assistant"}.get(role, role.title())
lines += [f"{marker} {label}{suffix}", "", text, ""]
def _messages(session: Dict[str, Any]) -> List[Dict[str, Any]]:
@@ -185,8 +166,7 @@ def _content_part_text(part: Any) -> str:
return part
if isinstance(part, dict):
for key in ("text", "content"):
value = part.get(key)
if isinstance(value, str):
if isinstance(value := part.get(key), str):
return value
return json.dumps(part, ensure_ascii=False, sort_keys=True)
return str(part)
@@ -273,9 +253,7 @@ def normalize_save_format(fmt: Optional[str]) -> str:
"""Map a user-typed /save format token to a canonical format."""
token = (fmt or "json").strip().lower()
if token not in _SAVE_FORMAT_ALIASES:
raise ValueError(
f"Unknown format {token!r} — expected one of: json, md, html"
)
raise ValueError(f"Unknown format {token!r} — expected one of: json, md, html")
return _SAVE_FORMAT_ALIASES[token]
@@ -302,7 +280,5 @@ def render_session_for_save(session: Dict[str, Any], fmt: str) -> str:
def default_save_filename(session_id: str, fmt: str) -> str:
"""Default filename for a /save export of the given session."""
safe_id = "".join(
ch for ch in str(session_id) if ch.isalnum() or ch in ("-", "_")
) or "session"
safe_id = "".join(ch for ch in str(session_id) if ch.isalnum() or ch in ("-", "_")) or "session"
return f"hermes_session_{safe_id}.{fmt}"