Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,50 @@ def handle_tool_call(name: str, arguments: dict) -> str:
entry_id = arguments.get("entry_id", "")
return run_rlm("forget", entry_id)

elif name == "rlm_scratchpad_save":
content = arguments.get("content", "")
label = arguments.get("label", "")
tags = arguments.get("tags", "")
ttl = arguments.get("ttl_hours", 24)
session = arguments.get("analysis_session", "")
cmd = ["scratchpad", "save", content]
if label:
cmd += ["--label", label]
if tags:
cmd += ["--tags", tags]
if ttl != 24:
cmd += ["--ttl", str(ttl)]
if session:
cmd += ["--session", session]
return run_rlm(*cmd)

elif name == "rlm_scratchpad_list":
cmd = ["scratchpad", "list"]
if arguments.get("include_expired"):
cmd += ["--all"]
return run_rlm(*cmd)

elif name == "rlm_scratchpad_get":
entry_id = arguments.get("entry_id", "")
return run_rlm("scratchpad", "get", entry_id)

elif name == "rlm_scratchpad_clear":
cmd = ["scratchpad", "clear"]
if arguments.get("expired_only"):
cmd += ["--expired"]
return run_rlm(*cmd)

elif name == "rlm_scratchpad_promote":
entry_id = arguments.get("entry_id", "")
tags = arguments.get("tags", "")
summary = arguments.get("summary", "")
cmd = ["scratchpad", "promote", entry_id]
if tags:
cmd += ["--tags", tags]
if summary:
cmd += ["--summary", summary]
return run_rlm(*cmd)

else:
return f"Unknown tool: {name}"

Expand Down Expand Up @@ -259,6 +303,118 @@ def handle_tool_call(name: str, arguments: dict) -> str:
"required": ["entry_id"],
},
},
{
"name": "rlm_scratchpad_save",
"description": (
"Save intermediate analysis state to the scratchpad — short-lived working memory "
"for in-progress RLM analyses. Use this to record partial results, hypotheses, "
"or findings mid-analysis so they survive context limits. "
"Entries auto-expire after a configurable TTL (default 24h). "
"Use rlm_scratchpad_promote to graduate important findings to long-term memory."
),
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The intermediate analysis content to save",
},
"label": {
"type": "string",
"description": "Short label/title for this scratchpad entry",
},
"tags": {
"type": "string",
"description": "Comma-separated tags",
},
"ttl_hours": {
"type": "number",
"description": "Hours until this entry expires (default: 24)",
"default": 24,
},
"analysis_session": {
"type": "string",
"description": "Optional RLM analysis session ID to associate with",
},
},
"required": ["content"],
},
},
{
"name": "rlm_scratchpad_list",
"description": (
"List active scratchpad entries (short-lived working memory). "
"Shows entry IDs, labels, sizes, tags, and time until expiry."
),
"inputSchema": {
"type": "object",
"properties": {
"include_expired": {
"type": "boolean",
"description": "Include expired entries (default: false)",
"default": False,
},
},
},
},
{
"name": "rlm_scratchpad_get",
"description": "Get the full content of a specific scratchpad entry by ID.",
"inputSchema": {
"type": "object",
"properties": {
"entry_id": {
"type": "string",
"description": "The scratchpad entry ID (e.g. 'scratch-abc123')",
},
},
"required": ["entry_id"],
},
},
{
"name": "rlm_scratchpad_clear",
"description": (
"Clear scratchpad entries. By default removes all entries. "
"Use expired_only=true to only clean up entries past their TTL."
),
"inputSchema": {
"type": "object",
"properties": {
"expired_only": {
"type": "boolean",
"description": "Only remove expired entries (default: false, removes all)",
"default": False,
},
},
},
},
{
"name": "rlm_scratchpad_promote",
"description": (
"Promote a scratchpad entry to long-term memory. "
"Use this when intermediate analysis results are worth keeping permanently. "
"The entry is moved from the scratchpad into the persistent memory store "
"and removed from the scratchpad."
),
"inputSchema": {
"type": "object",
"properties": {
"entry_id": {
"type": "string",
"description": "The scratchpad entry ID to promote",
},
"tags": {
"type": "string",
"description": "Additional comma-separated tags for the long-term memory entry",
},
"summary": {
"type": "string",
"description": "Summary for the long-term memory entry (defaults to entry label)",
},
},
"required": ["entry_id"],
},
},
]


Expand Down
115 changes: 114 additions & 1 deletion rlm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import os
import sys

from rlm import scanner, chunker, extractor, state, memory, export, url_fetcher, archive
from rlm import scanner, chunker, extractor, state, memory, export, url_fetcher, archive, scratchpad as scratchpad_mod

MAX_OUTPUT = 4000

Expand Down Expand Up @@ -610,6 +610,84 @@ def fmt_bytes(b):
_print("\n".join(lines))


def cmd_scratchpad(args):
"""Scratchpad subcommands: save, list, get, clear, promote."""
action = args.scratchpad_action

if action == "save":
if args.stdin:
import sys as _sys
content = _sys.stdin.read()
elif args.content:
content = args.content
else:
_print("Error: provide content as argument, or use --stdin")
import sys; sys.exit(1)

if not content.strip():
_print("Error: empty content")
import sys; sys.exit(1)

tags = [t.strip() for t in args.tags.split(",")] if args.tags else []
result = scratchpad_mod.save(
content=content,
label=args.label or "",
tags=tags,
ttl_hours=args.ttl,
analysis_session=args.session,
)
lines = [
f"Scratchpad entry saved: {result['id']}",
f"Label: {result['label'] or '(none)'}",
f"TTL: {result['ttl_hours']}h (expires in {result['ttl_hours']:.1f}h)",
f"Size: {result['char_count']:,} chars",
]
if result["tags"]:
lines.append(f"Tags: {', '.join(result['tags'])}")
if result["analysis_session"]:
lines.append(f"Session: {result['analysis_session']}")
_print("\n".join(lines))

elif action == "list":
entries = scratchpad_mod.list_entries(include_expired=args.all)
_print(scratchpad_mod.format_entry_list(entries))

elif action == "get":
entry = scratchpad_mod.get(args.id)
if entry is None:
_print(f"Error: no scratchpad entry with id '{args.id}'")
import sys; sys.exit(1)
_print(scratchpad_mod.format_entry(entry))

elif action == "clear":
n = scratchpad_mod.clear(expired_only=args.expired)
if args.expired:
_print(f"Cleared {n} expired scratchpad entries.")
else:
_print(f"Cleared {n} scratchpad entries.")

elif action == "promote":
tags = [t.strip() for t in args.tags.split(",")] if args.tags else []
result = scratchpad_mod.promote(
entry_id=args.id,
tags=tags or None,
summary=args.summary,
)
if result is None:
_print(f"Error: no scratchpad entry with id '{args.id}'")
import sys; sys.exit(1)
lines = [
f"Promoted to long-term memory: {result['id']}",
f"Summary: {result.get('summary', '')}",
f"Tags: {', '.join(result.get('tags', []))}",
]
_print("\n".join(lines))

else:
_print(f"Unknown scratchpad action: {action}")
import sys; sys.exit(1)


def cmd_tui(args):
"""Launch the interactive TUI dashboard."""
from rlm.tui import RlmTuiApp
Expand Down Expand Up @@ -766,6 +844,41 @@ def main():
p_stats = subparsers.add_parser("stats", help="Show memory store statistics")
p_stats.set_defaults(func=cmd_stats)

# scratchpad
p_scratch = subparsers.add_parser("scratchpad", help="Manage scratchpad (short-lived working memory)")
scratch_sub = p_scratch.add_subparsers(dest="scratchpad_action", required=True)

# scratchpad save
p_scratch_save = scratch_sub.add_parser("save", help="Save a scratchpad entry")
p_scratch_save.add_argument("content", nargs="?", help="Content to save")
p_scratch_save.add_argument("--label", help="Short label/title for this entry")
p_scratch_save.add_argument("--tags", help="Comma-separated tags")
p_scratch_save.add_argument("--ttl", type=float, default=24.0,
help="Time-to-live in hours (default: 24)")
p_scratch_save.add_argument("--session", help="Associate with an RLM analysis session ID")
p_scratch_save.add_argument("--stdin", action="store_true", help="Read content from stdin")

# scratchpad list
p_scratch_list = scratch_sub.add_parser("list", help="List scratchpad entries")
p_scratch_list.add_argument("--all", action="store_true", help="Include expired entries")

# scratchpad get
p_scratch_get = scratch_sub.add_parser("get", help="Get a scratchpad entry by ID")
p_scratch_get.add_argument("id", help="Scratchpad entry ID")

# scratchpad clear
p_scratch_clear = scratch_sub.add_parser("clear", help="Clear scratchpad entries")
p_scratch_clear.add_argument("--expired", action="store_true",
help="Only remove expired entries (leave active ones)")

# scratchpad promote
p_scratch_promote = scratch_sub.add_parser("promote", help="Promote scratchpad entry to long-term memory")
p_scratch_promote.add_argument("id", help="Scratchpad entry ID")
p_scratch_promote.add_argument("--tags", help="Additional comma-separated tags for the memory entry")
p_scratch_promote.add_argument("--summary", help="Override the memory summary")

p_scratch.set_defaults(func=cmd_scratchpad)

# tui
p_tui = subparsers.add_parser("tui", help="Launch interactive TUI dashboard")
p_tui.set_defaults(func=cmd_tui)
Expand Down
Loading