refactor(hermes_cli): inline bind-board workdir sync, compact export/save renderers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user