personal memory agent
0
fork

Configure Feed

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

think/tools: add ledger CLI

+186
+2
think/call.py
··· 73 73 74 74 # Mount built-in CLIs (not auto-discovered since they live under think/) 75 75 from think.tools.call import app as journal_app 76 + from think.tools.ledger import app as ledger_app 76 77 from think.tools.navigate import app as navigate_app 77 78 from think.tools.routines import app as routines_app 78 79 from think.tools.sol import app as sol_app 79 80 80 81 call_app.add_typer(journal_app, name="journal") 82 + call_app.add_typer(ledger_app, name="ledger") 81 83 call_app.add_typer(navigate_app, name="navigate") 82 84 call_app.add_typer(routines_app, name="routines") 83 85 call_app.add_typer(sol_app, name="identity")
+184
think/tools/ledger.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + import dataclasses 5 + import json as jsonlib 6 + 7 + import typer 8 + 9 + from think.surfaces import ledger as ledger_surface 10 + from think.utils import require_solstone 11 + 12 + app = typer.Typer(help="Ledger: commitments ↔ closures view", no_args_is_help=True) 13 + 14 + 15 + @app.callback() 16 + def callback() -> None: 17 + require_solstone() 18 + 19 + 20 + def _parse_facets_csv(value: str | None) -> list[str] | None: 21 + if value is None: 22 + return None 23 + return [part.strip() for part in value.split(",") if part.strip()] 24 + 25 + 26 + def _echo_json(payload: object) -> None: 27 + typer.echo(jsonlib.dumps(payload, indent=2, sort_keys=False)) 28 + 29 + 30 + def _render_table(headers: list[str], rows: list[list[str]]) -> None: 31 + if not rows: 32 + return 33 + widths = [ 34 + max(len(header), *(len(row[index]) for row in rows)) 35 + for index, header in enumerate(headers) 36 + ] 37 + typer.echo( 38 + " ".join(header.ljust(widths[index]) for index, header in enumerate(headers)) 39 + ) 40 + typer.echo(" ".join("-" * width for width in widths)) 41 + for row in rows: 42 + typer.echo( 43 + " ".join(cell.ljust(widths[index]) for index, cell in enumerate(row)) 44 + ) 45 + 46 + 47 + def _item_summary(item: ledger_surface.LedgerItem) -> str: 48 + if item.counterparty: 49 + return f"{item.owner}: {item.summary} -> {item.counterparty}" 50 + return f"{item.owner}: {item.summary}" 51 + 52 + 53 + def _render_items(items: list[ledger_surface.LedgerItem]) -> None: 54 + if not items: 55 + typer.echo("No ledger items found.") 56 + return 57 + rows = [ 58 + [ 59 + item.id, 60 + item.state, 61 + str(item.age_days), 62 + _item_summary(item), 63 + item.when or "", 64 + str(item.opened_at), 65 + str(item.closed_at or ""), 66 + ] 67 + for item in items 68 + ] 69 + _render_table( 70 + ["id", "state", "age_days", "summary", "when", "opened_at", "closed_at"], 71 + rows, 72 + ) 73 + 74 + 75 + def _render_decisions(items: list[ledger_surface.Decision]) -> None: 76 + if not items: 77 + typer.echo("No decisions found.") 78 + return 79 + rows = [ 80 + [item.id, item.day, item.owner, item.action, item.context] for item in items 81 + ] 82 + _render_table(["id", "day", "owner", "action", "context"], rows) 83 + 84 + 85 + @app.command("list") 86 + def list_cmd( 87 + state: str = typer.Option("open"), 88 + owner: str | None = typer.Option(None), 89 + counterparty: str | None = typer.Option(None), 90 + age_days_gte: int | None = typer.Option(None, "--age-days-gte"), 91 + closed_since: str | None = typer.Option(None, "--closed-since"), 92 + top: int | None = typer.Option(None, "--top"), 93 + sort: str | None = typer.Option(None), 94 + facets: str | None = typer.Option(None, help="csv"), 95 + json: bool = typer.Option(False, "--json"), 96 + ) -> None: 97 + """List ledger items.""" 98 + if sort is not None and sort not in { 99 + "age_days_desc", 100 + "opened_at_desc", 101 + "closed_at_desc", 102 + }: 103 + raise typer.BadParameter( 104 + "sort must be one of age_days_desc, opened_at_desc, closed_at_desc" 105 + ) 106 + try: 107 + items = ledger_surface.list( 108 + state=state, 109 + owner=owner, 110 + counterparty=counterparty, 111 + age_days_gte=age_days_gte, 112 + closed_since=closed_since, 113 + top=top, 114 + sort=sort, 115 + facets=_parse_facets_csv(facets), 116 + ) 117 + except ValueError as exc: 118 + raise typer.BadParameter(str(exc)) from exc 119 + if json: 120 + _echo_json([dataclasses.asdict(item) for item in items]) 121 + return 122 + _render_items(items) 123 + 124 + 125 + @app.command("get") 126 + def get_cmd(item_id: str, json: bool = typer.Option(False, "--json")) -> None: 127 + """Fetch one ledger item.""" 128 + item = ledger_surface.get(item_id) 129 + if item is None: 130 + typer.echo(f"ledger item not found: {item_id}", err=True) 131 + raise typer.Exit(1) 132 + if json: 133 + _echo_json(dataclasses.asdict(item)) 134 + return 135 + _render_items([item]) 136 + 137 + 138 + @app.command("close") 139 + def close_cmd( 140 + item_id: str, 141 + note: str = typer.Option(..., "--note"), 142 + as_state: str = typer.Option("closed", "--as"), 143 + json: bool = typer.Option(False, "--json"), 144 + ) -> None: 145 + """Manually close or drop one ledger item.""" 146 + if as_state not in {"closed", "dropped"}: 147 + raise typer.BadParameter("as_state must be 'closed' or 'dropped'") 148 + try: 149 + item = ledger_surface.close(item_id, note=note, as_state=as_state) 150 + except KeyError: 151 + typer.echo(f"ledger item not found: {item_id}", err=True) 152 + raise typer.Exit(1) from None 153 + except ValueError as exc: 154 + raise typer.BadParameter(str(exc)) from exc 155 + if json: 156 + _echo_json(dataclasses.asdict(item)) 157 + return 158 + _render_items([item]) 159 + 160 + 161 + @app.command("decisions") 162 + def decisions_cmd( 163 + owner: str | None = typer.Option(None), 164 + since: str | None = typer.Option(None), 165 + involving: str | None = typer.Option(None), 166 + top: int | None = typer.Option(None), 167 + facets: str | None = typer.Option(None, help="csv"), 168 + json: bool = typer.Option(False, "--json"), 169 + ) -> None: 170 + """List deduplicated decisions.""" 171 + try: 172 + items = ledger_surface.decisions( 173 + owner=owner, 174 + since=since, 175 + involving=involving, 176 + top=top, 177 + facets=_parse_facets_csv(facets), 178 + ) 179 + except ValueError as exc: 180 + raise typer.BadParameter(str(exc)) from exc 181 + if json: 182 + _echo_json([dataclasses.asdict(item) for item in items]) 183 + return 184 + _render_decisions(items)