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 <noreply@anthropic.com>
This commit is contained in:
Alex Newman 2026-01-28 14:10:09 -05:00
parent d9f23cb000
commit 0cefba22fe

View File

@ -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
},
};