@memorystack/clawdbot-memorystack 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
2
+ import { MemoryStackClient } from "@memorystack/sdk";
3
+ import type { MemorystackConfig } from "../config.ts";
4
+ import { log } from "../logger.ts";
5
+
6
+ export function registerCliCommands(
7
+ api: ClawdbotPluginApi,
8
+ cfg: MemorystackConfig,
9
+ ): void {
10
+ api.registerCli(
11
+ // biome-ignore lint/suspicious/noExplicitAny: clawdbot SDK does not ship types
12
+ ({ program }: { program: any }) => {
13
+ const cmd = program
14
+ .command("memorystack")
15
+ .description("MemoryStack long-term memory commands");
16
+
17
+ // clawdbot memorystack search <query>
18
+ cmd
19
+ .command("search")
20
+ .argument("<query>", "Search query")
21
+ .option("--limit <n>", "Max results", "5")
22
+ .option("--type <type>", "Filter by memory type (fact, preference, episode)")
23
+ .action(async (query: string, opts: { limit: string; type?: string }) => {
24
+ const limit = Number.parseInt(opts.limit, 10) || 5;
25
+ log.debug(`cli search: query="${query}" limit=${limit}`);
26
+
27
+ try {
28
+ const client = new MemoryStackClient({
29
+ apiKey: cfg.apiKey,
30
+ baseUrl: cfg.baseUrl,
31
+ enableLogging: cfg.debug,
32
+ });
33
+
34
+ const results = await client.search(query, {
35
+ limit,
36
+ memory_type: opts.type,
37
+ });
38
+
39
+ if (results.count === 0) {
40
+ console.log("No memories found.");
41
+ return;
42
+ }
43
+
44
+ console.log(`Found ${results.count} memories:\n`);
45
+ for (const r of results.results) {
46
+ const conf = r.confidence
47
+ ? ` (${(r.confidence * 100).toFixed(0)}%)`
48
+ : "";
49
+ const type = r.memory_type ? ` [${r.memory_type}]` : "";
50
+ console.log(`- ${r.content}${type}${conf}`);
51
+ }
52
+ } catch (err) {
53
+ console.error("Search failed:", err);
54
+ }
55
+ });
56
+
57
+ // clawdbot memorystack stats
58
+ cmd
59
+ .command("stats")
60
+ .description("View usage statistics")
61
+ .action(async () => {
62
+ log.debug("cli stats");
63
+
64
+ try {
65
+ const client = new MemoryStackClient({
66
+ apiKey: cfg.apiKey,
67
+ baseUrl: cfg.baseUrl,
68
+ enableLogging: cfg.debug,
69
+ });
70
+
71
+ const stats = await client.getStats();
72
+
73
+ console.log("\nšŸ“Š MemoryStack Statistics\n");
74
+ console.log(`Total Memories: ${stats.totals.total_memories}`);
75
+ console.log(`Total API Calls: ${stats.totals.total_api_calls}`);
76
+ console.log(`This Month's Calls: ${stats.usage.current_month_api_calls}/${stats.usage.monthly_api_limit}`);
77
+ console.log(`Plan: ${stats.plan_tier || "Free"}`);
78
+ } catch (err) {
79
+ console.error("Failed to get stats:", err);
80
+ }
81
+ });
82
+
83
+ // clawdbot memorystack add <text>
84
+ cmd
85
+ .command("add")
86
+ .argument("<text>", "Text to save as memory")
87
+ .option("--type <type>", "Memory type (fact, preference, episode)")
88
+ .action(async (text: string, opts: { type?: string }) => {
89
+ log.debug(`cli add: "${text.slice(0, 50)}"`);
90
+
91
+ try {
92
+ const client = new MemoryStackClient({
93
+ apiKey: cfg.apiKey,
94
+ baseUrl: cfg.baseUrl,
95
+ enableLogging: cfg.debug,
96
+ });
97
+
98
+ await client.add(text, {
99
+ metadata: {
100
+ source: "clawdbot_cli",
101
+ memory_type: opts.type,
102
+ },
103
+ });
104
+
105
+ console.log(`āœ“ Saved: "${text.length > 60 ? text.slice(0, 60) + "…" : text}"`);
106
+ } catch (err) {
107
+ console.error("Failed to add memory:", err);
108
+ }
109
+ });
110
+
111
+ // clawdbot memorystack deleteall (destructive!)
112
+ cmd
113
+ .command("deleteall")
114
+ .description("Delete ALL your memories (destructive, requires confirmation)")
115
+ .action(async () => {
116
+ const readline = await import("node:readline");
117
+ const rl = readline.createInterface({
118
+ input: process.stdin,
119
+ output: process.stdout,
120
+ });
121
+
122
+ const answer = await new Promise<string>((resolve) => {
123
+ rl.question(
124
+ "āš ļø This will permanently delete ALL your memories. Type 'yes' to confirm: ",
125
+ resolve,
126
+ );
127
+ });
128
+ rl.close();
129
+
130
+ if (answer.trim().toLowerCase() !== "yes") {
131
+ console.log("Aborted.");
132
+ return;
133
+ }
134
+
135
+ log.debug("cli wipe: confirmed");
136
+
137
+ try {
138
+ const client = new MemoryStackClient({
139
+ apiKey: cfg.apiKey,
140
+ baseUrl: cfg.baseUrl,
141
+ enableLogging: cfg.debug,
142
+ });
143
+
144
+ // List all memories first, then delete them
145
+ const memories = await client.listMemories({ limit: 1000 });
146
+ if (memories.count === 0) {
147
+ console.log("No memories to delete.");
148
+ return;
149
+ }
150
+
151
+ const memoryIds = memories.results.map(m => m.id);
152
+ const result = await client.deleteMemories(memoryIds, true);
153
+ console.log(`Wiped ${result.deleted_count} memories.`);
154
+ } catch (err) {
155
+ console.error("Failed to wipe memories:", err);
156
+ }
157
+ });
158
+ },
159
+ { commands: ["memorystack"] },
160
+ );
161
+ }
@@ -0,0 +1,130 @@
1
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
2
+ import { MemoryStackClient } from "@memorystack/sdk";
3
+ import type { MemorystackConfig } from "../config.ts";
4
+ import { log } from "../logger.ts";
5
+
6
+ export function registerSlashCommands(
7
+ api: ClawdbotPluginApi,
8
+ cfg: MemorystackConfig,
9
+ getSessionKey: () => string | undefined,
10
+ getContext: () => Record<string, any>,
11
+ ): void {
12
+ // /add <text> - Save something to memory
13
+ api.registerCommand({
14
+ name: "add",
15
+ description: "Save something to long-term memory",
16
+ acceptsArgs: true,
17
+ requireAuth: true,
18
+ handler: async (ctx: { args?: string }) => {
19
+ const text = ctx.args?.trim();
20
+ if (!text) {
21
+ return { text: "Usage: /add <text to remember>" };
22
+ }
23
+
24
+ log.debug(`/add command: "${text.slice(0, 50)}"`);
25
+
26
+ try {
27
+ const client = new MemoryStackClient({
28
+ apiKey: cfg.apiKey,
29
+ baseUrl: cfg.baseUrl,
30
+ enableLogging: cfg.debug,
31
+ });
32
+
33
+ const context = getContext();
34
+ const sessionKey = getSessionKey();
35
+ const effectiveAgentId = context.subagentId || context.agentId || "main";
36
+
37
+ await client.add(text, {
38
+ agentId: effectiveAgentId,
39
+ sessionId: sessionKey,
40
+ metadata: {
41
+ source: "clawdbot_command",
42
+ session_key: sessionKey,
43
+ },
44
+ });
45
+
46
+ const preview = text.length > 60 ? `${text.slice(0, 60)}…` : text;
47
+ return { text: `āœ“ Saved to memory: "${preview}"` };
48
+ } catch (err) {
49
+ log.error("/add failed", err);
50
+ return { text: "Failed to save memory. Check logs for details." };
51
+ }
52
+ },
53
+ });
54
+
55
+ // /search <query> - Search memories
56
+ api.registerCommand({
57
+ name: "search",
58
+ description: "Search your long-term memories",
59
+ acceptsArgs: true,
60
+ requireAuth: true,
61
+ handler: async (ctx: { args?: string }) => {
62
+ const query = ctx.args?.trim();
63
+ if (!query) {
64
+ return { text: "Usage: /search <search query>" };
65
+ }
66
+
67
+ log.debug(`/search command: "${query}"`);
68
+
69
+ try {
70
+ const client = new MemoryStackClient({
71
+ apiKey: cfg.apiKey,
72
+ baseUrl: cfg.baseUrl,
73
+ enableLogging: cfg.debug,
74
+ });
75
+
76
+ const results = await client.search(query, { limit: 5 });
77
+
78
+ if (results.count === 0) {
79
+ return { text: `No memories found for: "${query}"` };
80
+ }
81
+
82
+ const lines = results.results.map((r, i) => {
83
+ const conf = r.confidence
84
+ ? ` (${(r.confidence * 100).toFixed(0)}%)`
85
+ : "";
86
+ const type = r.memory_type ? ` [${r.memory_type}]` : "";
87
+ return `${i + 1}. ${r.content}${type}${conf}`;
88
+ });
89
+
90
+ return {
91
+ text: `Found ${results.count} memories:\n\n${lines.join("\n")}`,
92
+ };
93
+ } catch (err) {
94
+ log.error("/search failed", err);
95
+ return { text: "Failed to search memories. Check logs for details." };
96
+ }
97
+ },
98
+ });
99
+
100
+ // /stats - View memory usage statistics
101
+ api.registerCommand({
102
+ name: "stats",
103
+ description: "View your memory usage statistics",
104
+ acceptsArgs: false,
105
+ requireAuth: true,
106
+ handler: async () => {
107
+ log.debug("/stats command");
108
+
109
+ try {
110
+ const client = new MemoryStackClient({
111
+ apiKey: cfg.apiKey,
112
+ baseUrl: cfg.baseUrl,
113
+ enableLogging: cfg.debug,
114
+ });
115
+
116
+ const stats = await client.getStats();
117
+
118
+ return {
119
+ text: `šŸ“Š Memory Statistics\n\n` +
120
+ `Total Memories: ${stats.totals.total_memories}\n` +
121
+ `API Calls (this month): ${stats.usage.current_month_api_calls}/${stats.usage.monthly_api_limit}\n` +
122
+ `Plan: ${stats.plan_tier || "Free"}`,
123
+ };
124
+ } catch (err) {
125
+ log.error("/stats failed", err);
126
+ return { text: "Failed to get stats. Check logs for details." };
127
+ }
128
+ },
129
+ });
130
+ }
package/hooks/capture.ts CHANGED
@@ -18,7 +18,11 @@ function getLastTurn(messages: unknown[]): unknown[] {
18
18
  return lastUserIdx >= 0 ? messages.slice(lastUserIdx) : messages;
19
19
  }
20
20
 
21
- export function buildCaptureHandler(cfg: MemorystackConfig) {
21
+ export function buildCaptureHandler(
22
+ cfg: MemorystackConfig,
23
+ getSessionKey?: () => string | undefined,
24
+ getContext?: () => Record<string, any>,
25
+ ) {
22
26
  return async (event: Record<string, unknown>) => {
23
27
  if (
24
28
  !event.success ||
@@ -72,6 +76,8 @@ export function buildCaptureHandler(cfg: MemorystackConfig) {
72
76
  if (captured.length === 0) return;
73
77
 
74
78
  const content = captured.join("\n\n");
79
+ const sessionKey = getSessionKey?.();
80
+ const ctx = getContext?.() || {};
75
81
 
76
82
  log.debug(
77
83
  `capturing ${captured.length} texts (${content.length} chars)`,
@@ -84,8 +90,28 @@ export function buildCaptureHandler(cfg: MemorystackConfig) {
84
90
  enableLogging: cfg.debug,
85
91
  });
86
92
 
93
+ // Prepare metadata from context
94
+ const metadata: Record<string, any> = {
95
+ source: "clawdbot_auto_capture",
96
+ session_id: sessionKey,
97
+ timestamp: new Date().toISOString(),
98
+ user_id: ctx.userId,
99
+ agent_id: ctx.agentId,
100
+ provider: ctx.Provider,
101
+ model: ctx.Model,
102
+ sender_name: ctx.SenderName,
103
+ };
104
+
105
+ // Remove undefined values
106
+ Object.keys(metadata).forEach(key => metadata[key] === undefined && delete metadata[key]);
107
+
87
108
  await client.add(content, {
88
- metadata: { source: "clawdbot_auto_capture", timestamp: new Date().toISOString() },
109
+ sessionId: sessionKey,
110
+ agentId: ctx.agentId,
111
+ userId: ctx.userId,
112
+ teamId: ctx.teamId,
113
+ conversationId: ctx.conversationId,
114
+ metadata: metadata,
89
115
  });
90
116
 
91
117
  log.debug(`captured ${captured.length} messages successfully`);
package/index.ts CHANGED
@@ -4,8 +4,41 @@ import { initLogger } from "./logger.ts";
4
4
  import { registerSearchTool } from "./tools/search.ts";
5
5
  import { registerAddTool } from "./tools/add.ts";
6
6
  import { registerStatsTool } from "./tools/stats.ts";
7
+ import { registerDeleteTool } from "./tools/delete.ts";
8
+ import { registerReflectTool } from "./tools/reflect.ts";
9
+ import { registerConsolidateTool } from "./tools/consolidate.ts";
7
10
  import { buildRecallHandler } from "./hooks/recall.ts";
8
11
  import { buildCaptureHandler } from "./hooks/capture.ts";
12
+ import { registerSlashCommands } from "./commands/slash.ts";
13
+ import { registerCliCommands } from "./commands/cli.ts";
14
+
15
+ // Helper to properly parse Moltbot session keys
16
+ // Format: "agent:{agentId}:{rest}" where rest may be "subagent:{uuid}" or "main" etc.
17
+ function parseSessionKey(sessionKey: string | undefined | null): {
18
+ agentId: string;
19
+ subagentId: string | null;
20
+ sessionId: string;
21
+ } {
22
+ const DEFAULT_AGENT = "main";
23
+ if (!sessionKey) return { agentId: DEFAULT_AGENT, subagentId: null, sessionId: "" };
24
+
25
+ const parts = sessionKey.split(":");
26
+ if (parts.length < 3 || parts[0] !== "agent") {
27
+ // Not a standard agent session key, return as-is
28
+ return { agentId: DEFAULT_AGENT, subagentId: null, sessionId: sessionKey };
29
+ }
30
+
31
+ const agentId = parts[1] || DEFAULT_AGENT;
32
+ const rest = parts.slice(2).join(":");
33
+
34
+ // Check if this is a subagent session
35
+ let subagentId: string | null = null;
36
+ if (rest.toLowerCase().startsWith("subagent:")) {
37
+ subagentId = rest.slice("subagent:".length);
38
+ }
39
+
40
+ return { agentId, subagentId, sessionId: sessionKey };
41
+ }
9
42
 
10
43
  export default {
11
44
  id: "clawdbot-memorystack",
@@ -24,22 +57,72 @@ export default {
24
57
 
25
58
  initLogger(api.logger, cfg.debug);
26
59
 
60
+ // Context tracking
61
+ let currentContext: Record<string, any> = {};
62
+ const getContext = () => currentContext;
63
+ const getSessionKey = () => currentContext.sessionKey;
64
+
27
65
  // Register tools
28
- registerSearchTool(api, cfg);
29
- registerAddTool(api, cfg);
66
+ registerSearchTool(api, cfg, getSessionKey, getContext);
67
+ registerAddTool(api, cfg, getSessionKey, getContext);
30
68
  registerStatsTool(api, cfg);
69
+ registerDeleteTool(api, cfg);
70
+ registerReflectTool(api, cfg);
71
+ registerConsolidateTool(api, cfg);
31
72
 
32
- // Auto-recall hook
33
- if (cfg.autoRecall) {
34
- const recallHandler = buildRecallHandler(cfg);
35
- api.on("before_agent_start", recallHandler);
36
- }
73
+ // ALWAYS capture context on before_agent_start (regardless of autoRecall setting)
74
+ // This ensures tools like memorystack_add have access to agentId, subagentId, etc.
75
+ const recallHandler = cfg.autoRecall ? buildRecallHandler(cfg, getSessionKey) : null;
76
+
77
+ api.on("before_agent_start", (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
78
+ if (ctx) {
79
+ currentContext = { ...ctx };
80
+
81
+ // Parse session key to extract proper IDs
82
+ // Moltbot passes agentId incorrectly as "agent" (the literal prefix)
83
+ // We need to parse the sessionKey to get the actual agentId
84
+ const parsed = parseSessionKey(currentContext.sessionKey as string);
85
+
86
+ // Use parsed agentId, overriding the incorrect ctx.agentId
87
+ if (!currentContext.agentId || currentContext.agentId === "agent") {
88
+ currentContext.agentId = parsed.agentId;
89
+ }
90
+
91
+ // Track subagent ID separately if present (for scoped searches)
92
+ if (parsed.subagentId) {
93
+ currentContext.subagentId = parsed.subagentId;
94
+ }
95
+
96
+ // Normalize other commonly used fields
97
+ if (!currentContext.userId && currentContext.SenderId) currentContext.userId = currentContext.SenderId;
98
+
99
+ // Map group context to MemoryStack scoping
100
+ if (currentContext.groupId) currentContext.teamId = currentContext.groupId;
101
+ if (currentContext.groupChannel) currentContext.conversationId = currentContext.groupChannel;
102
+ // Fallback for conversation ID from session key if no group channel
103
+ if (!currentContext.conversationId && getSessionKey()) currentContext.conversationId = getSessionKey();
104
+ }
105
+
106
+ // Only run recall if autoRecall is enabled
107
+ if (recallHandler) {
108
+ return recallHandler(event);
109
+ }
110
+ });
37
111
 
38
112
  // Auto-capture hook
39
113
  if (cfg.autoCapture) {
40
- api.on("agent_end", buildCaptureHandler(cfg));
114
+ const captureHandler = buildCaptureHandler(cfg, getSessionKey, getContext);
115
+ api.on("agent_end", (event: Record<string, unknown>) => {
116
+ return captureHandler(event);
117
+ });
41
118
  }
42
119
 
120
+ // Register slash commands (/add, /search, /stats)
121
+ registerSlashCommands(api, cfg, getSessionKey, getContext);
122
+
123
+ // Register CLI commands (memorystack search, stats, add, wipe)
124
+ registerCliCommands(api, cfg);
125
+
43
126
  // Register service
44
127
  api.registerService({
45
128
  id: "clawdbot-memorystack",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorystack/clawdbot-memorystack",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Clawdbot MemoryStack memory plugin - long-term memory for your AI assistant",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@memorystack/sdk": "^1.0.3",
25
+ "@memorystack/sdk": "^1.0.4",
26
26
  "@sinclair/typebox": "0.34.47"
27
27
  },
28
28
  "peerDependencies": {
package/tools/add.ts CHANGED
@@ -7,21 +7,51 @@ import { log } from "../logger.ts";
7
7
  export function registerAddTool(
8
8
  api: ClawdbotPluginApi,
9
9
  cfg: MemorystackConfig,
10
+ getSessionKey?: () => string | undefined,
11
+ getContext?: () => Record<string, any>,
10
12
  ): void {
11
13
  api.registerTool(
12
14
  {
13
15
  name: "memorystack_add",
14
16
  label: "Memory Add",
15
- description: "Save important information to long-term memory with automatic importance scoring.",
17
+ description: `Save important information to long-term memory with automatic importance scoring.
18
+
19
+ **Automatic Context Capture**: The following are automatically captured from your session context:
20
+ - agent_id: Your agent identifier (e.g., "main", subagent UUID)
21
+ - session_id: Current session key for conversation tracking
22
+ - team_id: Team/group identifier (if in a group chat)
23
+ - conversation_id: Specific conversation thread ID
24
+ - user_id: The sender's ID (can be overridden with userId parameter)
25
+
26
+ You don't need to specify these manually - they are captured automatically to organize memories.`,
16
27
  parameters: Type.Object({
17
28
  text: Type.String({ description: "Information to remember" }),
18
- userId: Type.Optional(Type.String({ description: "User ID (for B2B apps)" })),
29
+ userId: Type.Optional(Type.String({ description: "Override user ID (defaults to sender)" })),
30
+ memory_type: Type.Optional(
31
+ Type.String({
32
+ description: "Type of memory: fact, preference, episode, procedure, belief",
33
+ }),
34
+ ),
19
35
  }),
20
36
  async execute(
21
37
  _toolCallId: string,
22
- params: { text: string; userId?: string },
38
+ params: { text: string; userId?: string; memory_type?: string },
23
39
  ) {
24
- log.debugRequest("add", { textLength: params.text.length, userId: params.userId });
40
+ const sessionKey = getSessionKey?.();
41
+ const ctx = getContext?.() || {};
42
+
43
+ // For subagents, use the subagent UUID as the agent_id
44
+ // This ensures subagent memories are properly attributed
45
+ const effectiveAgentId = ctx.subagentId || ctx.agentId || "main";
46
+
47
+ log.debugRequest("add", {
48
+ textLength: params.text.length,
49
+ userId: params.userId,
50
+ sessionKey,
51
+ agentId: ctx.agentId,
52
+ subagentId: ctx.subagentId,
53
+ effectiveAgentId,
54
+ });
25
55
 
26
56
  const client = new MemoryStackClient({
27
57
  apiKey: cfg.apiKey,
@@ -29,9 +59,30 @@ export function registerAddTool(
29
59
  enableLogging: cfg.debug,
30
60
  });
31
61
 
62
+ // Prepare metadata from context
63
+ const metadata: Record<string, any> = {
64
+ source: "clawdbot_tool",
65
+ session_key: sessionKey,
66
+ timestamp: new Date().toISOString(),
67
+ user_id: params.userId ?? ctx.userId,
68
+ parent_agent_id: ctx.agentId, // Track the parent agent
69
+ subagent_id: ctx.subagentId, // Track if this is a subagent
70
+ provider: ctx.Provider,
71
+ model: ctx.Model,
72
+ sender_name: ctx.SenderName,
73
+ };
74
+
75
+ // Remove undefined values
76
+ Object.keys(metadata).forEach(key => metadata[key] === undefined && delete metadata[key]);
77
+
32
78
  const result = await client.add(params.text, {
33
- userId: params.userId,
34
- metadata: { source: "clawdbot_tool" },
79
+ userId: params.userId ?? ctx.userId,
80
+ sessionId: sessionKey,
81
+ agentId: effectiveAgentId, // Use subagentId if available!
82
+ teamId: ctx.teamId,
83
+ conversationId: ctx.conversationId,
84
+ memory_type: params.memory_type,
85
+ metadata: metadata,
35
86
  });
36
87
 
37
88
  log.debugResponse("add", {
@@ -0,0 +1,116 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
3
+ import { MemoryStackClient } from "@memorystack/sdk";
4
+ import type { MemorystackConfig } from "../config.ts";
5
+ import { log } from "../logger.ts";
6
+
7
+ export function registerConsolidateTool(
8
+ api: ClawdbotPluginApi,
9
+ cfg: MemorystackConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "memorystack_consolidate",
14
+ label: "Consolidate Memories",
15
+ description:
16
+ "Merge duplicate or highly similar memories to reduce noise and keep memory clean. Useful for maintenance.",
17
+ parameters: Type.Object({
18
+ similarityThreshold: Type.Optional(
19
+ Type.Number({
20
+ description: "Similarity threshold 0-1 (default: 0.85). Higher = stricter matching.",
21
+ }),
22
+ ),
23
+ dryRun: Type.Optional(
24
+ Type.Boolean({
25
+ description: "Preview only, don't merge (default: false)",
26
+ }),
27
+ ),
28
+ maxPairs: Type.Optional(
29
+ Type.Number({
30
+ description: "Max pairs to process (default: 20)",
31
+ }),
32
+ ),
33
+ }),
34
+ async execute(
35
+ _toolCallId: string,
36
+ params: {
37
+ similarityThreshold?: number;
38
+ dryRun?: boolean;
39
+ maxPairs?: number;
40
+ },
41
+ ) {
42
+ const similarityThreshold = params.similarityThreshold ?? 0.85;
43
+ const dryRun = params.dryRun ?? false;
44
+ const maxPairs = params.maxPairs ?? 20;
45
+
46
+ log.debugRequest("consolidate", { similarityThreshold, dryRun, maxPairs });
47
+
48
+ const client = new MemoryStackClient({
49
+ apiKey: cfg.apiKey,
50
+ baseUrl: cfg.baseUrl,
51
+ enableLogging: cfg.debug,
52
+ });
53
+
54
+ try {
55
+ const result = await client.consolidateMemories({
56
+ similarityThreshold,
57
+ dryRun,
58
+ });
59
+
60
+ log.debugResponse("consolidate", {
61
+ merged: result.memoriesMerged ?? 0,
62
+ processed: result.memoriesProcessed ?? 0,
63
+ });
64
+
65
+ // Format output
66
+ const lines: string[] = [];
67
+ lines.push(`# 🧹 Memory Consolidation`);
68
+ lines.push("");
69
+ lines.push(`**Memories Processed:** ${result.memoriesProcessed ?? 0}`);
70
+ lines.push(`**Duplicates Merged:** ${result.memoriesMerged ?? 0}`);
71
+ lines.push(`**Memories Removed:** ${result.memoriesRemoved ?? 0}`);
72
+
73
+ if (result.mergedPairs && result.mergedPairs.length > 0) {
74
+ lines.push("");
75
+ lines.push("## Merged Pairs");
76
+ for (const pair of result.mergedPairs.slice(0, 5)) {
77
+ lines.push(`- Merged: "${pair.original?.substring(0, 50)}..." → "${pair.merged?.substring(0, 50)}..."`);
78
+ }
79
+ }
80
+
81
+ if (dryRun) {
82
+ lines.push("");
83
+ lines.push("*This was a dry run - no changes were made.*");
84
+ } else if (result.memoriesMerged === 0) {
85
+ lines.push("");
86
+ lines.push("✨ No duplicates found - your memory is already clean!");
87
+ } else {
88
+ lines.push("");
89
+ lines.push("āœ… Consolidation complete!");
90
+ }
91
+
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text" as const,
96
+ text: lines.join("\n"),
97
+ },
98
+ ],
99
+ details: result,
100
+ };
101
+ } catch (error: any) {
102
+ log.error("consolidate failed", error);
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text" as const,
107
+ text: `āŒ Consolidation failed: ${error?.message || "Unknown error"}`,
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ },
113
+ },
114
+ { name: "memorystack_consolidate" },
115
+ );
116
+ }
@@ -0,0 +1,68 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
3
+ import { MemoryStackClient } from "@memorystack/sdk";
4
+ import type { MemorystackConfig } from "../config.ts";
5
+ import { log } from "../logger.ts";
6
+
7
+ export function registerDeleteTool(
8
+ api: ClawdbotPluginApi,
9
+ cfg: MemorystackConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "memorystack_delete",
14
+ label: "Delete Memory",
15
+ description:
16
+ "Delete a memory by ID. Use search first to find the memory ID if you only have the content.",
17
+ parameters: Type.Object({
18
+ memoryId: Type.String({ description: "UUID of the memory to delete" }),
19
+ hard: Type.Optional(
20
+ Type.Boolean({
21
+ description: "Permanently delete (true) or soft delete (false, default)",
22
+ }),
23
+ ),
24
+ }),
25
+ async execute(
26
+ _toolCallId: string,
27
+ params: { memoryId: string; hard?: boolean },
28
+ ) {
29
+ log.debugRequest("delete", { memoryId: params.memoryId, hard: params.hard });
30
+
31
+ const client = new MemoryStackClient({
32
+ apiKey: cfg.apiKey,
33
+ baseUrl: cfg.baseUrl,
34
+ enableLogging: cfg.debug,
35
+ });
36
+
37
+ try {
38
+ const result = await client.deleteMemory(params.memoryId, params.hard ?? false);
39
+
40
+ log.debugResponse("delete", { success: result.success });
41
+
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text" as const,
46
+ text: result.success
47
+ ? `āœ… Memory deleted successfully (ID: ${params.memoryId})`
48
+ : `āŒ Failed to delete memory`,
49
+ },
50
+ ],
51
+ details: result,
52
+ };
53
+ } catch (error: any) {
54
+ log.error("delete failed", error);
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text" as const,
59
+ text: `āŒ Failed to delete memory: ${error?.message || "Unknown error"}`,
60
+ },
61
+ ],
62
+ };
63
+ }
64
+ },
65
+ },
66
+ { name: "memorystack_delete" },
67
+ );
68
+ }
@@ -0,0 +1,119 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
3
+ import { MemoryStackClient } from "@memorystack/sdk";
4
+ import type { MemorystackConfig } from "../config.ts";
5
+ import { log } from "../logger.ts";
6
+
7
+ export function registerReflectTool(
8
+ api: ClawdbotPluginApi,
9
+ cfg: MemorystackConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "memorystack_reflect",
14
+ label: "Reflect on Memories",
15
+ description:
16
+ "Analyze memories to discover patterns, generate insights, and identify recurring themes. Great for understanding user behavior over time.",
17
+ parameters: Type.Object({
18
+ timeWindowDays: Type.Optional(
19
+ Type.Number({
20
+ description: "Analyze memories from last N days (default: 7, max: 90)",
21
+ }),
22
+ ),
23
+ analysisDepth: Type.Optional(
24
+ Type.String({
25
+ description: "Analysis depth: 'shallow' (faster) or 'deep' (more thorough)",
26
+ }),
27
+ ),
28
+ dryRun: Type.Optional(
29
+ Type.Boolean({
30
+ description: "Preview only, don't save insights (default: false)",
31
+ }),
32
+ ),
33
+ }),
34
+ async execute(
35
+ _toolCallId: string,
36
+ params: {
37
+ timeWindowDays?: number;
38
+ analysisDepth?: string;
39
+ dryRun?: boolean;
40
+ },
41
+ ) {
42
+ const timeWindowDays = params.timeWindowDays ?? 7;
43
+ const analysisDepth = params.analysisDepth ?? "shallow";
44
+ const dryRun = params.dryRun ?? false;
45
+
46
+ log.debugRequest("reflect", { timeWindowDays, analysisDepth, dryRun });
47
+
48
+ const client = new MemoryStackClient({
49
+ apiKey: cfg.apiKey,
50
+ baseUrl: cfg.baseUrl,
51
+ enableLogging: cfg.debug,
52
+ });
53
+
54
+ try {
55
+ const result = await client.reflectOnMemories({
56
+ timeWindowDays,
57
+ analysisDepth: analysisDepth as "shallow" | "deep",
58
+ dryRun,
59
+ });
60
+
61
+ log.debugResponse("reflect", {
62
+ patterns: result.patterns?.length ?? 0,
63
+ insights: result.insightsGenerated ?? 0,
64
+ });
65
+
66
+ // Format output
67
+ const lines: string[] = [];
68
+ lines.push(`# šŸ”® Memory Reflection`);
69
+ lines.push("");
70
+ lines.push(`**Memories Analyzed:** ${result.memoriesAnalyzed}`);
71
+ lines.push(`**Patterns Found:** ${result.patterns?.length ?? 0}`);
72
+ lines.push(`**Insights Generated:** ${result.insightsGenerated ?? 0}`);
73
+
74
+ if (result.patterns && result.patterns.length > 0) {
75
+ lines.push("");
76
+ lines.push("## Patterns");
77
+ for (const pattern of result.patterns.slice(0, 5)) {
78
+ lines.push(`- **${pattern.type || "Pattern"}:** ${pattern.description || pattern.content}`);
79
+ }
80
+ }
81
+
82
+ if (result.insights && result.insights.length > 0) {
83
+ lines.push("");
84
+ lines.push("## Insights");
85
+ for (const insight of result.insights.slice(0, 5)) {
86
+ lines.push(`- ${insight.content || insight}`);
87
+ }
88
+ }
89
+
90
+ if (dryRun) {
91
+ lines.push("");
92
+ lines.push("*This was a dry run - no insights were saved.*");
93
+ }
94
+
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text" as const,
99
+ text: lines.join("\n"),
100
+ },
101
+ ],
102
+ details: result,
103
+ };
104
+ } catch (error: any) {
105
+ log.error("reflect failed", error);
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text" as const,
110
+ text: `āŒ Reflection failed: ${error?.message || "Unknown error"}`,
111
+ },
112
+ ],
113
+ };
114
+ }
115
+ },
116
+ },
117
+ { name: "memorystack_reflect" },
118
+ );
119
+ }
package/tools/search.ts CHANGED
@@ -7,18 +7,43 @@ import { log } from "../logger.ts";
7
7
  export function registerSearchTool(
8
8
  api: ClawdbotPluginApi,
9
9
  cfg: MemorystackConfig,
10
+ getSessionKey?: () => string | undefined,
11
+ getContext?: () => Record<string, any>,
10
12
  ): void {
11
13
  api.registerTool(
12
14
  {
13
15
  name: "memorystack_search",
14
16
  label: "Memory Search",
15
- description:
16
- "Search through long-term memories for relevant information using semantic search. Supports filtering by memory type, confidence, and recency.",
17
+ description: `Search through long-term memories using semantic search.
18
+
19
+ **Automatic Context**: Your agent_id, session_id, team_id, and conversation_id are available for filtering.
20
+
21
+ **Scoping Options**:
22
+ - scope="global": Search all memories (default)
23
+ - scope="agent": Search only memories from THIS agent
24
+ - scope="team": Search only memories from this team/group
25
+
26
+ **Explicit Filters** (override scope):
27
+ - agent_id: Search memories from a specific agent/subagent by UUID
28
+ - session_id: Search memories from a specific session
29
+ - metadata: Filter by metadata key-value pairs (e.g., parent_agent_id, source)
30
+
31
+ **Filtering**: Supports memory_type, min_confidence, days_ago, and current_session filters.`,
17
32
  parameters: Type.Object({
18
33
  query: Type.String({ description: "Search query" }),
19
34
  limit: Type.Optional(
20
35
  Type.Number({ description: "Max results (default: 5)" }),
21
36
  ),
37
+ agent_id: Type.Optional(
38
+ Type.String({
39
+ description: "Filter by specific agent/subagent UUID (e.g., from a spawned subagent)",
40
+ }),
41
+ ),
42
+ session_id: Type.Optional(
43
+ Type.String({
44
+ description: "Filter by specific session key",
45
+ }),
46
+ ),
22
47
  memory_type: Type.Optional(
23
48
  Type.String({
24
49
  description:
@@ -35,19 +60,47 @@ export function registerSearchTool(
35
60
  description: "Only memories from last N days",
36
61
  }),
37
62
  ),
63
+ current_session: Type.Optional(
64
+ Type.Boolean({
65
+ description: "Filter to current session only (default: false)",
66
+ }),
67
+ ),
68
+ scope: Type.Optional(
69
+ Type.String({
70
+ description: "Scope of search: 'global', 'agent', 'team' (default: 'global')",
71
+ enum: ["global", "agent", "team"],
72
+ }),
73
+ ),
74
+ metadata: Type.Optional(
75
+ Type.Record(Type.String(), Type.Any(), {
76
+ description: "Filter by metadata fields (e.g., { parent_agent_id: 'main' })",
77
+ }),
78
+ ),
38
79
  }),
39
80
  async execute(
40
81
  _toolCallId: string,
41
82
  params: {
42
83
  query: string;
43
84
  limit?: number;
85
+ agent_id?: string;
86
+ session_id?: string;
44
87
  memory_type?: string;
45
88
  min_confidence?: number;
46
89
  days_ago?: number;
90
+ current_session?: boolean;
91
+ scope?: "global" | "agent" | "team";
92
+ metadata?: Record<string, any>;
47
93
  },
48
94
  ) {
49
95
  const limit = params.limit ?? 5;
50
- log.debugRequest("search", { query: params.query, limit, ...params });
96
+ const sessionKey = getSessionKey?.();
97
+ const ctx = getContext?.() || {};
98
+
99
+ log.debugRequest("search", {
100
+ query: params.query,
101
+ scope: params.scope,
102
+ agentId: ctx.agentId
103
+ });
51
104
 
52
105
  const client = new MemoryStackClient({
53
106
  apiKey: cfg.apiKey,
@@ -55,51 +108,102 @@ export function registerSearchTool(
55
108
  enableLogging: cfg.debug,
56
109
  });
57
110
 
58
- const searchOpts: Record<string, unknown> = { limit };
59
- if (params.memory_type) {
60
- searchOpts.memory_type = params.memory_type;
111
+ // Build search options
112
+ const searchOpts: any = {
113
+ limit,
114
+ userId: ctx.userId, // Default to current user
115
+ min_confidence: params.min_confidence,
116
+ memory_type: params.memory_type,
117
+ };
118
+
119
+ // For subagents, use subagentId as the effective agent
120
+ const effectiveAgentId = ctx.subagentId || ctx.agentId;
121
+
122
+ // Priority: explicit params > scope > default
123
+ // 1. Explicit agent_id/session_id params override everything
124
+ if (params.agent_id) {
125
+ searchOpts.agentId = params.agent_id;
126
+ } else if (params.scope === "agent" && effectiveAgentId) {
127
+ // 2. Scope-based filtering
128
+ searchOpts.agentId = effectiveAgentId;
129
+ } else if (params.scope === "team" && ctx.teamId) {
130
+ searchOpts.teamId = ctx.teamId;
131
+ }
132
+ // 3. global scope = no agent filter (default)
133
+
134
+ // Session filtering: explicit param > current_session flag
135
+ if (params.session_id) {
136
+ searchOpts.sessionId = params.session_id;
137
+ } else if (params.current_session && sessionKey) {
138
+ searchOpts.sessionId = sessionKey;
139
+ }
140
+
141
+ // Metadata filtering (passed directly to SDK)
142
+ if (params.metadata && Object.keys(params.metadata).length > 0) {
143
+ searchOpts.metadata = params.metadata;
144
+ }
145
+
146
+ // Apply Time Filter (handled via generic params if needed, or client-side filtering)
147
+ // improved SDK might support start_date, but for now we can rely on natural language query
148
+ // or pass it if backend supports it. The backend supports start_date.
149
+ if (params.days_ago) {
150
+ const date = new Date();
151
+ date.setDate(date.getDate() - params.days_ago);
152
+ searchOpts.start_date = date.toISOString();
61
153
  }
62
154
 
63
- const results = await client.search(params.query, searchOpts);
155
+ try {
156
+ const results = await client.search(params.query, searchOpts);
157
+
158
+ if (results.count === 0) {
159
+ return {
160
+ content: [
161
+ { type: "text" as const, text: "No relevant memories found." },
162
+ ],
163
+ };
164
+ }
165
+
166
+ const text = results.results
167
+ .map((r, i) => {
168
+ const type = r.memory_type ? ` [${r.memory_type}]` : "";
169
+ const conf = r.confidence
170
+ ? ` (${(r.confidence * 100).toFixed(0)}%)`
171
+ : "";
172
+ // Full ISO timestamp for temporal reasoning (e.g., 2024-06-03T14:30:00Z)
173
+ const timestamp = r.created_at ? ` [${r.created_at}]` : "";
174
+ return `${i + 1}. ${r.content}${type}${conf}${timestamp}`;
175
+ })
176
+ .join("\n");
177
+
178
+ log.debugResponse("search", { count: results.count });
64
179
 
65
- if (results.count === 0) {
66
180
  return {
67
181
  content: [
68
- { type: "text" as const, text: "No relevant memories found." },
182
+ {
183
+ type: "text" as const,
184
+ text: `Found ${results.count} memories:\n\n${text}`,
185
+ },
69
186
  ],
187
+ details: {
188
+ count: results.count,
189
+ mode: results.mode,
190
+ memories: results.results.map((r) => ({
191
+ id: r.id,
192
+ content: r.content,
193
+ memory_type: r.memory_type,
194
+ confidence: r.confidence,
195
+ created_at: r.created_at,
196
+ metadata: r.metadata,
197
+ })),
198
+ },
199
+ };
200
+ } catch (error: any) {
201
+ log.error("search failed", error);
202
+ return {
203
+ content: [{ type: "text", text: `Search failed: ${error.message}` }],
204
+ isError: true,
70
205
  };
71
206
  }
72
-
73
- const text = results.results
74
- .map((r, i) => {
75
- const type = r.memory_type ? ` [${r.memory_type}]` : "";
76
- const conf = r.confidence
77
- ? ` (${(r.confidence * 100).toFixed(0)}%)`
78
- : "";
79
- return `${i + 1}. ${r.content}${type}${conf}`;
80
- })
81
- .join("\n");
82
-
83
- log.debugResponse("search", { count: results.count });
84
-
85
- return {
86
- content: [
87
- {
88
- type: "text" as const,
89
- text: `Found ${results.count} memories:\n\n${text}`,
90
- },
91
- ],
92
- details: {
93
- count: results.count,
94
- mode: results.mode,
95
- memories: results.results.map((r) => ({
96
- id: r.id,
97
- content: r.content,
98
- memory_type: r.memory_type,
99
- confidence: r.confidence,
100
- })),
101
- },
102
- };
103
207
  },
104
208
  },
105
209
  { name: "memorystack_search" },
@@ -0,0 +1,52 @@
1
+ import { MemoryStackClient } from "@memorystack/sdk";
2
+
3
+ const API_KEY = "mem_live_KT9roc_L4O60KJ6_l7SGdzXeTib1RHoF3QlmhLghMbQ";
4
+
5
+ async function verifyRecentMemories() {
6
+ const client = new MemoryStackClient({
7
+ apiKey: API_KEY,
8
+ baseUrl: "http://localhost:3000",
9
+ });
10
+
11
+ console.log("šŸ” Inspecting Most Recent Memories\n");
12
+ console.log("=".repeat(60) + "\n");
13
+
14
+ try {
15
+ // Fetch 5 most recent memories
16
+ const results = await client.search("memorystack", {
17
+ limit: 5,
18
+ });
19
+
20
+ console.log(`Found ${results.count} recent memories.\n`);
21
+
22
+ results.results.forEach((m, i) => {
23
+ console.log(`Memory #${i + 1}`);
24
+ console.log(` ID: ${m.id}`);
25
+ console.log(` Agent ID: "${m.agent_id}"`);
26
+ console.log(` Session ID: "${m.session_id}"`);
27
+ console.log(` Created At: ${m.created_at}`);
28
+ console.log(` Content: ${m.content.substring(0, 100)}...`);
29
+
30
+ // Check metadata for subagent details
31
+ if (m.metadata) {
32
+ console.log(` Metadata:`);
33
+ if (m.metadata.subagent_id) console.log(` - subagent_id: ${m.metadata.subagent_id}`);
34
+ if (m.metadata.parent_agent_id) console.log(` - parent_agent_id: ${m.metadata.parent_agent_id}`);
35
+ }
36
+
37
+ // Verification logic
38
+ if (m.agent_id !== "main" && m.agent_id !== "agent") {
39
+ console.log(` āœ… SUCCESS: Uses specific Agent ID!`);
40
+ } else if (m.metadata?.subagent_id) {
41
+ console.log(` āš ļø Stored as 'main', but has subagent_id in metadata.`);
42
+ }
43
+
44
+ console.log("-".repeat(40));
45
+ });
46
+
47
+ } catch (e: any) {
48
+ console.log(`āŒ Error: ${e.message}`);
49
+ }
50
+ }
51
+
52
+ verifyRecentMemories().catch(console.error);