mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat: add project-log CLI command with JSONL write + auto-render
This commit is contained in:
parent
b1aa22290e
commit
06a3f2bf97
2 changed files with 197 additions and 0 deletions
|
|
@ -310,6 +310,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
p_rl.add_argument("--lookup", help="Look up all reading notes for a paper key")
|
||||
p_rl.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
|
||||
p_pl = sub.add_parser("project-log", help="Record or render project work logs")
|
||||
p_pl.add_argument("--write", action="store_true", help="Write a new project log entry")
|
||||
p_pl.add_argument("--payload", help="JSON payload for the entry")
|
||||
p_pl.add_argument("--project", help="Project name (required for write/list/render)")
|
||||
p_pl.add_argument("--list", action="store_true", help="List all entries for a project")
|
||||
p_pl.add_argument("--render", action="store_true", help="Render project-log.md")
|
||||
p_pl.add_argument("--limit", type=int, default=50, help="Max entries to list")
|
||||
p_pl.add_argument("--json", action="store_true", help="Output as PFResult JSON")
|
||||
|
||||
p_search = sub.add_parser("search", help="Full-text search across the library")
|
||||
p_search.add_argument("query", help="Search query (supports FTS5 syntax)")
|
||||
p_search.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
|
|
@ -568,6 +577,11 @@ def main(argv: list[str] | None = None) -> int:
|
|||
|
||||
return run(args)
|
||||
|
||||
if args.command == "project-log":
|
||||
from paperforge.commands.project_log import run
|
||||
|
||||
return run(args)
|
||||
|
||||
if args.command == "search":
|
||||
from paperforge.commands.search import run
|
||||
|
||||
|
|
|
|||
183
paperforge/commands/project_log.py
Normal file
183
paperforge/commands/project_log.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from paperforge import __version__ as PF_VERSION
|
||||
from paperforge.config import paperforge_paths
|
||||
from paperforge.core.errors import ErrorCode
|
||||
from paperforge.core.result import PFError, PFResult
|
||||
from paperforge.memory.permanent import (
|
||||
append_project_entry,
|
||||
get_project_entries,
|
||||
read_all_project_entries,
|
||||
)
|
||||
|
||||
|
||||
def _render_project_log_md(vault: Path, project: str) -> None:
|
||||
"""Render project-log.md from JSONL."""
|
||||
entries = get_project_entries(vault, project)
|
||||
if not entries:
|
||||
return
|
||||
|
||||
lines = [f"# Project Log — {project}", ""]
|
||||
lines.append("> Auto-generated from project-log.jsonl. Do not edit manually.")
|
||||
lines.append("")
|
||||
|
||||
for entry in sorted(entries, key=lambda x: x.get("created_at", ""), reverse=True):
|
||||
lines.append(f"## {entry.get('date', '')} — {entry.get('title', '(untitled)')}")
|
||||
lines.append(f"**Type:** {entry.get('type', '')}")
|
||||
lines.append("")
|
||||
|
||||
if entry.get("decisions"):
|
||||
lines.append("### Core Decisions")
|
||||
for d in entry["decisions"]:
|
||||
lines.append(f"- {d}")
|
||||
lines.append("")
|
||||
|
||||
if entry.get("detours"):
|
||||
lines.append("### Detours & Corrections")
|
||||
for dt in entry["detours"]:
|
||||
if isinstance(dt, dict):
|
||||
lines.append(f"- **Wrong:** {dt.get('wrong', '')}")
|
||||
lines.append(f" **Correction:** {dt.get('correction', '')}")
|
||||
lines.append(f" **Resolution:** {dt.get('resolution', '')}")
|
||||
else:
|
||||
lines.append(f"- {dt}")
|
||||
lines.append("")
|
||||
|
||||
if entry.get("reusable"):
|
||||
lines.append("### Reusable Methods")
|
||||
for r in entry["reusable"]:
|
||||
lines.append(f"- {r}")
|
||||
lines.append("")
|
||||
|
||||
if entry.get("todos"):
|
||||
lines.append("### Todos")
|
||||
for t in entry["todos"]:
|
||||
done = "x" if t.get("done", False) else " "
|
||||
lines.append(f"- [{done}] {t.get('content', '')}")
|
||||
lines.append("")
|
||||
|
||||
if entry.get("tags"):
|
||||
lines.append(f"**Tags:** {', '.join(entry['tags'])}")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
paths = paperforge_paths(vault)
|
||||
resource_dir = paths.get("resources")
|
||||
if resource_dir:
|
||||
output_dir = resource_dir / "Projects" / project
|
||||
else:
|
||||
output_dir = vault / "Projects" / project
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / "project-log.md"
|
||||
output_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
vault = args.vault_path
|
||||
|
||||
if getattr(args, "write", False):
|
||||
project = getattr(args, "project", "")
|
||||
payload_str = getattr(args, "payload", "")
|
||||
|
||||
if not project:
|
||||
result = PFResult(ok=False, command="project-log", version=PF_VERSION,
|
||||
error=PFError(code=ErrorCode.VALIDATION_ERROR, message="--project is required for --write"))
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(f"Error: {result.error.message}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not payload_str:
|
||||
result = PFResult(ok=False, command="project-log", version=PF_VERSION,
|
||||
error=PFError(code=ErrorCode.VALIDATION_ERROR, message="--payload is required for --write"))
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(f"Error: {result.error.message}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
entry = json.loads(payload_str)
|
||||
entry["project"] = project
|
||||
result_data = append_project_entry(vault, entry)
|
||||
|
||||
_render_project_log_md(vault, project)
|
||||
|
||||
result = PFResult(ok=True, command="project-log", version=PF_VERSION, data=result_data)
|
||||
except json.JSONDecodeError as e:
|
||||
result = PFResult(ok=False, command="project-log", version=PF_VERSION,
|
||||
error=PFError(code=ErrorCode.VALIDATION_ERROR, message=f"Invalid JSON: {e}"))
|
||||
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
print("Written." if result.ok else f"Error: {result.error.message}")
|
||||
return 0 if result.ok else 1
|
||||
|
||||
if getattr(args, "list", False):
|
||||
project = getattr(args, "project", "")
|
||||
if not project:
|
||||
result = PFResult(ok=False, command="project-log", version=PF_VERSION,
|
||||
error=PFError(code=ErrorCode.VALIDATION_ERROR, message="--project is required for --list"))
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(f"Error: {result.error.message}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
entries = get_project_entries(vault, project)
|
||||
data = {"project": project, "entries": entries[:getattr(args, "limit", 50)], "count": len(entries)}
|
||||
result = PFResult(ok=True, command="project-log", version=PF_VERSION, data=data)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(f"{len(entries)} entries for project '{project}'")
|
||||
for e in entries[:5]:
|
||||
print(f" [{e.get('date', '')}] {e.get('type', '')}: {e.get('title', '')}")
|
||||
return 0
|
||||
|
||||
if getattr(args, "render", False):
|
||||
project = getattr(args, "project", "")
|
||||
if not project:
|
||||
result = PFResult(ok=False, command="project-log", version=PF_VERSION,
|
||||
error=PFError(code=ErrorCode.VALIDATION_ERROR, message="--project is required for --render"))
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(f"Error: {result.error.message}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
_render_project_log_md(vault, project)
|
||||
result = PFResult(ok=True, command="project-log", version=PF_VERSION,
|
||||
data={"rendered": True, "project": project})
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(f"Rendered project-log.md for '{project}'")
|
||||
return 0
|
||||
|
||||
# Default: show all projects with entry counts
|
||||
all_entries = read_all_project_entries(vault)
|
||||
project_counts = Counter(e["project"] for e in all_entries if e.get("project"))
|
||||
|
||||
result = PFResult(ok=True, command="project-log", version=PF_VERSION,
|
||||
data={"projects": dict(project_counts)})
|
||||
if getattr(args, "json", False):
|
||||
print(result.to_json())
|
||||
else:
|
||||
if project_counts:
|
||||
print("Projects with log entries:")
|
||||
for proj, cnt in project_counts.most_common():
|
||||
print(f" {proj}: {cnt} entries")
|
||||
else:
|
||||
print("No project log entries found.")
|
||||
return 0
|
||||
Loading…
Reference in a new issue