@memoryrelay/mcp-server 0.2.0 → 0.3.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.
- package/README.md +66 -138
- package/dist/chunk-P6TZEH6O.js +796 -0
- package/dist/chunk-P6TZEH6O.js.map +1 -0
- package/dist/cli/setup.d.ts +12 -0
- package/dist/cli/setup.js +243 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/test.d.ts +10 -0
- package/dist/cli/test.js +80 -0
- package/dist/cli/test.js.map +1 -0
- package/dist/index.js +1439 -649
- package/dist/index.js.map +1 -1
- package/docs/OPENCLAW_GUIDE.md +264 -0
- package/docs/SECURITY.md +1 -1
- package/package.json +7 -4
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var configSchema = z.object({
|
|
4
|
+
apiKey: z.string().startsWith("mem_", { message: 'API key must start with "mem_"' }).min(20, { message: "API key appears to be invalid (too short)" }),
|
|
5
|
+
apiUrl: z.string().url({ message: "API URL must be a valid URL" }).default("https://api.memoryrelay.net"),
|
|
6
|
+
agentId: z.string().optional().describe("Agent identifier - auto-detected if not provided"),
|
|
7
|
+
timeout: z.number().positive({ message: "Timeout must be positive" }).default(3e4),
|
|
8
|
+
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
9
|
+
});
|
|
10
|
+
var TOOL_GROUPS = {
|
|
11
|
+
core: [
|
|
12
|
+
"memory_store",
|
|
13
|
+
"memory_search",
|
|
14
|
+
"memory_list",
|
|
15
|
+
"memory_get",
|
|
16
|
+
"memory_update",
|
|
17
|
+
"memory_delete",
|
|
18
|
+
"entity_create",
|
|
19
|
+
"entity_link",
|
|
20
|
+
"entity_list",
|
|
21
|
+
"entity_graph",
|
|
22
|
+
"memory_batch_store",
|
|
23
|
+
"memory_context",
|
|
24
|
+
"agent_list",
|
|
25
|
+
"agent_create",
|
|
26
|
+
"agent_get",
|
|
27
|
+
"memory_health"
|
|
28
|
+
],
|
|
29
|
+
sessions: ["session_start", "session_end", "session_recall", "session_list"],
|
|
30
|
+
decisions: ["decision_record", "decision_list", "decision_supersede", "decision_check"],
|
|
31
|
+
patterns: ["pattern_create", "pattern_search", "pattern_adopt", "pattern_suggest"],
|
|
32
|
+
projects: ["project_register", "project_list", "project_info"],
|
|
33
|
+
relationships: [
|
|
34
|
+
"project_add_relationship",
|
|
35
|
+
"project_dependencies",
|
|
36
|
+
"project_dependents",
|
|
37
|
+
"project_related",
|
|
38
|
+
"project_impact",
|
|
39
|
+
"project_shared_patterns"
|
|
40
|
+
],
|
|
41
|
+
context: ["project_context", "memory_promote"]
|
|
42
|
+
};
|
|
43
|
+
function getEnabledTools() {
|
|
44
|
+
const raw = process.env.MEMORYRELAY_TOOLS;
|
|
45
|
+
if (!raw || raw.trim().toLowerCase() === "all") {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const groups = raw.split(",").map((g) => g.trim().toLowerCase());
|
|
49
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const group of groups) {
|
|
51
|
+
const tools = TOOL_GROUPS[group];
|
|
52
|
+
if (tools) {
|
|
53
|
+
for (const tool of tools) {
|
|
54
|
+
enabled.add(tool);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return enabled;
|
|
59
|
+
}
|
|
60
|
+
function loadConfig() {
|
|
61
|
+
try {
|
|
62
|
+
const config = configSchema.parse({
|
|
63
|
+
apiKey: process.env.MEMORYRELAY_API_KEY,
|
|
64
|
+
apiUrl: process.env.MEMORYRELAY_API_URL,
|
|
65
|
+
agentId: process.env.MEMORYRELAY_AGENT_ID,
|
|
66
|
+
timeout: process.env.MEMORYRELAY_TIMEOUT ? parseInt(process.env.MEMORYRELAY_TIMEOUT, 10) : void 0,
|
|
67
|
+
logLevel: process.env.MEMORYRELAY_LOG_LEVEL
|
|
68
|
+
});
|
|
69
|
+
return config;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof z.ZodError) {
|
|
72
|
+
const issues = error.issues.map(
|
|
73
|
+
(issue) => ` - ${issue.path.join(".")}: ${issue.message}`
|
|
74
|
+
).join("\n");
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Configuration validation failed:
|
|
77
|
+
${issues}
|
|
78
|
+
|
|
79
|
+
Please check your environment variables:
|
|
80
|
+
- MEMORYRELAY_API_KEY (required, starts with "mem_")
|
|
81
|
+
- MEMORYRELAY_API_URL (optional, default: https://api.memoryrelay.net)
|
|
82
|
+
- MEMORYRELAY_AGENT_ID (optional, auto-detected)
|
|
83
|
+
- MEMORYRELAY_TIMEOUT (optional, default: 30000)
|
|
84
|
+
- MEMORYRELAY_LOG_LEVEL (optional, default: info)`,
|
|
85
|
+
{ cause: error }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getAgentId(config) {
|
|
92
|
+
if (config.agentId) {
|
|
93
|
+
return config.agentId;
|
|
94
|
+
}
|
|
95
|
+
const openclawAgent = process.env.OPENCLAW_AGENT_NAME;
|
|
96
|
+
if (openclawAgent) {
|
|
97
|
+
return openclawAgent.slice(0, 32);
|
|
98
|
+
}
|
|
99
|
+
const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || "unknown";
|
|
100
|
+
const user = process.env.USER || process.env.USERNAME || "";
|
|
101
|
+
return user ? `${user}-${hostname}`.slice(0, 32) : `mcp-${hostname}`.slice(0, 32);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/logger.ts
|
|
105
|
+
var LOG_LEVELS = {
|
|
106
|
+
debug: 0,
|
|
107
|
+
info: 1,
|
|
108
|
+
warn: 2,
|
|
109
|
+
error: 3
|
|
110
|
+
};
|
|
111
|
+
var Logger = class {
|
|
112
|
+
minLevel;
|
|
113
|
+
constructor(level = "info") {
|
|
114
|
+
this.minLevel = LOG_LEVELS[level];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Mask sensitive data in log messages
|
|
118
|
+
* - API keys starting with "mem_" are masked
|
|
119
|
+
* - Internal paths are sanitized
|
|
120
|
+
*/
|
|
121
|
+
sanitize(message) {
|
|
122
|
+
let sanitized = message;
|
|
123
|
+
sanitized = sanitized.replace(/mem_[a-zA-Z0-9_-]+/g, "mem_****");
|
|
124
|
+
sanitized = sanitized.replace(/\/[a-zA-Z0-9_\-./]+\.(ts|js|json)/g, "<file>");
|
|
125
|
+
sanitized = sanitized.replace(/at\s+[^\s]+\s+\([^)]+\)/g, "at <location>");
|
|
126
|
+
return sanitized;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Format log message with timestamp and level
|
|
130
|
+
*/
|
|
131
|
+
format(level, message, data) {
|
|
132
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
133
|
+
const sanitizedMessage = this.sanitize(message);
|
|
134
|
+
let output = `[${timestamp}] [${level.toUpperCase()}] ${sanitizedMessage}`;
|
|
135
|
+
if (data !== void 0) {
|
|
136
|
+
const sanitizedData = this.sanitize(JSON.stringify(data, null, 2));
|
|
137
|
+
output += `
|
|
138
|
+
${sanitizedData}`;
|
|
139
|
+
}
|
|
140
|
+
return output;
|
|
141
|
+
}
|
|
142
|
+
debug(message, data) {
|
|
143
|
+
if (this.minLevel <= LOG_LEVELS.debug) {
|
|
144
|
+
console.error(this.format("debug", message, data));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
info(message, data) {
|
|
148
|
+
if (this.minLevel <= LOG_LEVELS.info) {
|
|
149
|
+
console.error(this.format("info", message, data));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
warn(message, data) {
|
|
153
|
+
if (this.minLevel <= LOG_LEVELS.warn) {
|
|
154
|
+
console.error(this.format("warn", message, data));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
error(message, data) {
|
|
158
|
+
if (this.minLevel <= LOG_LEVELS.error) {
|
|
159
|
+
console.error(this.format("error", message, data));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var logger;
|
|
164
|
+
function initLogger(level = "info") {
|
|
165
|
+
logger = new Logger(level);
|
|
166
|
+
return logger;
|
|
167
|
+
}
|
|
168
|
+
function getLogger() {
|
|
169
|
+
if (!logger) {
|
|
170
|
+
logger = new Logger();
|
|
171
|
+
}
|
|
172
|
+
return logger;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/client.ts
|
|
176
|
+
var MAX_RETRIES = 3;
|
|
177
|
+
var INITIAL_DELAY_MS = 1e3;
|
|
178
|
+
var MAX_CONTENT_SIZE = 50 * 1024;
|
|
179
|
+
async function withRetry(fn, retries = MAX_RETRIES) {
|
|
180
|
+
let lastError;
|
|
181
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
182
|
+
try {
|
|
183
|
+
return await fn();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
186
|
+
if (lastError.message.includes("401") || lastError.message.includes("403") || lastError.message.includes("404") || lastError.message.includes("400")) {
|
|
187
|
+
throw lastError;
|
|
188
|
+
}
|
|
189
|
+
if (attempt === retries) {
|
|
190
|
+
throw lastError;
|
|
191
|
+
}
|
|
192
|
+
const delay = INITIAL_DELAY_MS * Math.pow(2, attempt);
|
|
193
|
+
const jitter = Math.random() * 0.3 * delay;
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw lastError || new Error("Retry failed");
|
|
198
|
+
}
|
|
199
|
+
function maskApiKey(message, apiKey) {
|
|
200
|
+
if (!apiKey) return message;
|
|
201
|
+
const maskedKey = apiKey.substring(0, 8) + "***";
|
|
202
|
+
return message.replace(new RegExp(apiKey, "g"), maskedKey);
|
|
203
|
+
}
|
|
204
|
+
var MemoryRelayClient = class {
|
|
205
|
+
config;
|
|
206
|
+
logger = getLogger();
|
|
207
|
+
constructor(config) {
|
|
208
|
+
this.config = config;
|
|
209
|
+
this.logger.info("MemoryRelay client initialized", {
|
|
210
|
+
apiUrl: config.apiUrl,
|
|
211
|
+
agentId: config.agentId
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Make authenticated HTTP request to MemoryRelay API with retry logic
|
|
216
|
+
*/
|
|
217
|
+
async request(method, path, body) {
|
|
218
|
+
return withRetry(async () => {
|
|
219
|
+
const url = `${this.config.apiUrl}${path}`;
|
|
220
|
+
this.logger.debug(`API request: ${method} ${path}`);
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(url, {
|
|
225
|
+
method,
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
229
|
+
"User-Agent": "@memoryrelay/mcp-server"
|
|
230
|
+
},
|
|
231
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
232
|
+
signal: controller.signal
|
|
233
|
+
});
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
if (response.status === 429) {
|
|
236
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
237
|
+
const waitMs = retryAfter ? parseInt(retryAfter) * 1e3 : 5e3;
|
|
238
|
+
this.logger.warn(`Rate limited, waiting ${waitMs}ms`);
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
240
|
+
throw new Error(`Rate limited: 429 - Retry after ${waitMs}ms`);
|
|
241
|
+
}
|
|
242
|
+
const errorData = await response.json().catch(() => ({}));
|
|
243
|
+
const errorMsg = `API request failed: ${response.status} ${response.statusText}` + (errorData.message ? ` - ${errorData.message}` : "");
|
|
244
|
+
throw new Error(maskApiKey(errorMsg, this.config.apiKey));
|
|
245
|
+
}
|
|
246
|
+
const data = await response.json();
|
|
247
|
+
this.logger.debug(`API response: ${method} ${path}`, { status: response.status });
|
|
248
|
+
return data;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof Error) {
|
|
251
|
+
if (error.name === "AbortError") {
|
|
252
|
+
throw new Error(`Request timeout after ${this.config.timeout}ms`, { cause: error });
|
|
253
|
+
}
|
|
254
|
+
error.message = maskApiKey(error.message, this.config.apiKey);
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
} finally {
|
|
258
|
+
clearTimeout(timeout);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Validate content size
|
|
264
|
+
*/
|
|
265
|
+
validateContentSize(content) {
|
|
266
|
+
if (content.length > MAX_CONTENT_SIZE) {
|
|
267
|
+
throw new Error(`Content exceeds maximum size of ${MAX_CONTENT_SIZE} bytes`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Store a new memory
|
|
272
|
+
*/
|
|
273
|
+
async storeMemory(content, metadata, deduplicate, dedupThreshold, project, importance, tier) {
|
|
274
|
+
this.validateContentSize(content);
|
|
275
|
+
const body = {
|
|
276
|
+
content,
|
|
277
|
+
metadata,
|
|
278
|
+
agent_id: this.config.agentId
|
|
279
|
+
};
|
|
280
|
+
if (deduplicate) {
|
|
281
|
+
body.deduplicate = true;
|
|
282
|
+
}
|
|
283
|
+
if (dedupThreshold !== void 0) {
|
|
284
|
+
body.dedup_threshold = dedupThreshold;
|
|
285
|
+
}
|
|
286
|
+
if (project) {
|
|
287
|
+
body.project = project;
|
|
288
|
+
}
|
|
289
|
+
if (importance !== void 0) {
|
|
290
|
+
body.importance = importance;
|
|
291
|
+
}
|
|
292
|
+
if (tier) {
|
|
293
|
+
body.tier = tier;
|
|
294
|
+
}
|
|
295
|
+
return this.request("POST", "/v1/memories", body);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Search memories using semantic search
|
|
299
|
+
* @param agentId - Optional agent ID override. If omitted, uses config agentId. Pass null for cross-agent search.
|
|
300
|
+
* @param includeConfidential - Include confidential memories in results
|
|
301
|
+
* @param includeArchived - Include archived memories in results
|
|
302
|
+
* @param project - Optional project slug to filter by
|
|
303
|
+
*/
|
|
304
|
+
async searchMemories(query, limit = 10, threshold = 0.5, agentId, includeConfidential = false, includeArchived = false, compress = false, maxContextTokens, project, tier, minImportance) {
|
|
305
|
+
this.validateContentSize(query);
|
|
306
|
+
const effectiveAgentId = agentId === null ? void 0 : agentId ?? this.config.agentId;
|
|
307
|
+
const body = { query, limit, threshold };
|
|
308
|
+
if (effectiveAgentId) {
|
|
309
|
+
body.agent_id = effectiveAgentId;
|
|
310
|
+
}
|
|
311
|
+
if (includeConfidential) {
|
|
312
|
+
body.include_confidential = true;
|
|
313
|
+
}
|
|
314
|
+
if (includeArchived) {
|
|
315
|
+
body.include_archived = true;
|
|
316
|
+
}
|
|
317
|
+
if (compress) {
|
|
318
|
+
body.compress = true;
|
|
319
|
+
}
|
|
320
|
+
if (maxContextTokens !== void 0) {
|
|
321
|
+
body.max_context_tokens = maxContextTokens;
|
|
322
|
+
}
|
|
323
|
+
if (project) {
|
|
324
|
+
body.project = project;
|
|
325
|
+
}
|
|
326
|
+
if (tier) {
|
|
327
|
+
body.tier = tier;
|
|
328
|
+
}
|
|
329
|
+
if (minImportance !== void 0) {
|
|
330
|
+
body.min_importance = minImportance;
|
|
331
|
+
}
|
|
332
|
+
const response = await this.request(
|
|
333
|
+
"POST",
|
|
334
|
+
"/v1/memories/search",
|
|
335
|
+
body
|
|
336
|
+
);
|
|
337
|
+
return response.data;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* List recent memories with pagination
|
|
341
|
+
*/
|
|
342
|
+
async listMemories(limit = 20, offset = 0) {
|
|
343
|
+
return this.request(
|
|
344
|
+
"GET",
|
|
345
|
+
`/v1/memories?limit=${limit}&offset=${offset}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get a specific memory by ID
|
|
350
|
+
*/
|
|
351
|
+
async getMemory(id) {
|
|
352
|
+
return this.request("GET", `/v1/memories/${id}`);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Update an existing memory
|
|
356
|
+
*/
|
|
357
|
+
async updateMemory(id, content, metadata) {
|
|
358
|
+
this.validateContentSize(content);
|
|
359
|
+
return this.request("PATCH", `/v1/memories/${id}`, {
|
|
360
|
+
content,
|
|
361
|
+
metadata
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Delete a memory
|
|
366
|
+
*/
|
|
367
|
+
async deleteMemory(id) {
|
|
368
|
+
await this.request("DELETE", `/v1/memories/${id}`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Create a named entity
|
|
372
|
+
*/
|
|
373
|
+
async createEntity(name, type, metadata) {
|
|
374
|
+
this.validateContentSize(name);
|
|
375
|
+
return this.request("POST", "/v1/entities", {
|
|
376
|
+
name,
|
|
377
|
+
type,
|
|
378
|
+
metadata
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Link an entity to a memory
|
|
383
|
+
*/
|
|
384
|
+
async linkEntity(entityId, memoryId, relationship = "mentioned_in") {
|
|
385
|
+
await this.request("POST", "/v1/entities/links", {
|
|
386
|
+
entity_id: entityId,
|
|
387
|
+
memory_id: memoryId,
|
|
388
|
+
relationship
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get an entity by ID
|
|
393
|
+
*/
|
|
394
|
+
async getEntity(id) {
|
|
395
|
+
return this.request("GET", `/v1/entities/${id}`);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* List entities with pagination
|
|
399
|
+
*/
|
|
400
|
+
async listEntities(limit = 20, offset = 0) {
|
|
401
|
+
return this.request(
|
|
402
|
+
"GET",
|
|
403
|
+
`/v1/entities?limit=${limit}&offset=${offset}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get entity neighborhood (ego-centric subgraph).
|
|
408
|
+
* Returns the entity's 1-hop or 2-hop neighbors and relationships.
|
|
409
|
+
*/
|
|
410
|
+
async getEntityNeighborhood(entityId, depth = 1, maxNeighbors = 50) {
|
|
411
|
+
return this.request(
|
|
412
|
+
"GET",
|
|
413
|
+
`/v1/entities/${entityId}/neighborhood?depth=${depth}&max_neighbors=${maxNeighbors}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Delete an entity
|
|
418
|
+
*/
|
|
419
|
+
async deleteEntity(id) {
|
|
420
|
+
await this.request("DELETE", `/v1/entities/${id}`);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* List agents with pagination
|
|
424
|
+
*/
|
|
425
|
+
async listAgents(limit = 20, offset = 0) {
|
|
426
|
+
return this.request(
|
|
427
|
+
"GET",
|
|
428
|
+
`/v1/agents?limit=${limit}&offset=${offset}`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Create a new agent
|
|
433
|
+
*/
|
|
434
|
+
async createAgent(name, description, metadata) {
|
|
435
|
+
const body = { name };
|
|
436
|
+
if (description) body.description = description;
|
|
437
|
+
if (metadata) body.metadata = metadata;
|
|
438
|
+
return this.request("POST", "/v1/agents", body);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get an agent by ID
|
|
442
|
+
*/
|
|
443
|
+
async getAgent(id) {
|
|
444
|
+
return this.request("GET", `/v1/agents/${id}`);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Batch store multiple memories in a single API call.
|
|
448
|
+
* Uses the /v1/memories/batch endpoint.
|
|
449
|
+
*/
|
|
450
|
+
async batchStoreMemories(items) {
|
|
451
|
+
if (items.length === 0) {
|
|
452
|
+
return { success: true, total: 0, succeeded: 0, failed: 0, skipped: 0, results: [] };
|
|
453
|
+
}
|
|
454
|
+
if (items.length > 100) {
|
|
455
|
+
throw new Error("Batch size exceeds maximum of 100 memories");
|
|
456
|
+
}
|
|
457
|
+
const memories = items.map((item) => ({
|
|
458
|
+
content: item.content,
|
|
459
|
+
metadata: item.metadata || {},
|
|
460
|
+
agent_id: item.agent_id || this.config.agentId
|
|
461
|
+
}));
|
|
462
|
+
return this.request("POST", "/v1/memories/batch", { memories });
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Build a context string from search results.
|
|
466
|
+
* Searches for relevant memories, formats them, and returns
|
|
467
|
+
* a single string ready for prompt injection.
|
|
468
|
+
*/
|
|
469
|
+
async buildContext(query, limit = 10, threshold = 0.5, maxTokens) {
|
|
470
|
+
const results = await this.searchMemories(
|
|
471
|
+
query,
|
|
472
|
+
limit,
|
|
473
|
+
threshold,
|
|
474
|
+
void 0,
|
|
475
|
+
// use default agent
|
|
476
|
+
false,
|
|
477
|
+
// no confidential
|
|
478
|
+
false,
|
|
479
|
+
// no archived
|
|
480
|
+
!!maxTokens,
|
|
481
|
+
// compress if token budget provided
|
|
482
|
+
maxTokens
|
|
483
|
+
);
|
|
484
|
+
if (results.length === 0) {
|
|
485
|
+
return { context: "", memories_used: 0, total_chars: 0 };
|
|
486
|
+
}
|
|
487
|
+
const lines = [];
|
|
488
|
+
for (const result of results) {
|
|
489
|
+
const score = (result.score * 100).toFixed(0);
|
|
490
|
+
lines.push(`[${score}%] ${result.memory.content}`);
|
|
491
|
+
}
|
|
492
|
+
const context = lines.join("\n\n");
|
|
493
|
+
let finalContext = context;
|
|
494
|
+
if (maxTokens) {
|
|
495
|
+
const charBudget = maxTokens * 4;
|
|
496
|
+
if (finalContext.length > charBudget) {
|
|
497
|
+
finalContext = finalContext.slice(0, charBudget) + "\n\n[...truncated]";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
context: finalContext,
|
|
502
|
+
memories_used: results.length,
|
|
503
|
+
total_chars: finalContext.length
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Start a new session
|
|
508
|
+
*/
|
|
509
|
+
async startSession(title, project, metadata) {
|
|
510
|
+
const body = {};
|
|
511
|
+
if (this.config.agentId) body.agent_id = this.config.agentId;
|
|
512
|
+
if (title) body.title = title;
|
|
513
|
+
if (project) body.project = project;
|
|
514
|
+
if (metadata) body.metadata = metadata;
|
|
515
|
+
return this.request("POST", "/v1/sessions", body);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* End an active session
|
|
519
|
+
*/
|
|
520
|
+
async endSession(sessionId, summary) {
|
|
521
|
+
const body = {};
|
|
522
|
+
if (summary) body.summary = summary;
|
|
523
|
+
return this.request("PUT", `/v1/sessions/${sessionId}/end`, body);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get a session by ID with its memories
|
|
527
|
+
*/
|
|
528
|
+
async getSession(sessionId) {
|
|
529
|
+
return this.request(
|
|
530
|
+
"GET",
|
|
531
|
+
`/v1/sessions/${sessionId}?include_memories=true`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* List sessions with optional filters
|
|
536
|
+
*/
|
|
537
|
+
async listSessions(limit = 20, agentId, project, status) {
|
|
538
|
+
const params = new URLSearchParams();
|
|
539
|
+
params.set("limit", String(limit));
|
|
540
|
+
const effectiveAgentId = agentId ?? this.config.agentId;
|
|
541
|
+
if (effectiveAgentId) params.set("agent_id", effectiveAgentId);
|
|
542
|
+
if (project) params.set("project", project);
|
|
543
|
+
if (status) params.set("status", status);
|
|
544
|
+
return this.request(
|
|
545
|
+
"GET",
|
|
546
|
+
`/v1/sessions?${params.toString()}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Record a new decision
|
|
551
|
+
*/
|
|
552
|
+
async recordDecision(title, rationale, alternatives, project, tags, status, metadata) {
|
|
553
|
+
const body = { title, rationale };
|
|
554
|
+
if (this.config.agentId) body.agent_id = this.config.agentId;
|
|
555
|
+
if (alternatives) body.alternatives = alternatives;
|
|
556
|
+
if (project) body.project_slug = project;
|
|
557
|
+
if (tags) body.tags = tags;
|
|
558
|
+
if (status) body.status = status;
|
|
559
|
+
if (metadata) body.metadata = metadata;
|
|
560
|
+
return this.request("POST", "/v1/decisions", body);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* List decisions with optional filters
|
|
564
|
+
*/
|
|
565
|
+
async listDecisions(limit, project, status, tags) {
|
|
566
|
+
const params = new URLSearchParams();
|
|
567
|
+
if (limit) params.set("limit", String(limit));
|
|
568
|
+
if (project) params.set("project", project);
|
|
569
|
+
if (status) params.set("status", status);
|
|
570
|
+
if (tags) params.set("tags", tags);
|
|
571
|
+
return this.request(
|
|
572
|
+
"GET",
|
|
573
|
+
`/v1/decisions?${params.toString()}`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Supersede a decision with a new one
|
|
578
|
+
*/
|
|
579
|
+
async supersedeDecision(decisionId, title, rationale, alternatives, tags) {
|
|
580
|
+
const body = { title, rationale };
|
|
581
|
+
if (alternatives) body.alternatives = alternatives;
|
|
582
|
+
if (tags) body.tags = tags;
|
|
583
|
+
return this.request(
|
|
584
|
+
"POST",
|
|
585
|
+
`/v1/decisions/${decisionId}/supersede`,
|
|
586
|
+
body
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Check for existing decisions about a topic (semantic search)
|
|
591
|
+
*/
|
|
592
|
+
async checkDecisions(query, project, limit, threshold, includeSuperseded) {
|
|
593
|
+
const params = new URLSearchParams();
|
|
594
|
+
params.set("query", query);
|
|
595
|
+
if (limit) params.set("limit", String(limit));
|
|
596
|
+
if (threshold !== void 0) params.set("threshold", String(threshold));
|
|
597
|
+
if (project) params.set("project", project);
|
|
598
|
+
if (includeSuperseded) params.set("include_superseded", "true");
|
|
599
|
+
return this.request(
|
|
600
|
+
"GET",
|
|
601
|
+
`/v1/decisions/check?${params.toString()}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Register a new project
|
|
606
|
+
*/
|
|
607
|
+
async createProject(slug, name, description, stack, repo_url, metadata) {
|
|
608
|
+
const body = { slug, name };
|
|
609
|
+
if (description) body.description = description;
|
|
610
|
+
if (stack) body.stack = stack;
|
|
611
|
+
if (repo_url) body.repo_url = repo_url;
|
|
612
|
+
if (metadata) body.metadata = metadata;
|
|
613
|
+
return this.request("POST", "/v1/projects", body);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* List projects with optional pagination
|
|
617
|
+
*/
|
|
618
|
+
async listProjects(limit = 20, cursor) {
|
|
619
|
+
const params = new URLSearchParams();
|
|
620
|
+
params.set("limit", String(limit));
|
|
621
|
+
if (cursor) params.set("cursor", cursor);
|
|
622
|
+
return this.request(
|
|
623
|
+
"GET",
|
|
624
|
+
`/v1/projects?${params.toString()}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Get a project by slug
|
|
629
|
+
*/
|
|
630
|
+
async getProject(slug) {
|
|
631
|
+
return this.request("GET", `/v1/projects/${slug}`);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Create a reusable pattern
|
|
635
|
+
*/
|
|
636
|
+
async createPattern(title, description, category, example_code, scope, tags, source_project, metadata) {
|
|
637
|
+
const body = { title, description };
|
|
638
|
+
if (category) body.category = category;
|
|
639
|
+
if (example_code) body.example_code = example_code;
|
|
640
|
+
if (scope) body.scope = scope;
|
|
641
|
+
if (tags) body.tags = tags;
|
|
642
|
+
if (source_project) body.source_project = source_project;
|
|
643
|
+
if (metadata) body.metadata = metadata;
|
|
644
|
+
return this.request("POST", "/v1/patterns", body);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Search patterns using semantic search
|
|
648
|
+
*/
|
|
649
|
+
async searchPatterns(query, category, project, limit, threshold) {
|
|
650
|
+
const params = new URLSearchParams();
|
|
651
|
+
params.set("query", query);
|
|
652
|
+
if (category) params.set("category", category);
|
|
653
|
+
if (project) params.set("project", project);
|
|
654
|
+
if (limit) params.set("limit", String(limit));
|
|
655
|
+
if (threshold !== void 0) params.set("threshold", String(threshold));
|
|
656
|
+
return this.request(
|
|
657
|
+
"GET",
|
|
658
|
+
`/v1/patterns/search?${params.toString()}`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Adopt a pattern for a project
|
|
663
|
+
*/
|
|
664
|
+
async adoptPattern(patternId, project) {
|
|
665
|
+
return this.request(
|
|
666
|
+
"POST",
|
|
667
|
+
`/v1/patterns/${patternId}/adopt`,
|
|
668
|
+
{ project }
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Suggest patterns for a project
|
|
673
|
+
*/
|
|
674
|
+
async suggestPatterns(project, limit) {
|
|
675
|
+
const params = new URLSearchParams();
|
|
676
|
+
params.set("project", project);
|
|
677
|
+
if (limit) params.set("limit", String(limit));
|
|
678
|
+
return this.request(
|
|
679
|
+
"GET",
|
|
680
|
+
`/v1/patterns/suggest?${params.toString()}`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
// ── Project Relationships (Issue #186) ──
|
|
684
|
+
/**
|
|
685
|
+
* Add a relationship between two projects
|
|
686
|
+
*/
|
|
687
|
+
async addProjectRelationship(sourceSlug, targetProject, relationshipType, metadata) {
|
|
688
|
+
const body = {
|
|
689
|
+
target_project: targetProject,
|
|
690
|
+
relationship_type: relationshipType
|
|
691
|
+
};
|
|
692
|
+
if (metadata) body.metadata = metadata;
|
|
693
|
+
return this.request(
|
|
694
|
+
"POST",
|
|
695
|
+
`/v1/projects/${sourceSlug}/relationships`,
|
|
696
|
+
body
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Get what this project depends on
|
|
701
|
+
*/
|
|
702
|
+
async getProjectDependencies(slug) {
|
|
703
|
+
return this.request(
|
|
704
|
+
"GET",
|
|
705
|
+
`/v1/projects/${slug}/dependencies`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get what depends on this project
|
|
710
|
+
*/
|
|
711
|
+
async getProjectDependents(slug) {
|
|
712
|
+
return this.request(
|
|
713
|
+
"GET",
|
|
714
|
+
`/v1/projects/${slug}/dependents`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Get all related projects
|
|
719
|
+
*/
|
|
720
|
+
async getProjectRelated(slug) {
|
|
721
|
+
return this.request(
|
|
722
|
+
"GET",
|
|
723
|
+
`/v1/projects/${slug}/related`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Run impact analysis for a project change
|
|
728
|
+
*/
|
|
729
|
+
async projectImpactAnalysis(project, changeDescription) {
|
|
730
|
+
return this.request(
|
|
731
|
+
"POST",
|
|
732
|
+
"/v1/projects/impact-analysis",
|
|
733
|
+
{ project, change_description: changeDescription }
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Find patterns shared between two projects
|
|
738
|
+
*/
|
|
739
|
+
async getSharedPatterns(slugA, slugB) {
|
|
740
|
+
return this.request(
|
|
741
|
+
"GET",
|
|
742
|
+
`/v1/projects/shared-patterns?a=${encodeURIComponent(slugA)}&b=${encodeURIComponent(slugB)}`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Get full project context (hot memories, decisions, patterns, formatted text)
|
|
747
|
+
*/
|
|
748
|
+
async getProjectContext(slug) {
|
|
749
|
+
return this.request(
|
|
750
|
+
"GET",
|
|
751
|
+
`/v1/projects/${encodeURIComponent(slug)}/context`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Promote/demote a memory by updating its importance and tier
|
|
756
|
+
*/
|
|
757
|
+
async promoteMemory(memoryId, importance, tier) {
|
|
758
|
+
const body = { importance };
|
|
759
|
+
if (tier) {
|
|
760
|
+
body.tier = tier;
|
|
761
|
+
}
|
|
762
|
+
return this.request(
|
|
763
|
+
"PUT",
|
|
764
|
+
`/v1/memories/${encodeURIComponent(memoryId)}/importance`,
|
|
765
|
+
body
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Health check - verify API connectivity
|
|
770
|
+
*/
|
|
771
|
+
async healthCheck() {
|
|
772
|
+
try {
|
|
773
|
+
await this.request("GET", "/v1/health");
|
|
774
|
+
return {
|
|
775
|
+
status: "healthy",
|
|
776
|
+
message: "API connection successful"
|
|
777
|
+
};
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
780
|
+
return {
|
|
781
|
+
status: "unhealthy",
|
|
782
|
+
message: `API connection failed: ${errorMsg}`
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
export {
|
|
789
|
+
getEnabledTools,
|
|
790
|
+
loadConfig,
|
|
791
|
+
getAgentId,
|
|
792
|
+
initLogger,
|
|
793
|
+
getLogger,
|
|
794
|
+
MemoryRelayClient
|
|
795
|
+
};
|
|
796
|
+
//# sourceMappingURL=chunk-P6TZEH6O.js.map
|