@@ -1636,6 +1636,175 @@ def _emit_local_search_json(
16361636 sys .exit (0 if (result .get ("results" ) or []) else 1 )
16371637
16381638
1639+ # ── mempalace list — fast direct-to-daemon drawer browser (#191) ────────
1640+ #
1641+ # Pure metadata browse: no ranking, no exclusion, no embedding. Wraps
1642+ # the daemon's GET /list endpoint (which itself wraps the
1643+ # ``mempalace_list_drawers`` MCP tool). Read-only, safe to run during
1644+ # backfill — it just paginates the metadata table.
1645+ #
1646+ # Recall-preserving by design: every drawer matching the wing/room
1647+ # filter is reachable via offset, and no drawer is dropped. This is the
1648+ # human/script counterpart to the existing ``mempalace_list_drawers``
1649+ # MCP tool (which serves the AI path).
1650+
1651+
1652+ _LIST_LIMIT_MAX = 1000 # sanity ceiling; the daemon clamps to 100 anyway
1653+
1654+
1655+ def _print_list_table (data : dict ) -> None :
1656+ """Human-readable multi-line render — drawer_id (short) + wing/room + preview."""
1657+ drawers = data .get ("drawers" ) or []
1658+ total = data .get ("total" , len (drawers ))
1659+ offset = data .get ("offset" , 0 )
1660+ limit = data .get ("limit" , len (drawers ))
1661+ if not drawers :
1662+ print ("\n No drawers found." )
1663+ return
1664+ end = offset + len (drawers )
1665+ print (f"\n Drawers { offset + 1 } –{ end } of { total } (limit { limit } )\n " )
1666+ for d in drawers :
1667+ did = d .get ("drawer_id" , "" )
1668+ short = did [:12 ] if did else "(no-id)"
1669+ wing = d .get ("wing" ) or "(no-wing)"
1670+ room = d .get ("room" ) or "(no-room)"
1671+ preview = (d .get ("content_preview" ) or "" ).replace ("\n " , " " ).strip ()
1672+ if len (preview ) > 80 :
1673+ preview = preview [:77 ] + "..."
1674+ print (f" { short } { wing } /{ room } " )
1675+ if preview :
1676+ print (f" { preview } " )
1677+ print ()
1678+
1679+
1680+ def _print_list_compact (data : dict ) -> None :
1681+ """One line per drawer: ``<id12> <wing>/<room>: <preview[:120]>``."""
1682+ for d in data .get ("drawers" ) or []:
1683+ did = (d .get ("drawer_id" ) or "" )[:12 ]
1684+ wing = d .get ("wing" ) or "-"
1685+ room = d .get ("room" ) or "-"
1686+ preview = (d .get ("content_preview" ) or "" ).replace ("\n " , " " ).strip ()
1687+ if len (preview ) > 120 :
1688+ preview = preview [:117 ] + "..."
1689+ print (f"{ did } { wing } /{ room } : { preview } " )
1690+
1691+
1692+ def _print_list_full (data : dict ) -> None :
1693+ """Labelled sections, no truncation; separators between drawers."""
1694+ drawers = data .get ("drawers" ) or []
1695+ total = data .get ("total" , len (drawers ))
1696+ offset = data .get ("offset" , 0 )
1697+ if not drawers :
1698+ print ("\n No drawers found." )
1699+ return
1700+ print (f"\n Drawers { offset + 1 } –{ offset + len (drawers )} of { total } \n " )
1701+ sep = " " + "─" * 70
1702+ for d in drawers :
1703+ print (sep )
1704+ print (f" drawer_id: { d .get ('drawer_id' , '' )} " )
1705+ print (f" wing: { d .get ('wing' ) or '' } " )
1706+ print (f" room: { d .get ('room' ) or '' } " )
1707+ tags = d .get ("tags" ) or []
1708+ if tags :
1709+ print (f" tags: { ', ' .join (tags )} " )
1710+ print (" content:" )
1711+ for line in (d .get ("content_preview" ) or "" ).splitlines () or ["" ]:
1712+ print (f" { line } " )
1713+ print (sep )
1714+ print ()
1715+
1716+
1717+ def _resolve_list_format (args ) -> str :
1718+ """Pick ``mempalace list`` output format. ``--format`` wins, then ``--json``."""
1719+ fmt = getattr (args , "format" , None )
1720+ if fmt :
1721+ return fmt
1722+ if getattr (args , "json" , False ):
1723+ return "json"
1724+ return "table"
1725+
1726+
1727+ def cmd_list (args ):
1728+ """Fast direct-to-daemon drawer browser (issue #191).
1729+
1730+ Pure metadata listing — wraps ``GET /list?wing=&room=&limit=&offset=``
1731+ on the palace daemon. Output formats: ``table`` (default), ``compact``,
1732+ ``full``, ``json``. Daemon unreachable → stderr error + exit 1.
1733+ """
1734+ fmt = _resolve_list_format (args )
1735+ want_json = fmt == "json"
1736+
1737+ limit = max (1 , min (int (getattr (args , "limit" , 20 ) or 20 ), _LIST_LIMIT_MAX ))
1738+ offset = max (0 , int (getattr (args , "offset" , 0 ) or 0 ))
1739+
1740+ params : dict = {"limit" : limit , "offset" : offset }
1741+ if getattr (args , "wing" , None ):
1742+ params ["wing" ] = args .wing
1743+ if getattr (args , "room" , None ):
1744+ params ["room" ] = args .room
1745+
1746+ try :
1747+ data = _call_daemon_rest ("/list" , params )
1748+ except DaemonError as e :
1749+ # Match cmd_status's daemon-down fallback (line 2230) and the
1750+ # graceful 401/403 + unreachable handling added in 850e08c. On
1751+ # JSON output, emit a structured error so machine callers see
1752+ # the same shape as other failure paths.
1753+ if want_json :
1754+ _emit_json ({"error" : str (e ), "source" : "daemon" })
1755+ else :
1756+ print (
1757+ f"palace daemon unreachable at { _daemon_url ()} — "
1758+ f"see mempalace status for diagnostics ({ e } )" ,
1759+ file = sys .stderr ,
1760+ )
1761+ sys .exit (1 )
1762+
1763+ if data is None :
1764+ # _call_daemon_rest returns None on 404/401/403 — endpoint
1765+ # missing on an older daemon, or auth mismatch. Same exit code
1766+ # as the unreachable case so scripts can treat "no daemon list"
1767+ # uniformly without parsing the message.
1768+ if want_json :
1769+ _emit_json ({"error" : "daemon /list unavailable" , "source" : "daemon" })
1770+ else :
1771+ print (
1772+ f"palace daemon unreachable at { _daemon_url ()} — "
1773+ "see mempalace status for diagnostics" ,
1774+ file = sys .stderr ,
1775+ )
1776+ sys .exit (1 )
1777+
1778+ # Daemon /list mirrors mempalace_list_drawers' shape: error key when
1779+ # the underlying palace is unreachable from inside the daemon.
1780+ if "error" in data and not data .get ("drawers" ):
1781+ if want_json :
1782+ _emit_json (data )
1783+ else :
1784+ print (f"\n { data ['error' ]} " , file = sys .stderr )
1785+ sys .exit (2 )
1786+
1787+ if want_json :
1788+ # Stable top-level shape — drawers/total/count/offset/limit pass
1789+ # through unchanged so scripts can rely on the keys.
1790+ out = {
1791+ "drawers" : data .get ("drawers" ) or [],
1792+ "total" : data .get ("total" , 0 ),
1793+ "count" : data .get ("count" , len (data .get ("drawers" ) or [])),
1794+ "offset" : data .get ("offset" , offset ),
1795+ "limit" : data .get ("limit" , limit ),
1796+ }
1797+ _emit_json (out )
1798+ return
1799+
1800+ if fmt == "compact" :
1801+ _print_list_compact (data )
1802+ elif fmt == "full" :
1803+ _print_list_full (data )
1804+ else :
1805+ _print_list_table (data )
1806+
1807+
16391808def cmd_wakeup (args ):
16401809 """Show L0 (identity) + L1 (essential story) — the wake-up context."""
16411810 from .layers import MemoryStack
@@ -3456,6 +3625,36 @@ def main():
34563625 ),
34573626 )
34583627
3628+ # list — fast direct-to-daemon drawer browser (#191)
3629+ p_list = sub .add_parser (
3630+ "list" ,
3631+ help = "Browse drawers by wing/room metadata (no ranking, no embedding)" ,
3632+ )
3633+ p_list .add_argument ("--wing" , default = None , help = "Limit to one wing" )
3634+ p_list .add_argument ("--room" , default = None , help = "Limit to one room" )
3635+ p_list .add_argument (
3636+ "--limit" ,
3637+ type = int ,
3638+ default = 20 ,
3639+ help = "Max drawers to return (default: 20, max: 1000)" ,
3640+ )
3641+ p_list .add_argument (
3642+ "--offset" ,
3643+ type = int ,
3644+ default = 0 ,
3645+ help = "Pagination offset (default: 0)" ,
3646+ )
3647+ p_list .add_argument (
3648+ "--format" ,
3649+ choices = ("table" , "compact" , "full" , "json" ),
3650+ default = None ,
3651+ help = (
3652+ "Output format: table (default, multi-line preview), "
3653+ "compact (one line per drawer), full (labelled sections, "
3654+ "no truncation), json (machine-readable; same as --json)"
3655+ ),
3656+ )
3657+
34593658 # compress
34603659 p_compress = sub .add_parser (
34613660 "compress" , help = "Compress drawers using AAAK Dialect (~30x reduction)"
@@ -3855,6 +4054,7 @@ def _nonneg_int(value: str) -> int:
38554054 "mine" : cmd_mine ,
38564055 "split" : cmd_split ,
38574056 "search" : cmd_search ,
4057+ "list" : cmd_list ,
38584058 "export" : cmd_export ,
38594059 "sweep" : cmd_sweep ,
38604060 "sync" : cmd_sync ,
0 commit comments