From 0cefba22fe187961eee49a2fc83dbe9fa6f252e5 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Wed, 28 Jan 2026 14:10:09 -0500 Subject: [PATCH] feat(memory-claudemem): implement progressive disclosure tools Add two memory tools following claude-mem's 3-layer pattern: - memory_search: Layer 1, compact results (~50-100 tokens) Returns [#id] title for filtering - memory_observations: Layer 3, full details (~500-1000 tokens) Returns narrative with files_modified Both use @sinclair/typebox for schema definition. Part of claude-mem integration (Phase 5/7). Co-Authored-By: Claude Opus 4.5 --- extensions/memory-claudemem/index.ts | 97 +++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/extensions/memory-claudemem/index.ts b/extensions/memory-claudemem/index.ts index a646f542e..d83488c1e 100644 --- a/extensions/memory-claudemem/index.ts +++ b/extensions/memory-claudemem/index.ts @@ -1,4 +1,5 @@ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; +import { Type } from "@sinclair/typebox"; import { claudeMemConfigSchema } from "./config.js"; import { ClaudeMemClient } from "./client.js"; @@ -61,7 +62,101 @@ const claudeMemPlugin = { } }); - // TODO: Phase 5 - Tool registration + // Phase 5: Tool registration + // memory_search - Layer 1: compact results (~50-100 tokens per result) + api.registerTool( + { + name: "memory_search", + label: "Memory Search", + description: + "Search past observations. Returns compact results with IDs. Use memory_observations for full details.", + parameters: Type.Object({ + query: Type.String({ description: "Search query" }), + limit: Type.Optional( + Type.Number({ description: "Max results (default: 10)" }), + ), + }), + async execute(_toolCallId, params) { + const { query, limit = 10 } = params as { + query: string; + limit?: number; + }; + + const results = await client.search(query, limit); + + if (results.length === 0) { + return { + content: [{ type: "text", text: "No relevant memories found." }], + }; + } + + const text = results.map((r) => `[#${r.id}] ${r.title}`).join("\n"); + + return { + content: [ + { + type: "text", + text: `Found ${results.length} memories:\n\n${text}`, + }, + ], + }; + }, + }, + { name: "memory_search" }, + ); + + // memory_observations - Layer 3: full details (~500-1000 tokens per result) + api.registerTool( + { + name: "memory_observations", + label: "Memory Observations", + description: + "Get full details for specific observation IDs. Use after memory_search to filter.", + parameters: Type.Object({ + ids: Type.Array(Type.Number(), { + description: "Array of observation IDs to fetch", + }), + }), + async execute(_toolCallId, params) { + const { ids } = params as { ids: number[] }; + + if (ids.length === 0) { + return { + content: [{ type: "text", text: "No observation IDs provided." }], + }; + } + + const observations = await client.getObservations(ids); + + if (observations.length === 0) { + return { + content: [ + { + type: "text", + text: "No observations found for the given IDs.", + }, + ], + }; + } + + const text = observations + .map((obs) => { + const filesSection = + obs.files_modified && obs.files_modified.length > 0 + ? `\n\nFiles: ${obs.files_modified.join(", ")}` + : ""; + return `## #${obs.id}\n${obs.narrative}${filesSection}`; + }) + .join("\n\n---\n\n"); + + return { + content: [{ type: "text", text }], + }; + }, + }, + { name: "memory_observations" }, + ); + // TODO: Phase 6 - CLI registration }, };