mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
55d675e278
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
163 lines
4.6 KiB
Python
163 lines
4.6 KiB
Python
"""Formatting helpers for MCP tool outputs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Sequence
|
|
|
|
from basic_memory.schemas.search import SearchResponse, SearchResult
|
|
|
|
ANSI_RESET = "\x1b[0m"
|
|
ANSI_BOLD = "\x1b[1m"
|
|
ANSI_DIM = "\x1b[2m"
|
|
ANSI_CYAN = "\x1b[36m"
|
|
|
|
|
|
def _apply_style(text: str, style: str, enabled: bool) -> str:
|
|
if not enabled:
|
|
return text
|
|
return f"{style}{text}{ANSI_RESET}"
|
|
|
|
|
|
def _strip_frontmatter(text: str) -> str:
|
|
lines = text.splitlines()
|
|
if not lines or lines[0].strip() != "---":
|
|
return text
|
|
|
|
for idx in range(1, len(lines)):
|
|
if lines[idx].strip() == "---":
|
|
return "\n".join(lines[idx + 1 :]).lstrip()
|
|
return text
|
|
|
|
|
|
def _parse_title(text: str) -> str | None:
|
|
for line in text.splitlines():
|
|
if line.startswith("# "):
|
|
return line[2:].strip()
|
|
return None
|
|
|
|
|
|
def _truncate(text: str, width: int) -> str:
|
|
if width <= 0:
|
|
return ""
|
|
if len(text) <= width:
|
|
return text
|
|
if width <= 3:
|
|
return text[:width]
|
|
return text[: width - 3] + "..."
|
|
|
|
|
|
def _make_separator(widths: Sequence[int]) -> str:
|
|
return "+" + "+".join("-" * (width + 2) for width in widths) + "+"
|
|
|
|
|
|
def _format_row(values: Sequence[str], widths: Sequence[int]) -> str:
|
|
cells = []
|
|
for value, width in zip(values, widths, strict=True):
|
|
cells.append(f" {_truncate(value, width).ljust(width)} ")
|
|
return "|" + "|".join(cells) + "|"
|
|
|
|
|
|
def _get_result_tags(result: SearchResult) -> str:
|
|
metadata = result.metadata or {}
|
|
if isinstance(metadata, dict):
|
|
tags = metadata.get("tags")
|
|
if isinstance(tags, list):
|
|
return ", ".join(str(tag) for tag in tags if tag)
|
|
return ""
|
|
|
|
|
|
def _get_result_path(result: SearchResult) -> str:
|
|
return result.permalink or result.file_path or ""
|
|
|
|
|
|
def format_search_results_ascii(
|
|
result: SearchResponse,
|
|
query: str | None = None,
|
|
color: bool = False,
|
|
) -> str:
|
|
"""Format search results as an ASCII table for TUI clients."""
|
|
|
|
results = result.results or []
|
|
header_line = _apply_style("Search results", f"{ANSI_BOLD}{ANSI_CYAN}", color)
|
|
lines = [header_line]
|
|
|
|
if query:
|
|
lines.append(f"Query: {query}")
|
|
|
|
summary = (
|
|
f"Results: {len(results)} | Page: {result.current_page} | Page size: {result.page_size}"
|
|
)
|
|
lines.append(_apply_style(summary, ANSI_DIM, color))
|
|
|
|
if not results:
|
|
lines.append("No results.")
|
|
return "\n".join(lines).strip()
|
|
|
|
headers = ["#", "Title", "Type", "Score", "Path", "Tags"]
|
|
rows = []
|
|
for idx, item in enumerate(results, start=1):
|
|
rows.append(
|
|
[
|
|
str(idx),
|
|
item.title or "Untitled",
|
|
item.type.value if hasattr(item.type, "value") else str(item.type),
|
|
f"{item.score:.2f}" if isinstance(item.score, (int, float)) else "",
|
|
_get_result_path(item),
|
|
_get_result_tags(item),
|
|
]
|
|
)
|
|
|
|
max_widths = [3, 32, 10, 7, 36, 24]
|
|
widths = []
|
|
for index, header in enumerate(headers):
|
|
column_values = [header] + [row[index] for row in rows]
|
|
max_len = max(len(value) for value in column_values)
|
|
widths.append(min(max_widths[index], max_len))
|
|
|
|
table = [_make_separator(widths)]
|
|
header_row = _format_row(headers, widths)
|
|
if color:
|
|
header_cells = []
|
|
for value, width in zip(headers, widths, strict=True):
|
|
padded = f" {_truncate(value, width).ljust(width)} "
|
|
header_cells.append(_apply_style(padded, f"{ANSI_BOLD}{ANSI_CYAN}", color))
|
|
header_row = "|" + "|".join(header_cells) + "|"
|
|
table.append(header_row)
|
|
table.append(_make_separator(widths))
|
|
|
|
for row in rows:
|
|
table.append(_format_row(row, widths))
|
|
|
|
table.append(_make_separator(widths))
|
|
|
|
lines.append("")
|
|
lines.extend(table)
|
|
return "\n".join(lines).rstrip()
|
|
|
|
|
|
def format_note_preview_ascii(
|
|
content: str,
|
|
identifier: str | None = None,
|
|
color: bool = False,
|
|
) -> str:
|
|
"""Format note content for ASCII/TUI display."""
|
|
|
|
identifier = identifier or ""
|
|
cleaned = _strip_frontmatter(content)
|
|
title = _parse_title(cleaned) or identifier or "Note Preview"
|
|
|
|
header = _apply_style("Note preview", f"{ANSI_BOLD}{ANSI_CYAN}", color)
|
|
lines = [header, f"Title: {title}"]
|
|
|
|
if identifier:
|
|
lines.append(f"Identifier: {identifier}")
|
|
|
|
lines.append(_apply_style("-" * 72, ANSI_DIM, color))
|
|
|
|
if content.strip():
|
|
lines.append(content.rstrip())
|
|
else:
|
|
lines.append("(empty note)")
|
|
|
|
return "\n".join(lines).rstrip()
|