personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'hopper-ukaieu7l'

+157
+48
tests/test_call.py
··· 28 28 """Unknown app name should produce an error.""" 29 29 result = runner.invoke(call_app, ["nonexistent"]) 30 30 assert result.exit_code != 0 31 + 32 + 33 + class TestJournal: 34 + """Tests for 'sol call journal' commands.""" 35 + 36 + def test_journal_app_discovered(self): 37 + """Journal sub-app is registered and shows help.""" 38 + result = runner.invoke(call_app, ["journal", "--help"]) 39 + assert result.exit_code == 0 40 + assert "search" in result.output 41 + assert "events" in result.output 42 + assert "facet" in result.output 43 + assert "news" in result.output 44 + 45 + def test_journal_search(self): 46 + """Search command runs without error.""" 47 + result = runner.invoke(call_app, ["journal", "search", "test", "--limit", "5"]) 48 + assert result.exit_code == 0 49 + assert "results" in result.output 50 + 51 + def test_journal_events(self): 52 + """Events command returns fixture data.""" 53 + result = runner.invoke(call_app, ["journal", "events", "20240101"]) 54 + assert result.exit_code == 0 55 + # Fixture has work + personal events for this day 56 + assert "Team standup" in result.output 57 + 58 + def test_journal_events_with_facet(self): 59 + """Events command filters by facet.""" 60 + result = runner.invoke( 61 + call_app, ["journal", "events", "20240101", "--facet", "work"] 62 + ) 63 + assert result.exit_code == 0 64 + assert "Team standup" in result.output 65 + 66 + def test_journal_facet(self): 67 + """Facet command shows summary for test-facet.""" 68 + result = runner.invoke(call_app, ["journal", "facet", "test-facet"]) 69 + assert result.exit_code == 0 70 + assert "Test Facet" in result.output 71 + 72 + def test_journal_news(self): 73 + """News command reads fixture news.""" 74 + result = runner.invoke( 75 + call_app, ["journal", "news", "work", "--day", "20240101"] 76 + ) 77 + assert result.exit_code == 0 78 + assert "Authentication" in result.output
+5
think/call.py
··· 72 72 73 73 _discover_app_calls() 74 74 75 + # Mount built-in journal CLI (not auto-discovered since it lives under think/) 76 + from think.tools.call import app as journal_app 77 + 78 + call_app.add_typer(journal_app, name="journal") 79 + 75 80 76 81 def main() -> None: 77 82 """Entry point for ``sol call``."""
+104
think/tools/call.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """CLI commands for journal search and browsing. 5 + 6 + Provides human-friendly CLI access to journal operations, paralleling the 7 + MCP tools in ``think/tools/search.py`` and ``think/tools/facets.py`` but 8 + optimized for terminal use. 9 + 10 + Mounted by ``think.call`` as ``sol call journal ...``. 11 + """ 12 + 13 + import typer 14 + 15 + from think.facets import facet_summary, get_facet_news 16 + from think.indexer.journal import get_events as get_events_impl 17 + from think.indexer.journal import search_journal as search_journal_impl 18 + 19 + app = typer.Typer(help="Journal search and browsing.") 20 + 21 + 22 + @app.command() 23 + def search( 24 + query: str = typer.Argument("", help="Search query (FTS5 syntax)."), 25 + limit: int = typer.Option(10, "--limit", "-n", help="Max results."), 26 + offset: int = typer.Option(0, "--offset", help="Skip N results."), 27 + day: str | None = typer.Option(None, "--day", "-d", help="Filter by day YYYYMMDD."), 28 + day_from: str | None = typer.Option( 29 + None, "--day-from", help="Date range start YYYYMMDD." 30 + ), 31 + day_to: str | None = typer.Option( 32 + None, "--day-to", help="Date range end YYYYMMDD." 33 + ), 34 + facet: str | None = typer.Option(None, "--facet", "-f", help="Filter by facet."), 35 + topic: str | None = typer.Option(None, "--topic", "-t", help="Filter by topic."), 36 + ) -> None: 37 + """Search the journal index.""" 38 + total, results = search_journal_impl( 39 + query, 40 + limit, 41 + offset, 42 + day=day, 43 + day_from=day_from, 44 + day_to=day_to, 45 + facet=facet, 46 + topic=topic, 47 + ) 48 + typer.echo(f"{total} results") 49 + for r in results: 50 + meta = r["metadata"] 51 + typer.echo(f"\n--- {meta['day']} | {meta['facet']} | {meta['topic']} ---") 52 + typer.echo(r["text"].strip()) 53 + 54 + 55 + @app.command() 56 + def events( 57 + day: str = typer.Argument(help="Day in YYYYMMDD format."), 58 + facet: str | None = typer.Option(None, "--facet", "-f", help="Filter by facet."), 59 + ) -> None: 60 + """List events for a day.""" 61 + items = get_events_impl(day, facet) 62 + if not items: 63 + typer.echo("No events found.") 64 + return 65 + for ev in items: 66 + time_range = "" 67 + if ev.get("start"): 68 + time_range = ev["start"] 69 + if ev.get("end"): 70 + time_range += f"-{ev['end']}" 71 + time_range = f" ({time_range})" 72 + typer.echo(f"- {ev.get('title', 'Untitled')}{time_range}") 73 + if ev.get("summary"): 74 + typer.echo(f" {ev['summary']}") 75 + 76 + 77 + @app.command() 78 + def facet( 79 + name: str = typer.Argument(help="Facet name."), 80 + ) -> None: 81 + """Show facet summary.""" 82 + try: 83 + summary = facet_summary(name) 84 + except FileNotFoundError: 85 + typer.echo(f"Facet '{name}' not found.", err=True) 86 + raise typer.Exit(1) 87 + typer.echo(summary) 88 + 89 + 90 + @app.command() 91 + def news( 92 + name: str = typer.Argument(help="Facet name."), 93 + day: str | None = typer.Option(None, "--day", "-d", help="Specific day YYYYMMDD."), 94 + limit: int = typer.Option(5, "--limit", "-n", help="Max days to show."), 95 + cursor: str | None = typer.Option(None, "--cursor", help="Pagination cursor."), 96 + ) -> None: 97 + """Read facet news.""" 98 + result = get_facet_news(name, cursor=cursor, limit=limit, day=day) 99 + days = result.get("days", []) 100 + if not days: 101 + typer.echo("No news found.") 102 + return 103 + for entry in days: 104 + typer.echo(entry.get("raw_content", ""))