pi-automem-bridge 0.2.0 → 0.2.2

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.
package/src/mcp-client.ts CHANGED
@@ -1,361 +1,401 @@
1
- /**
2
- * mcp-client.ts - JSON-RPC client for AutoMem MCP sidecar.
3
- *
4
- * Reads connection info from pi's mcp.json (url + auth header).
5
- * All calls go through the MCP tools/call endpoint.
6
- */
7
-
8
- import { readFileSync, existsSync } from "node:fs";
9
- import { homedir } from "node:os";
10
- import { resolve } from "node:path";
11
- import { resolveEnvVars } from "./config";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Types
15
- // ---------------------------------------------------------------------------
16
-
17
- export interface McpCallResult {
18
- content: Array<{ type: string; text?: string }>;
19
- isError?: boolean;
20
- }
21
-
22
- export interface McpHealth {
23
- healthy: boolean;
24
- memoryCount?: number;
25
- error?: string;
26
- }
27
-
28
- // ---------------------------------------------------------------------------
29
- // MCP config reader
30
- // ---------------------------------------------------------------------------
31
-
32
- function loadMcpServerConfig(serverName: string): { url: string; auth: string } {
33
- const mcpJsonPath = resolve(homedir(), ".pi", "agent", "mcp.json");
34
-
35
- if (!existsSync(mcpJsonPath)) {
36
- throw new Error("mcp.json not found at " + mcpJsonPath);
37
- }
38
-
39
- const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8")) as {
40
- mcpServers?: Record<string, { url: string; headers?: Record<string, string> }>;
41
- };
42
-
43
- const server = mcpJson.mcpServers ? mcpJson.mcpServers[serverName] : undefined;
44
- if (!server) {
45
- const available = mcpJson.mcpServers ? Object.keys(mcpJson.mcpServers).join(", ") : "(none)";
46
- throw new Error('MCP server "' + serverName + '" not found. Available: ' + available);
47
- }
48
-
49
- return {
50
- url: server.url,
51
- auth: resolveEnvVars(server.headers?.Authorization || ""),
52
- };
53
- }
54
-
55
- // ---------------------------------------------------------------------------
56
- // Response parsing handles both JSON and text/event-stream (SSE)
57
- // ---------------------------------------------------------------------------
58
-
59
- async function parseJsonRpcResponse(resp: Response): Promise<any> {
60
- const ct = resp.headers.get("content-type") || "";
61
- if (ct.includes("text/event-stream")) {
62
- const text = await resp.text();
63
- // SSE lines are "data: <json>\n"; find the last non-empty data line
64
- const dataLine = text
65
- .split("\n")
66
- .map(function(l: string) { return l.trim(); })
67
- .filter(function(l: string) { return l.startsWith("data:") && l.length > 5; })
68
- .pop();
69
- if (!dataLine) throw new Error("SSE response contained no data lines");
70
- return JSON.parse(dataLine.slice(5).trim());
71
- }
72
- return resp.json();
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // JSON-RPC client
77
- // ---------------------------------------------------------------------------
78
-
79
- let callId = 0;
80
- let configuredServerName = process.env.AUTOMEM_MCP_SERVER || "automem";
81
-
82
- export function setAutoMemMcpServerName(serverName: string | undefined): void {
83
- if (serverName && serverName.trim()) {
84
- const newName = serverName.trim();
85
- if (newName !== configuredServerName) {
86
- discoveredTools = null;
87
- configuredServerName = newName;
88
- }
89
- }
90
- }
91
-
92
- function getAutoMemMcpServerName(): string {
93
- return process.env.AUTOMEM_MCP_SERVER || configuredServerName || "automem";
94
- }
95
-
96
- async function mcpCall(tool: string, args: Record<string, unknown>): Promise<McpCallResult> {
97
- const serverName = getAutoMemMcpServerName();
98
- const cfg = loadMcpServerConfig(serverName);
99
-
100
- const body = {
101
- jsonrpc: "2.0",
102
- id: ++callId,
103
- method: "tools/call",
104
- params: { name: tool, arguments: args },
105
- };
106
-
107
- const controller = new AbortController();
108
- const timeout = setTimeout(() => controller.abort(), 30000);
109
-
110
- try {
111
- const resp = await fetch(cfg.url, {
112
- method: "POST",
113
- headers: {
114
- "Content-Type": "application/json",
115
- ...(cfg.auth ? { Authorization: cfg.auth } : {}),
116
- Accept: "application/json, text/event-stream",
117
- },
118
- body: JSON.stringify(body),
119
- signal: controller.signal,
120
- });
121
-
122
- if (!resp.ok) {
123
- const text = await resp.text().catch(function() { return ""; });
124
- throw new Error("MCP HTTP " + resp.status + ": " + text.slice(0, 200));
125
- }
126
-
127
- const payload = (await parseJsonRpcResponse(resp)) as {
128
- result?: McpCallResult;
129
- error?: { code: number; message: string };
130
- };
131
-
132
- if (payload.error) {
133
- throw new Error("MCP error: " + payload.error.message);
134
- }
135
-
136
- return payload.result || { content: [] };
137
- } finally {
138
- clearTimeout(timeout);
139
- }
140
- }
141
-
142
- // ---------------------------------------------------------------------------
143
- // Tool discovery cache
144
- // ---------------------------------------------------------------------------
145
-
146
- let discoveredTools: Map<string, string> | null = null;
147
-
148
- /**
149
- * Discover available tools from the MCP server via tools/list.
150
- * Returns a Map of normalized tool name → actual tool name.
151
- * Cached after first call.
152
- */
153
- export async function discoverTools(): Promise<Map<string, string>> {
154
- if (discoveredTools) return discoveredTools;
155
-
156
- const serverName = getAutoMemMcpServerName();
157
- const cfg = loadMcpServerConfig(serverName);
158
-
159
- const body = {
160
- jsonrpc: "2.0",
161
- id: ++callId,
162
- method: "tools/list",
163
- params: {},
164
- };
165
-
166
- const controller = new AbortController();
167
- const timeout = setTimeout(() => controller.abort(), 15000);
168
-
169
- try {
170
- const resp = await fetch(cfg.url, {
171
- method: "POST",
172
- headers: {
173
- "Content-Type": "application/json",
174
- ...(cfg.auth ? { Authorization: cfg.auth } : {}),
175
- Accept: "application/json, text/event-stream",
176
- },
177
- body: JSON.stringify(body),
178
- signal: controller.signal,
179
- });
180
-
181
- if (!resp.ok) {
182
- throw new Error("MCP tools/list HTTP " + resp.status);
183
- }
184
-
185
- const payload = (await parseJsonRpcResponse(resp)) as {
186
- result?: { tools?: Array<{ name: string }> };
187
- error?: { code: number; message: string };
188
- };
189
-
190
- if (payload.error) {
191
- throw new Error("MCP tools/list error: " + payload.error.message);
192
- }
193
-
194
- const tools = payload.result?.tools || [];
195
- const map = new Map<string, string>();
196
- for (const t of tools) {
197
- map.set(t.name.toLowerCase(), t.name);
198
- // Also index without automem_ prefix for fuzzy matching
199
- if (t.name.toLowerCase().startsWith("automem_")) {
200
- map.set(t.name.toLowerCase().replace("automem_", ""), t.name);
201
- }
202
- // Also index with automem_ prefix for reverse lookups
203
- map.set("automem_" + t.name.toLowerCase(), t.name);
204
- }
205
-
206
- discoveredTools = map;
207
- console.log("[automem] discovered tools: " + Array.from(new Set(map.values())).join(", "));
208
- return map;
209
- } catch (err) {
210
- console.warn("[automem] tools/list failed, using default tool names: " + err);
211
- // Fallback: use actual server tool names (no automem_ prefix)
212
- discoveredTools = new Map<string, string>([
213
- ["recall_memory", "recall_memory"],
214
- ["automem_recall_memory", "recall_memory"],
215
- ["check_database_health", "check_database_health"],
216
- ["automem_check_database_health", "check_database_health"],
217
- ["store_memory", "store_memory"],
218
- ["automem_store_memory", "store_memory"],
219
- ["associate_memories", "associate_memories"],
220
- ["automem_associate_memories", "associate_memories"],
221
- ["update_memory", "update_memory"],
222
- ["automem_update_memory", "update_memory"],
223
- ["delete_memory", "delete_memory"],
224
- ["automem_delete_memory", "delete_memory"],
225
- ]);
226
- return discoveredTools;
227
- } finally {
228
- clearTimeout(timeout);
229
- }
230
- }
231
-
232
- /**
233
- * Resolve a logical tool name to the actual server tool name.
234
- * e.g. "recall_memory" the actual server tool name discovered from tools/list.
235
- */
236
- export function resolveToolName(logicalName: string): string {
237
- if (!discoveredTools) return logicalName;
238
- const key = logicalName.toLowerCase();
239
- return discoveredTools.get(key) || logicalName;
240
- }
241
-
242
- // ---------------------------------------------------------------------------
243
- // AutoMem-specific wrappers
244
- // ---------------------------------------------------------------------------
245
-
246
- export async function automemRecall(
247
- query: string,
248
- options?: {
249
- limit?: number;
250
- tags?: string[];
251
- tagMode?: "any" | "all";
252
- contextTypes?: string[];
253
- expandRelations?: boolean;
254
- expandEntities?: boolean;
255
- },
256
- ): Promise<McpCallResult> {
257
- const args: Record<string, unknown> = {
258
- query,
259
- limit: options && options.limit ? options.limit : 8,
260
- tags: options && options.tags ? options.tags : [],
261
- tag_mode: options && options.tagMode ? options.tagMode : "any",
262
- expand_relations: options ? !!options.expandRelations : false,
263
- expand_entities: options ? !!options.expandEntities : false,
264
- };
265
-
266
- if (options && options.contextTypes && options.contextTypes.length > 0) {
267
- args.context_types = options.contextTypes;
268
- }
269
-
270
- return mcpCall(resolveToolName("recall_memory"), args);
271
- }
272
-
273
- export async function automemHealth(): Promise<McpHealth> {
274
- try {
275
- const result = await mcpCall(resolveToolName("check_database_health"), {});
276
- const text = result.content && result.content[0] ? result.content[0].text : undefined;
277
- if (text) {
278
- try {
279
- const parsed = JSON.parse(text);
280
- const count = parsed.memory_count !== undefined
281
- ? parsed.memory_count
282
- : (parsed.count !== undefined ? parsed.count : parsed.memories);
283
- return {
284
- healthy: true,
285
- memoryCount: typeof count === "number" ? count : undefined,
286
- };
287
- } catch (_e) {
288
- return { healthy: true };
289
- }
290
- }
291
- return { healthy: true };
292
- } catch (err) {
293
- return { healthy: false, error: String(err) };
294
- }
295
- }
296
-
297
- export async function automemStore(
298
- content: string,
299
- type: string,
300
- tags: string[],
301
- options?: {
302
- source?: string;
303
- confidence?: number;
304
- importance?: number;
305
- metadata?: Record<string, unknown>;
306
- },
307
- ): Promise<McpCallResult> {
308
- const meta: Record<string, unknown> = {};
309
- if (options && options.source) meta.source = options.source;
310
- if (options && options.metadata) Object.assign(meta, options.metadata);
311
-
312
- return mcpCall(resolveToolName("store_memory"), {
313
- content,
314
- type,
315
- tags,
316
- confidence: options && options.confidence ? options.confidence : 0.8,
317
- importance: options && options.importance ? options.importance : 0.5,
318
- metadata: Object.keys(meta).length > 0 ? meta : undefined,
319
- });
320
- }
321
-
322
- export async function automemAssociate(
323
- memory1Id: string,
324
- memory2Id: string,
325
- relationship: string,
326
- strength: number = 0.5,
327
- ): Promise<McpCallResult> {
328
- return mcpCall(resolveToolName("associate_memories"), {
329
- memory1_id: memory1Id,
330
- memory2_id: memory2Id,
331
- type: relationship,
332
- strength,
333
- });
334
- }
335
-
336
- export async function automemUpdate(
337
- memoryId: string,
338
- updates: {
339
- content?: string;
340
- type?: string;
341
- tags?: string[];
342
- importance?: number;
343
- confidence?: number;
344
- metadata?: Record<string, unknown>;
345
- },
346
- ): Promise<McpCallResult> {
347
- const args: Record<string, unknown> = { memory_id: memoryId };
348
- if (updates.content !== undefined) args.content = updates.content;
349
- if (updates.type !== undefined) args.type = updates.type;
350
- if (updates.tags !== undefined) args.tags = updates.tags;
351
- if (updates.importance !== undefined) args.importance = updates.importance;
352
- if (updates.confidence !== undefined) args.confidence = updates.confidence;
353
- if (updates.metadata !== undefined) args.metadata = updates.metadata;
354
- return mcpCall(resolveToolName("update_memory"), args);
355
- }
356
-
357
- export async function automemDelete(memoryId: string): Promise<McpCallResult> {
358
- return mcpCall(resolveToolName("delete_memory"), {
359
- memory_id: memoryId,
360
- });
361
- }
1
+ /**
2
+ * mcp-client.ts - JSON-RPC client for AutoMem MCP sidecar.
3
+ *
4
+ * Reads connection info from pi's mcp.json (url + auth header).
5
+ * All calls go through the MCP tools/call endpoint.
6
+ */
7
+
8
+ import { readFileSync, existsSync, statSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { resolve } from "node:path";
11
+ import { resolveEnvVars } from "./config";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface McpCallResult {
18
+ content: Array<{ type: string; text?: string }>;
19
+ isError?: boolean;
20
+ }
21
+
22
+ export interface McpHealth {
23
+ healthy: boolean;
24
+ memoryCount?: number;
25
+ error?: string;
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // MCP config reader
30
+ // ---------------------------------------------------------------------------
31
+
32
+ // Cache the parsed server config per server name to keep readFileSync + JSON
33
+ // parse off the per-turn recall hot path. The cache is validated against a
34
+ // cheap stat signature (mtime + size — the same quick-check make/rsync use), so
35
+ // an in-place mcp.json edit is still picked up even within a single mtime tick.
36
+ // An empty signature (stat failed) never matches, forcing a fresh read.
37
+ interface CachedServerConfig { url: string; auth: string; signature: string }
38
+ let mcpConfigCache: Map<string, CachedServerConfig> = new Map();
39
+
40
+ function loadMcpServerConfig(serverName: string): CachedServerConfig {
41
+ const mcpJsonPath = resolve(homedir(), ".pi", "agent", "mcp.json");
42
+
43
+ if (!existsSync(mcpJsonPath)) {
44
+ throw new Error("mcp.json not found at " + mcpJsonPath);
45
+ }
46
+
47
+ let signature = "";
48
+ try {
49
+ const st = statSync(mcpJsonPath);
50
+ signature = st.mtimeMs + ":" + st.size;
51
+ } catch (_e) { /* leave signature empty so the cache is bypassed */ }
52
+
53
+ const cached = mcpConfigCache.get(serverName);
54
+ if (cached && signature !== "" && cached.signature === signature) return cached;
55
+
56
+ // The file changed on disk (or the stat failed). The endpoint may now expose
57
+ // different tools, so drop the discovery cache too.
58
+ if (cached) discoveredTools = null;
59
+
60
+ const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8")) as {
61
+ mcpServers?: Record<string, { url: string; headers?: Record<string, string> }>;
62
+ };
63
+
64
+ const server = mcpJson.mcpServers ? mcpJson.mcpServers[serverName] : undefined;
65
+ if (!server) {
66
+ const available = mcpJson.mcpServers ? Object.keys(mcpJson.mcpServers).join(", ") : "(none)";
67
+ throw new Error('MCP server "' + serverName + '" not found. Available: ' + available);
68
+ }
69
+
70
+ const entry: CachedServerConfig = {
71
+ url: server.url,
72
+ auth: resolveEnvVars(server.headers?.Authorization || ""),
73
+ signature,
74
+ };
75
+ mcpConfigCache.set(serverName, entry);
76
+ return entry;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Response parsing handles both JSON and text/event-stream (SSE)
81
+ // ---------------------------------------------------------------------------
82
+
83
+ async function parseJsonRpcResponse(resp: Response): Promise<any> {
84
+ const ct = resp.headers.get("content-type") || "";
85
+ if (ct.includes("text/event-stream")) {
86
+ const text = await resp.text();
87
+ // SSE lines are "data: <json>\n"; find the last non-empty data line
88
+ const dataLine = text
89
+ .split("\n")
90
+ .map(function(l: string) { return l.trim(); })
91
+ .filter(function(l: string) { return l.startsWith("data:") && l.length > 5; })
92
+ .pop();
93
+ if (!dataLine) throw new Error("SSE response contained no data lines");
94
+ return JSON.parse(dataLine.slice(5).trim());
95
+ }
96
+ return resp.json();
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // JSON-RPC client
101
+ // ---------------------------------------------------------------------------
102
+
103
+ let callId = 0;
104
+ let configuredServerName = process.env.AUTOMEM_MCP_SERVER || "automem";
105
+
106
+ export function setAutoMemMcpServerName(serverName: string | undefined): void {
107
+ if (serverName && serverName.trim()) {
108
+ const newName = serverName.trim();
109
+ if (newName !== configuredServerName) {
110
+ discoveredTools = null;
111
+ mcpConfigCache = new Map();
112
+ configuredServerName = newName;
113
+ }
114
+ }
115
+ }
116
+
117
+ function getAutoMemMcpServerName(): string {
118
+ return process.env.AUTOMEM_MCP_SERVER || configuredServerName || "automem";
119
+ }
120
+
121
+ async function mcpCall(
122
+ tool: string,
123
+ args: Record<string, unknown>,
124
+ timeoutMs: number = 30000,
125
+ ): Promise<McpCallResult> {
126
+ const serverName = getAutoMemMcpServerName();
127
+ const cfg = loadMcpServerConfig(serverName);
128
+
129
+ const body = {
130
+ jsonrpc: "2.0",
131
+ id: ++callId,
132
+ method: "tools/call",
133
+ params: { name: tool, arguments: args },
134
+ };
135
+
136
+ const controller = new AbortController();
137
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
138
+
139
+ try {
140
+ const resp = await fetch(cfg.url, {
141
+ method: "POST",
142
+ headers: {
143
+ "Content-Type": "application/json",
144
+ ...(cfg.auth ? { Authorization: cfg.auth } : {}),
145
+ Accept: "application/json, text/event-stream",
146
+ },
147
+ body: JSON.stringify(body),
148
+ signal: controller.signal,
149
+ });
150
+
151
+ if (!resp.ok) {
152
+ const text = await resp.text().catch(function() { return ""; });
153
+ throw new Error("MCP HTTP " + resp.status + ": " + text.slice(0, 200));
154
+ }
155
+
156
+ const payload = (await parseJsonRpcResponse(resp)) as {
157
+ result?: McpCallResult;
158
+ error?: { code: number; message: string };
159
+ };
160
+
161
+ if (payload.error) {
162
+ throw new Error("MCP error: " + payload.error.message);
163
+ }
164
+
165
+ const result = payload.result || { content: [] };
166
+ // A tool-level failure is reported as HTTP 200 with isError:true (no
167
+ // JSON-RPC error). Surface it as a thrown error so callers don't treat a
168
+ // failed health check / store / update as success.
169
+ if (result.isError) {
170
+ const errText = result.content && result.content[0] ? result.content[0].text : undefined;
171
+ throw new Error("MCP tool error: " + (errText || "tool reported isError"));
172
+ }
173
+ return result;
174
+ } finally {
175
+ clearTimeout(timeout);
176
+ }
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Tool discovery cache
181
+ // ---------------------------------------------------------------------------
182
+
183
+ let discoveredTools: Map<string, string> | null = null;
184
+
185
+ /**
186
+ * Discover available tools from the MCP server via tools/list.
187
+ * Returns a Map of normalized tool name → actual tool name.
188
+ * Cached after first call.
189
+ */
190
+ export async function discoverTools(): Promise<Map<string, string>> {
191
+ // Load config first if mcp.json changed on disk, loadMcpServerConfig drops
192
+ // the discovery cache, so this must run before the early return below.
193
+ const serverName = getAutoMemMcpServerName();
194
+ const cfg = loadMcpServerConfig(serverName);
195
+
196
+ if (discoveredTools) return discoveredTools;
197
+
198
+ const body = {
199
+ jsonrpc: "2.0",
200
+ id: ++callId,
201
+ method: "tools/list",
202
+ params: {},
203
+ };
204
+
205
+ const controller = new AbortController();
206
+ const timeout = setTimeout(() => controller.abort(), 15000);
207
+
208
+ try {
209
+ const resp = await fetch(cfg.url, {
210
+ method: "POST",
211
+ headers: {
212
+ "Content-Type": "application/json",
213
+ ...(cfg.auth ? { Authorization: cfg.auth } : {}),
214
+ Accept: "application/json, text/event-stream",
215
+ },
216
+ body: JSON.stringify(body),
217
+ signal: controller.signal,
218
+ });
219
+
220
+ if (!resp.ok) {
221
+ throw new Error("MCP tools/list HTTP " + resp.status);
222
+ }
223
+
224
+ const payload = (await parseJsonRpcResponse(resp)) as {
225
+ result?: { tools?: Array<{ name: string }> };
226
+ error?: { code: number; message: string };
227
+ };
228
+
229
+ if (payload.error) {
230
+ throw new Error("MCP tools/list error: " + payload.error.message);
231
+ }
232
+
233
+ const tools = payload.result?.tools || [];
234
+ const map = new Map<string, string>();
235
+ for (const t of tools) {
236
+ map.set(t.name.toLowerCase(), t.name);
237
+ // Also index without automem_ prefix for fuzzy matching
238
+ if (t.name.toLowerCase().startsWith("automem_")) {
239
+ map.set(t.name.toLowerCase().replace("automem_", ""), t.name);
240
+ }
241
+ // Also index with automem_ prefix for reverse lookups
242
+ map.set("automem_" + t.name.toLowerCase(), t.name);
243
+ }
244
+
245
+ discoveredTools = map;
246
+ console.log("[automem] discovered tools: " + Array.from(new Set(map.values())).join(", "));
247
+ return map;
248
+ } catch (err) {
249
+ console.warn("[automem] tools/list failed, using default tool names: " + err);
250
+ // Fallback: use actual server tool names (no automem_ prefix)
251
+ discoveredTools = new Map<string, string>([
252
+ ["recall_memory", "recall_memory"],
253
+ ["automem_recall_memory", "recall_memory"],
254
+ ["check_database_health", "check_database_health"],
255
+ ["automem_check_database_health", "check_database_health"],
256
+ ["store_memory", "store_memory"],
257
+ ["automem_store_memory", "store_memory"],
258
+ ["associate_memories", "associate_memories"],
259
+ ["automem_associate_memories", "associate_memories"],
260
+ ["update_memory", "update_memory"],
261
+ ["automem_update_memory", "update_memory"],
262
+ ["delete_memory", "delete_memory"],
263
+ ["automem_delete_memory", "delete_memory"],
264
+ ]);
265
+ return discoveredTools;
266
+ } finally {
267
+ clearTimeout(timeout);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Resolve a logical tool name to the actual server tool name.
273
+ * e.g. "recall_memory" the actual server tool name discovered from tools/list.
274
+ */
275
+ export function resolveToolName(logicalName: string): string {
276
+ if (!discoveredTools) return logicalName;
277
+ const key = logicalName.toLowerCase();
278
+ return discoveredTools.get(key) || logicalName;
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // AutoMem-specific wrappers
283
+ // ---------------------------------------------------------------------------
284
+
285
+ export async function automemRecall(
286
+ query: string,
287
+ options?: {
288
+ limit?: number;
289
+ tags?: string[];
290
+ tagMode?: "any" | "all";
291
+ contextTypes?: string[];
292
+ expandRelations?: boolean;
293
+ expandEntities?: boolean;
294
+ },
295
+ timeoutMs?: number,
296
+ ): Promise<McpCallResult> {
297
+ const args: Record<string, unknown> = {
298
+ query,
299
+ limit: options && options.limit ? options.limit : 8,
300
+ tags: options && options.tags ? options.tags : [],
301
+ tag_mode: options && options.tagMode ? options.tagMode : "any",
302
+ expand_relations: options ? !!options.expandRelations : false,
303
+ expand_entities: options ? !!options.expandEntities : false,
304
+ };
305
+
306
+ if (options && options.contextTypes && options.contextTypes.length > 0) {
307
+ args.context_types = options.contextTypes;
308
+ }
309
+
310
+ return mcpCall(resolveToolName("recall_memory"), args, timeoutMs);
311
+ }
312
+
313
+ export async function automemHealth(): Promise<McpHealth> {
314
+ try {
315
+ const result = await mcpCall(resolveToolName("check_database_health"), {});
316
+ const text = result.content && result.content[0] ? result.content[0].text : undefined;
317
+ if (text) {
318
+ try {
319
+ const parsed = JSON.parse(text);
320
+ const count = parsed.memory_count !== undefined
321
+ ? parsed.memory_count
322
+ : (parsed.count !== undefined ? parsed.count : parsed.memories);
323
+ return {
324
+ healthy: true,
325
+ memoryCount: typeof count === "number" ? count : undefined,
326
+ };
327
+ } catch (_e) {
328
+ return { healthy: true };
329
+ }
330
+ }
331
+ return { healthy: true };
332
+ } catch (err) {
333
+ return { healthy: false, error: String(err) };
334
+ }
335
+ }
336
+
337
+ export async function automemStore(
338
+ content: string,
339
+ type: string,
340
+ tags: string[],
341
+ options?: {
342
+ source?: string;
343
+ confidence?: number;
344
+ importance?: number;
345
+ metadata?: Record<string, unknown>;
346
+ },
347
+ ): Promise<McpCallResult> {
348
+ const meta: Record<string, unknown> = {};
349
+ if (options && options.source) meta.source = options.source;
350
+ if (options && options.metadata) Object.assign(meta, options.metadata);
351
+
352
+ return mcpCall(resolveToolName("store_memory"), {
353
+ content,
354
+ type,
355
+ tags,
356
+ confidence: options?.confidence ?? 0.8,
357
+ importance: options?.importance ?? 0.5,
358
+ metadata: Object.keys(meta).length > 0 ? meta : undefined,
359
+ });
360
+ }
361
+
362
+ export async function automemAssociate(
363
+ memory1Id: string,
364
+ memory2Id: string,
365
+ relationship: string,
366
+ strength: number = 0.5,
367
+ ): Promise<McpCallResult> {
368
+ return mcpCall(resolveToolName("associate_memories"), {
369
+ memory1_id: memory1Id,
370
+ memory2_id: memory2Id,
371
+ type: relationship,
372
+ strength,
373
+ });
374
+ }
375
+
376
+ export async function automemUpdate(
377
+ memoryId: string,
378
+ updates: {
379
+ content?: string;
380
+ type?: string;
381
+ tags?: string[];
382
+ importance?: number;
383
+ confidence?: number;
384
+ metadata?: Record<string, unknown>;
385
+ },
386
+ ): Promise<McpCallResult> {
387
+ const args: Record<string, unknown> = { memory_id: memoryId };
388
+ if (updates.content !== undefined) args.content = updates.content;
389
+ if (updates.type !== undefined) args.type = updates.type;
390
+ if (updates.tags !== undefined) args.tags = updates.tags;
391
+ if (updates.importance !== undefined) args.importance = updates.importance;
392
+ if (updates.confidence !== undefined) args.confidence = updates.confidence;
393
+ if (updates.metadata !== undefined) args.metadata = updates.metadata;
394
+ return mcpCall(resolveToolName("update_memory"), args);
395
+ }
396
+
397
+ export async function automemDelete(memoryId: string): Promise<McpCallResult> {
398
+ return mcpCall(resolveToolName("delete_memory"), {
399
+ memory_id: memoryId,
400
+ });
401
+ }