@memoryrelay/plugin-memoryrelay-ai 0.6.2 → 0.8.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/index.ts CHANGED
@@ -1,14 +1,27 @@
1
1
  /**
2
2
  * OpenClaw Memory Plugin - MemoryRelay
3
- * Version: 0.6.0 (Enhanced)
3
+ * Version: 0.8.0 (Enhanced Debug & Status)
4
4
  *
5
5
  * Long-term memory with vector search using MemoryRelay API.
6
6
  * Provides auto-recall and auto-capture via lifecycle hooks.
7
+ * Includes: memories, entities, agents, sessions, decisions, patterns, projects.
7
8
  *
8
9
  * API: https://api.memoryrelay.net
9
- * Docs: https://memoryrelay.io
10
+ * Docs: https://memoryrelay.ai
10
11
  *
11
- * ENHANCEMENTS (v0.6.0):
12
+ * ENHANCEMENTS (v0.8.0):
13
+ * - Debug mode with comprehensive API call logging
14
+ * - Enhanced status reporting with tool breakdown
15
+ * - Request/response capture (verbose mode)
16
+ * - Tool failure tracking and known issues display
17
+ * - Performance metrics (duration, success rate)
18
+ * - Recent activity display
19
+ * - Formatted CLI output with Unicode symbols
20
+ *
21
+ * ENHANCEMENTS (v0.7.0):
22
+ * - 39 tools covering all MemoryRelay API resources
23
+ * - Session tracking, decision logging, pattern management, project context
24
+ * - Agent workflow instructions injected via before_agent_start
12
25
  * - Retry logic with exponential backoff (3 attempts)
13
26
  * - Request timeout (30 seconds)
14
27
  * - Environment variable fallback support
@@ -17,6 +30,8 @@
17
30
  */
18
31
 
19
32
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
33
+ import { DebugLogger, type LogEntry } from "./src/debug-logger";
34
+ import { StatusReporter } from "./src/status-reporter";
20
35
 
21
36
  // ============================================================================
22
37
  // Constants
@@ -33,14 +48,21 @@ const INITIAL_RETRY_DELAY_MS = 1000; // 1 second
33
48
  // ============================================================================
34
49
 
35
50
  interface MemoryRelayConfig {
36
- apiKey?: string; // Optional now (can use env var)
37
- agentId?: string; // Optional now (can use env var)
51
+ apiKey?: string;
52
+ agentId?: string;
38
53
  apiUrl?: string;
39
54
  autoCapture?: boolean;
40
55
  autoRecall?: boolean;
41
56
  recallLimit?: number;
42
57
  recallThreshold?: number;
43
- excludeChannels?: string[]; // NEW: Channels to skip auto-recall
58
+ excludeChannels?: string[];
59
+ defaultProject?: string;
60
+ enabledTools?: string;
61
+ // Debug and logging options (v0.8.0)
62
+ debug?: boolean;
63
+ verbose?: boolean;
64
+ logFile?: string;
65
+ maxLogEntries?: number;
44
66
  }
45
67
 
46
68
  interface Memory {
@@ -120,15 +142,45 @@ async function fetchWithTimeout(
120
142
  }
121
143
 
122
144
  // ============================================================================
123
- // MemoryRelay API Client (Enhanced)
145
+ // MemoryRelay API Client (Full Suite)
124
146
  // ============================================================================
125
147
 
126
148
  class MemoryRelayClient {
149
+ private debugLogger?: DebugLogger;
150
+ private statusReporter?: StatusReporter;
151
+
127
152
  constructor(
128
153
  private readonly apiKey: string,
129
154
  private readonly agentId: string,
130
155
  private readonly apiUrl: string = DEFAULT_API_URL,
131
- ) {}
156
+ debugLogger?: DebugLogger,
157
+ statusReporter?: StatusReporter,
158
+ ) {
159
+ this.debugLogger = debugLogger;
160
+ this.statusReporter = statusReporter;
161
+ }
162
+
163
+ /**
164
+ * Extract tool name from API path
165
+ */
166
+ private extractToolName(path: string): string {
167
+ // /v1/memories -> memory
168
+ // /v1/memories/batch -> memory_batch
169
+ // /v1/sessions/123/end -> session_end
170
+ const parts = path.split("/").filter(Boolean);
171
+ if (parts.length < 2) return "unknown";
172
+
173
+ let toolName = parts[1].replace(/s$/, ""); // Remove trailing 's'
174
+
175
+ // Check for specific endpoints
176
+ if (path.includes("/batch")) toolName += "_batch";
177
+ if (path.includes("/recall")) toolName += "_recall";
178
+ if (path.includes("/context")) toolName += "_context";
179
+ if (path.includes("/end")) toolName += "_end";
180
+ if (path.includes("/health")) return "memory_health";
181
+
182
+ return toolName;
183
+ }
132
184
 
133
185
  /**
134
186
  * Make HTTP request with retry logic and timeout
@@ -140,6 +192,8 @@ class MemoryRelayClient {
140
192
  retryCount = 0,
141
193
  ): Promise<T> {
142
194
  const url = `${this.apiUrl}${path}`;
195
+ const startTime = Date.now();
196
+ const toolName = this.extractToolName(path);
143
197
 
144
198
  try {
145
199
  const response = await fetchWithTimeout(
@@ -149,20 +203,44 @@ class MemoryRelayClient {
149
203
  headers: {
150
204
  "Content-Type": "application/json",
151
205
  Authorization: `Bearer ${this.apiKey}`,
152
- "User-Agent": "openclaw-memory-memoryrelay/0.6.0",
206
+ "User-Agent": "openclaw-memory-memoryrelay/0.8.0",
153
207
  },
154
208
  body: body ? JSON.stringify(body) : undefined,
155
209
  },
156
210
  REQUEST_TIMEOUT_MS,
157
211
  );
158
212
 
213
+ const duration = Date.now() - startTime;
214
+
159
215
  if (!response.ok) {
160
216
  const errorData = await response.json().catch(() => ({}));
217
+ const errorMsg = errorData.detail || errorData.message || "";
161
218
  const error = new Error(
162
219
  `MemoryRelay API error: ${response.status} ${response.statusText}` +
163
- (errorData.message ? ` - ${errorData.message}` : ""),
220
+ (errorMsg ? ` - ${errorMsg}` : ""),
164
221
  );
165
222
 
223
+ // Log error
224
+ if (this.debugLogger) {
225
+ this.debugLogger.log({
226
+ timestamp: new Date().toISOString(),
227
+ tool: toolName,
228
+ method,
229
+ path,
230
+ duration,
231
+ status: "error",
232
+ responseStatus: response.status,
233
+ error: error.message,
234
+ retries: retryCount,
235
+ requestBody: this.debugLogger && body ? body : undefined,
236
+ });
237
+ }
238
+
239
+ // Track failure
240
+ if (this.statusReporter) {
241
+ this.statusReporter.recordFailure(toolName, `${response.status} ${errorMsg || response.statusText}`);
242
+ }
243
+
166
244
  // Retry on 5xx errors
167
245
  if (response.status >= 500 && retryCount < MAX_RETRIES) {
168
246
  const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
@@ -173,8 +251,53 @@ class MemoryRelayClient {
173
251
  throw error;
174
252
  }
175
253
 
176
- return response.json();
254
+ const result = await response.json();
255
+
256
+ // Log success
257
+ if (this.debugLogger) {
258
+ this.debugLogger.log({
259
+ timestamp: new Date().toISOString(),
260
+ tool: toolName,
261
+ method,
262
+ path,
263
+ duration,
264
+ status: "success",
265
+ responseStatus: response.status,
266
+ retries: retryCount,
267
+ requestBody: this.debugLogger && body ? body : undefined,
268
+ responseBody: this.debugLogger && result ? result : undefined,
269
+ });
270
+ }
271
+
272
+ // Track success
273
+ if (this.statusReporter) {
274
+ this.statusReporter.recordSuccess(toolName);
275
+ }
276
+
277
+ return result;
177
278
  } catch (err) {
279
+ const duration = Date.now() - startTime;
280
+
281
+ // Log error
282
+ if (this.debugLogger) {
283
+ this.debugLogger.log({
284
+ timestamp: new Date().toISOString(),
285
+ tool: toolName,
286
+ method,
287
+ path,
288
+ duration,
289
+ status: "error",
290
+ error: String(err),
291
+ retries: retryCount,
292
+ requestBody: this.debugLogger && body ? body : undefined,
293
+ });
294
+ }
295
+
296
+ // Track failure
297
+ if (this.statusReporter) {
298
+ this.statusReporter.recordFailure(toolName, String(err));
299
+ }
300
+
178
301
  // Retry on network errors
179
302
  if (isRetryableError(err) && retryCount < MAX_RETRIES) {
180
303
  const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
@@ -186,11 +309,26 @@ class MemoryRelayClient {
186
309
  }
187
310
  }
188
311
 
189
- async store(content: string, metadata?: Record<string, string>): Promise<Memory> {
312
+ // --------------------------------------------------------------------------
313
+ // Memory operations
314
+ // --------------------------------------------------------------------------
315
+
316
+ async store(
317
+ content: string,
318
+ metadata?: Record<string, string>,
319
+ options?: {
320
+ deduplicate?: boolean;
321
+ dedup_threshold?: number;
322
+ project?: string;
323
+ importance?: number;
324
+ tier?: string;
325
+ },
326
+ ): Promise<Memory> {
190
327
  return this.request<Memory>("POST", "/v1/memories", {
191
328
  content,
192
329
  metadata,
193
330
  agent_id: this.agentId,
331
+ ...options,
194
332
  });
195
333
  }
196
334
 
@@ -198,6 +336,15 @@ class MemoryRelayClient {
198
336
  query: string,
199
337
  limit: number = 5,
200
338
  threshold: number = 0.3,
339
+ options?: {
340
+ include_confidential?: boolean;
341
+ include_archived?: boolean;
342
+ compress?: boolean;
343
+ max_context_tokens?: number;
344
+ project?: string;
345
+ tier?: string;
346
+ min_importance?: number;
347
+ },
201
348
  ): Promise<SearchResult[]> {
202
349
  const response = await this.request<{ data: SearchResult[] }>(
203
350
  "POST",
@@ -207,6 +354,7 @@ class MemoryRelayClient {
207
354
  limit,
208
355
  threshold,
209
356
  agent_id: this.agentId,
357
+ ...options,
210
358
  },
211
359
  );
212
360
  return response.data || [];
@@ -215,7 +363,7 @@ class MemoryRelayClient {
215
363
  async list(limit: number = 20, offset: number = 0): Promise<Memory[]> {
216
364
  const response = await this.request<{ data: Memory[] }>(
217
365
  "GET",
218
- `/v1/memories/memories?limit=${limit}&offset=${offset}`,
366
+ `/v1/memories?limit=${limit}&offset=${offset}&agent_id=${encodeURIComponent(this.agentId)}`,
219
367
  );
220
368
  return response.data || [];
221
369
  }
@@ -224,10 +372,354 @@ class MemoryRelayClient {
224
372
  return this.request<Memory>("GET", `/v1/memories/${id}`);
225
373
  }
226
374
 
375
+ async update(id: string, content: string, metadata?: Record<string, string>): Promise<Memory> {
376
+ return this.request<Memory>("PUT", `/v1/memories/${id}`, {
377
+ content,
378
+ metadata,
379
+ });
380
+ }
381
+
227
382
  async delete(id: string): Promise<void> {
228
383
  await this.request<void>("DELETE", `/v1/memories/${id}`);
229
384
  }
230
385
 
386
+ async batchStore(
387
+ memories: Array<{ content: string; metadata?: Record<string, string> }>,
388
+ ): Promise<any> {
389
+ return this.request("POST", "/v1/memories/batch", {
390
+ memories,
391
+ agent_id: this.agentId,
392
+ });
393
+ }
394
+
395
+ async buildContext(
396
+ query: string,
397
+ limit?: number,
398
+ threshold?: number,
399
+ maxTokens?: number,
400
+ project?: string,
401
+ ): Promise<any> {
402
+ return this.request("POST", "/v1/memories/context", {
403
+ query,
404
+ limit,
405
+ threshold,
406
+ max_tokens: maxTokens,
407
+ agent_id: this.agentId,
408
+ project,
409
+ });
410
+ }
411
+
412
+ async promote(memoryId: string, importance: number, tier?: string): Promise<any> {
413
+ return this.request("PUT", `/v1/memories/${memoryId}/importance`, {
414
+ importance,
415
+ tier,
416
+ });
417
+ }
418
+
419
+ // --------------------------------------------------------------------------
420
+ // Entity operations
421
+ // --------------------------------------------------------------------------
422
+
423
+ async createEntity(
424
+ name: string,
425
+ type: string,
426
+ metadata?: Record<string, string>,
427
+ ): Promise<any> {
428
+ return this.request("POST", "/v1/entities", {
429
+ name,
430
+ type,
431
+ metadata,
432
+ agent_id: this.agentId,
433
+ });
434
+ }
435
+
436
+ async linkEntity(
437
+ entityId: string,
438
+ memoryId: string,
439
+ relationship?: string,
440
+ ): Promise<any> {
441
+ return this.request("POST", `/v1/entities/links`, {
442
+ entity_id: entityId,
443
+ memory_id: memoryId,
444
+ relationship,
445
+ });
446
+ }
447
+
448
+ async listEntities(limit: number = 20, offset: number = 0): Promise<any> {
449
+ return this.request("GET", `/v1/entities?limit=${limit}&offset=${offset}`);
450
+ }
451
+
452
+ async entityGraph(
453
+ entityId: string,
454
+ depth: number = 2,
455
+ maxNeighbors: number = 10,
456
+ ): Promise<any> {
457
+ return this.request(
458
+ "GET",
459
+ `/v1/entities/${entityId}/neighborhood?depth=${depth}&max_neighbors=${maxNeighbors}`,
460
+ );
461
+ }
462
+
463
+ // --------------------------------------------------------------------------
464
+ // Agent operations
465
+ // --------------------------------------------------------------------------
466
+
467
+ async listAgents(limit: number = 20): Promise<any> {
468
+ return this.request("GET", `/v1/agents?limit=${limit}`);
469
+ }
470
+
471
+ async createAgent(name: string, description?: string): Promise<any> {
472
+ return this.request("POST", "/v1/agents", { name, description });
473
+ }
474
+
475
+ async getAgent(id: string): Promise<any> {
476
+ return this.request("GET", `/v1/agents/${id}`);
477
+ }
478
+
479
+ // --------------------------------------------------------------------------
480
+ // Session operations
481
+ // --------------------------------------------------------------------------
482
+
483
+ async startSession(
484
+ title?: string,
485
+ project?: string,
486
+ metadata?: Record<string, string>,
487
+ ): Promise<any> {
488
+ return this.request("POST", "/v1/sessions", {
489
+ title,
490
+ project,
491
+ metadata,
492
+ agent_id: this.agentId,
493
+ });
494
+ }
495
+
496
+ async endSession(id: string, summary?: string): Promise<any> {
497
+ return this.request("PUT", `/v1/sessions/${id}/end`, { summary });
498
+ }
499
+
500
+ async getSession(id: string): Promise<any> {
501
+ return this.request("GET", `/v1/sessions/${id}`);
502
+ }
503
+
504
+ async listSessions(
505
+ limit: number = 20,
506
+ project?: string,
507
+ status?: string,
508
+ ): Promise<any> {
509
+ let path = `/v1/sessions?limit=${limit}`;
510
+ if (project) path += `&project=${encodeURIComponent(project)}`;
511
+ if (status) path += `&status=${encodeURIComponent(status)}`;
512
+ return this.request("GET", path);
513
+ }
514
+
515
+ // --------------------------------------------------------------------------
516
+ // Decision operations
517
+ // --------------------------------------------------------------------------
518
+
519
+ async recordDecision(
520
+ title: string,
521
+ rationale: string,
522
+ alternatives?: string,
523
+ project?: string,
524
+ tags?: string[],
525
+ status?: string,
526
+ ): Promise<any> {
527
+ return this.request("POST", "/v1/decisions", {
528
+ title,
529
+ rationale,
530
+ alternatives,
531
+ project_slug: project,
532
+ tags,
533
+ status,
534
+ agent_id: this.agentId,
535
+ });
536
+ }
537
+
538
+ async listDecisions(
539
+ limit: number = 20,
540
+ project?: string,
541
+ status?: string,
542
+ tags?: string,
543
+ ): Promise<any> {
544
+ let path = `/v1/decisions?limit=${limit}`;
545
+ if (project) path += `&project=${encodeURIComponent(project)}`;
546
+ if (status) path += `&status=${encodeURIComponent(status)}`;
547
+ if (tags) path += `&tags=${encodeURIComponent(tags)}`;
548
+ return this.request("GET", path);
549
+ }
550
+
551
+ async supersedeDecision(
552
+ id: string,
553
+ title: string,
554
+ rationale: string,
555
+ alternatives?: string,
556
+ tags?: string[],
557
+ ): Promise<any> {
558
+ return this.request("POST", `/v1/decisions/${id}/supersede`, {
559
+ title,
560
+ rationale,
561
+ alternatives,
562
+ tags,
563
+ });
564
+ }
565
+
566
+ async checkDecisions(
567
+ query: string,
568
+ project?: string,
569
+ limit?: number,
570
+ threshold?: number,
571
+ includeSuperseded?: boolean,
572
+ ): Promise<any> {
573
+ const params = new URLSearchParams();
574
+ params.set("query", query);
575
+ if (project) params.set("project", project);
576
+ if (limit !== undefined) params.set("limit", String(limit));
577
+ if (threshold !== undefined) params.set("threshold", String(threshold));
578
+ if (includeSuperseded) params.set("include_superseded", "true");
579
+ return this.request("GET", `/v1/decisions/check?${params.toString()}`);
580
+ }
581
+
582
+ // --------------------------------------------------------------------------
583
+ // Pattern operations
584
+ // --------------------------------------------------------------------------
585
+
586
+ async createPattern(
587
+ title: string,
588
+ description: string,
589
+ category?: string,
590
+ exampleCode?: string,
591
+ scope?: string,
592
+ tags?: string[],
593
+ sourceProject?: string,
594
+ ): Promise<any> {
595
+ return this.request("POST", "/v1/patterns", {
596
+ title,
597
+ description,
598
+ category,
599
+ example_code: exampleCode,
600
+ scope,
601
+ tags,
602
+ source_project: sourceProject,
603
+ });
604
+ }
605
+
606
+ async searchPatterns(
607
+ query: string,
608
+ category?: string,
609
+ project?: string,
610
+ limit?: number,
611
+ threshold?: number,
612
+ ): Promise<any> {
613
+ const params = new URLSearchParams();
614
+ params.set("query", query);
615
+ if (category) params.set("category", category);
616
+ if (project) params.set("project", project);
617
+ if (limit !== undefined) params.set("limit", String(limit));
618
+ if (threshold !== undefined) params.set("threshold", String(threshold));
619
+ return this.request("GET", `/v1/patterns/search?${params.toString()}`);
620
+ }
621
+
622
+ async adoptPattern(id: string, project: string): Promise<any> {
623
+ return this.request("POST", `/v1/patterns/${id}/adopt`, { project });
624
+ }
625
+
626
+ async suggestPatterns(project: string, limit?: number): Promise<any> {
627
+ let path = `/v1/patterns/suggest?project=${encodeURIComponent(project)}`;
628
+ if (limit) path += `&limit=${limit}`;
629
+ return this.request("GET", path);
630
+ }
631
+
632
+ // --------------------------------------------------------------------------
633
+ // Project operations
634
+ // --------------------------------------------------------------------------
635
+
636
+ async registerProject(
637
+ slug: string,
638
+ name: string,
639
+ description?: string,
640
+ stack?: Record<string, unknown>,
641
+ repoUrl?: string,
642
+ ): Promise<any> {
643
+ return this.request("POST", "/v1/projects", {
644
+ slug,
645
+ name,
646
+ description,
647
+ stack,
648
+ repo_url: repoUrl,
649
+ });
650
+ }
651
+
652
+ async listProjects(limit: number = 20): Promise<any> {
653
+ return this.request("GET", `/v1/projects?limit=${limit}`);
654
+ }
655
+
656
+ async getProject(slug: string): Promise<any> {
657
+ return this.request("GET", `/v1/projects/${encodeURIComponent(slug)}`);
658
+ }
659
+
660
+ async addProjectRelationship(
661
+ from: string,
662
+ to: string,
663
+ type: string,
664
+ metadata?: Record<string, unknown>,
665
+ ): Promise<any> {
666
+ return this.request("POST", `/v1/projects/${encodeURIComponent(from)}/relationships`, {
667
+ target_project: to,
668
+ relationship_type: type,
669
+ metadata,
670
+ });
671
+ }
672
+
673
+ async getProjectDependencies(project: string): Promise<any> {
674
+ return this.request(
675
+ "GET",
676
+ `/v1/projects/${encodeURIComponent(project)}/dependencies`,
677
+ );
678
+ }
679
+
680
+ async getProjectDependents(project: string): Promise<any> {
681
+ return this.request(
682
+ "GET",
683
+ `/v1/projects/${encodeURIComponent(project)}/dependents`,
684
+ );
685
+ }
686
+
687
+ async getProjectRelated(project: string): Promise<any> {
688
+ return this.request(
689
+ "GET",
690
+ `/v1/projects/${encodeURIComponent(project)}/related`,
691
+ );
692
+ }
693
+
694
+ async projectImpact(project: string, changeDescription: string): Promise<any> {
695
+ return this.request(
696
+ "POST",
697
+ `/v1/projects/impact-analysis`,
698
+ { project, change_description: changeDescription },
699
+ );
700
+ }
701
+
702
+ async getSharedPatterns(projectA: string, projectB: string): Promise<any> {
703
+ const params = new URLSearchParams();
704
+ params.set("a", projectA);
705
+ params.set("b", projectB);
706
+ return this.request(
707
+ "GET",
708
+ `/v1/projects/shared-patterns?${params.toString()}`,
709
+ );
710
+ }
711
+
712
+ async getProjectContext(project: string): Promise<any> {
713
+ return this.request(
714
+ "GET",
715
+ `/v1/projects/${encodeURIComponent(project)}/context`,
716
+ );
717
+ }
718
+
719
+ // --------------------------------------------------------------------------
720
+ // Health & stats
721
+ // --------------------------------------------------------------------------
722
+
231
723
  async health(): Promise<{ status: string }> {
232
724
  return this.request<{ status: string }>("GET", "/v1/health");
233
725
  }
@@ -256,7 +748,7 @@ class MemoryRelayClient {
256
748
  if (batch.length === 0) break;
257
749
  allMemories.push(...batch);
258
750
  offset += limit;
259
- if (batch.length < limit) break; // Last page
751
+ if (batch.length < limit) break;
260
752
  }
261
753
 
262
754
  return allMemories;
@@ -291,7 +783,7 @@ function shouldCapture(text: string): boolean {
291
783
  export default async function plugin(api: OpenClawPluginApi): Promise<void> {
292
784
  const cfg = api.pluginConfig as MemoryRelayConfig | undefined;
293
785
 
294
- // NEW: Fall back to environment variables
786
+ // Fall back to environment variables
295
787
  const apiKey = cfg?.apiKey || process.env.MEMORYRELAY_API_KEY;
296
788
  const agentId = cfg?.agentId || process.env.MEMORYRELAY_AGENT_ID || api.agentName;
297
789
 
@@ -317,7 +809,33 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
317
809
  }
318
810
 
319
811
  const apiUrl = cfg?.apiUrl || process.env.MEMORYRELAY_API_URL || DEFAULT_API_URL;
320
- const client = new MemoryRelayClient(apiKey, agentId, apiUrl);
812
+ const defaultProject = cfg?.defaultProject || process.env.MEMORYRELAY_DEFAULT_PROJECT;
813
+
814
+ // ========================================================================
815
+ // Debug Logger and Status Reporter (v0.8.0)
816
+ // ========================================================================
817
+
818
+ const debugEnabled = cfg?.debug || false;
819
+ const verboseEnabled = cfg?.verbose || false;
820
+ const logFile = cfg?.logFile;
821
+ const maxLogEntries = cfg?.maxLogEntries || 100;
822
+
823
+ let debugLogger: DebugLogger | undefined;
824
+ let statusReporter: StatusReporter | undefined;
825
+
826
+ if (debugEnabled) {
827
+ debugLogger = new DebugLogger({
828
+ enabled: true,
829
+ verbose: verboseEnabled,
830
+ maxEntries: maxLogEntries,
831
+ logFile: logFile,
832
+ });
833
+ api.logger.info(`memory-memoryrelay: debug mode enabled (verbose: ${verboseEnabled}, maxEntries: ${maxLogEntries})`);
834
+ }
835
+
836
+ statusReporter = new StatusReporter(debugLogger);
837
+
838
+ const client = new MemoryRelayClient(apiKey, agentId, apiUrl, debugLogger, statusReporter);
321
839
 
322
840
  // Verify connection on startup (with timeout)
323
841
  try {
@@ -334,30 +852,87 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
334
852
 
335
853
  api.registerGatewayMethod?.("memory.status", async ({ respond }) => {
336
854
  try {
855
+ // Get connection status
856
+ const startTime = Date.now();
337
857
  const health = await client.health();
858
+ const responseTime = Date.now() - startTime;
859
+
860
+ const healthStatus = String(health.status).toLowerCase();
861
+ const isConnected = VALID_HEALTH_STATUSES.includes(healthStatus);
862
+
863
+ const connectionStatus = {
864
+ status: isConnected ? "connected" as const : "disconnected" as const,
865
+ endpoint: apiUrl,
866
+ lastCheck: new Date().toISOString(),
867
+ responseTime,
868
+ };
869
+
870
+ // Get memory stats
338
871
  let memoryCount = 0;
339
-
340
872
  try {
341
873
  const stats = await client.stats();
342
874
  memoryCount = stats.total_memories;
343
875
  } catch (statsErr) {
344
876
  api.logger.debug?.(`memory-memoryrelay: stats endpoint unavailable: ${String(statsErr)}`);
345
877
  }
346
-
347
- const healthStatus = String(health.status).toLowerCase();
348
- const isConnected = VALID_HEALTH_STATUSES.includes(healthStatus);
349
-
350
- respond(true, {
351
- available: true,
352
- connected: isConnected,
353
- endpoint: apiUrl,
354
- memoryCount: memoryCount,
878
+
879
+ const memoryStats = {
880
+ total_memories: memoryCount,
881
+ };
882
+
883
+ // Get config
884
+ const pluginConfig = {
355
885
  agentId: agentId,
356
- vector: {
886
+ autoRecall: cfg?.autoRecall ?? true,
887
+ autoCapture: cfg?.autoCapture ?? false,
888
+ recallLimit: cfg?.recallLimit ?? 5,
889
+ recallThreshold: cfg?.recallThreshold ?? 0.3,
890
+ excludeChannels: cfg?.excludeChannels ?? [],
891
+ defaultProject: defaultProject,
892
+ };
893
+
894
+ // Build comprehensive status report
895
+ if (statusReporter) {
896
+ const report = statusReporter.buildReport(
897
+ connectionStatus,
898
+ pluginConfig,
899
+ memoryStats,
900
+ TOOL_GROUPS,
901
+ );
902
+
903
+ // Format and output
904
+ const formatted = StatusReporter.formatReport(report);
905
+ api.logger.info(formatted);
906
+
907
+ // Also return structured data for programmatic access
908
+ respond(true, {
357
909
  available: true,
358
- enabled: true,
359
- },
360
- });
910
+ connected: isConnected,
911
+ endpoint: apiUrl,
912
+ memoryCount: memoryCount,
913
+ agentId: agentId,
914
+ debug: debugEnabled,
915
+ verbose: verboseEnabled,
916
+ report: report,
917
+ vector: {
918
+ available: true,
919
+ enabled: true,
920
+ },
921
+ });
922
+ } else {
923
+ // Fallback to simple status (shouldn't happen)
924
+ respond(true, {
925
+ available: true,
926
+ connected: isConnected,
927
+ endpoint: apiUrl,
928
+ memoryCount: memoryCount,
929
+ agentId: agentId,
930
+ vector: {
931
+ available: true,
932
+ enabled: true,
933
+ },
934
+ });
935
+ }
361
936
  } catch (err) {
362
937
  respond(true, {
363
938
  available: false,
@@ -374,204 +949,2136 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
374
949
  });
375
950
 
376
951
  // ========================================================================
377
- // Tools (using JSON Schema directly)
952
+ // Helper to check if a tool is enabled (by group)
378
953
  // ========================================================================
379
954
 
380
- // memory_store tool
381
- api.registerTool(
382
- {
383
- name: "memory_store",
384
- description:
385
- "Store a new memory in MemoryRelay. Use this to save important information, facts, preferences, or context that should be remembered for future conversations.",
386
- parameters: {
387
- type: "object",
388
- properties: {
389
- content: {
390
- type: "string",
391
- description: "The memory content to store. Be specific and include relevant context.",
392
- },
393
- metadata: {
394
- type: "object",
395
- description: "Optional key-value metadata to attach to the memory",
396
- additionalProperties: { type: "string" },
397
- },
398
- },
399
- required: ["content"],
400
- },
401
- execute: async (_id, { content, metadata }: { content: string; metadata?: Record<string, string> }) => {
402
- try {
403
- const memory = await client.store(content, metadata);
404
- return {
405
- content: [
406
- {
407
- type: "text",
408
- text: `Memory stored successfully (id: ${memory.id.slice(0, 8)}...)`,
409
- },
410
- ],
411
- details: { id: memory.id, stored: true },
412
- };
413
- } catch (err) {
414
- return {
415
- content: [{ type: "text", text: `Failed to store memory: ${String(err)}` }],
416
- details: { error: String(err) },
417
- };
955
+ // Tool group mapping — matches MCP server's TOOL_GROUPS
956
+ const TOOL_GROUPS: Record<string, string[]> = {
957
+ memory: [
958
+ "memory_store", "memory_recall", "memory_forget", "memory_list",
959
+ "memory_get", "memory_update", "memory_batch_store", "memory_context",
960
+ "memory_promote",
961
+ ],
962
+ entity: ["entity_create", "entity_link", "entity_list", "entity_graph"],
963
+ agent: ["agent_list", "agent_create", "agent_get"],
964
+ session: ["session_start", "session_end", "session_recall", "session_list"],
965
+ decision: ["decision_record", "decision_list", "decision_supersede", "decision_check"],
966
+ pattern: ["pattern_create", "pattern_search", "pattern_adopt", "pattern_suggest"],
967
+ project: [
968
+ "project_register", "project_list", "project_info",
969
+ "project_add_relationship", "project_dependencies", "project_dependents",
970
+ "project_related", "project_impact", "project_shared_patterns", "project_context",
971
+ ],
972
+ health: ["memory_health"],
973
+ };
974
+
975
+ // Build a set of enabled tool names from group names
976
+ const enabledToolNames: Set<string> | null = (() => {
977
+ if (!cfg?.enabledTools) return null; // all enabled
978
+ const groups = cfg.enabledTools.split(",").map((s) => s.trim().toLowerCase());
979
+ if (groups.includes("all")) return null;
980
+ const enabled = new Set<string>();
981
+ for (const group of groups) {
982
+ const tools = TOOL_GROUPS[group];
983
+ if (tools) {
984
+ for (const tool of tools) {
985
+ enabled.add(tool);
418
986
  }
419
- },
420
- },
421
- { name: "memory_store" },
422
- );
423
-
424
- // memory_recall tool (semantic search)
425
- api.registerTool(
426
- {
427
- name: "memory_recall",
428
- description:
429
- "Search memories using natural language. Returns the most relevant memories based on semantic similarity.",
430
- parameters: {
431
- type: "object",
432
- properties: {
433
- query: {
434
- type: "string",
435
- description: "Natural language search query",
436
- },
437
- limit: {
438
- type: "number",
439
- description: "Maximum results (1-20)",
440
- minimum: 1,
441
- maximum: 20,
442
- default: 5,
443
- },
444
- },
445
- required: ["query"],
446
- },
447
- execute: async (_id, { query, limit = 5 }: { query: string; limit?: number }) => {
448
- try {
449
- const results = await client.search(query, limit, cfg?.recallThreshold || 0.3);
987
+ }
988
+ }
989
+ return enabled;
990
+ })();
450
991
 
451
- if (results.length === 0) {
452
- return {
453
- content: [{ type: "text", text: "No relevant memories found." }],
454
- details: { count: 0 },
455
- };
456
- }
992
+ function isToolEnabled(name: string): boolean {
993
+ if (!enabledToolNames) return true;
994
+ return enabledToolNames.has(name);
995
+ }
457
996
 
458
- const formatted = results
459
- .map(
460
- (r) =>
461
- `- [${r.score.toFixed(2)}] ${r.memory.content.slice(0, 200)}${
462
- r.memory.content.length > 200 ? "..." : ""
463
- }`,
464
- )
465
- .join("\n");
997
+ // ========================================================================
998
+ // Tools (39 total)
999
+ // ========================================================================
466
1000
 
467
- return {
468
- content: [
469
- {
470
- type: "text",
471
- text: `Found ${results.length} relevant memories:\n${formatted}`,
472
- },
473
- ],
474
- details: {
475
- count: results.length,
476
- memories: results.map((r) => ({
477
- id: r.memory.id,
478
- content: r.memory.content,
479
- score: r.score,
480
- })),
1001
+ // --------------------------------------------------------------------------
1002
+ // 1. memory_store
1003
+ // --------------------------------------------------------------------------
1004
+ if (isToolEnabled("memory_store")) {
1005
+ api.registerTool(
1006
+ {
1007
+ name: "memory_store",
1008
+ description:
1009
+ "Store a new memory in MemoryRelay. Use this to save important information, facts, preferences, or context that should be remembered for future conversations." +
1010
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : "") +
1011
+ " Set deduplicate=true to avoid storing near-duplicate memories.",
1012
+ parameters: {
1013
+ type: "object",
1014
+ properties: {
1015
+ content: {
1016
+ type: "string",
1017
+ description: "The memory content to store. Be specific and include relevant context.",
1018
+ },
1019
+ metadata: {
1020
+ type: "object",
1021
+ description: "Optional key-value metadata to attach to the memory",
1022
+ additionalProperties: { type: "string" },
1023
+ },
1024
+ deduplicate: {
1025
+ type: "boolean",
1026
+ description: "If true, check for duplicate memories before storing. Default false.",
1027
+ },
1028
+ dedup_threshold: {
1029
+ type: "number",
1030
+ description: "Similarity threshold for deduplication (0-1). Default 0.9.",
1031
+ },
1032
+ project: {
1033
+ type: "string",
1034
+ description: "Project slug to associate with this memory.",
1035
+ },
1036
+ importance: {
1037
+ type: "number",
1038
+ description: "Importance score (0-1). Higher values are retained longer.",
1039
+ },
1040
+ tier: {
1041
+ type: "string",
1042
+ description: "Memory tier: hot, warm, or cold.",
1043
+ enum: ["hot", "warm", "cold"],
481
1044
  },
482
- };
483
- } catch (err) {
484
- return {
485
- content: [{ type: "text", text: `Search failed: ${String(err)}` }],
486
- details: { error: String(err) },
487
- };
488
- }
489
- },
490
- },
491
- { name: "memory_recall" },
492
- );
493
-
494
- // memory_forget tool
495
- api.registerTool(
496
- {
497
- name: "memory_forget",
498
- description: "Delete a memory by ID or search for memories to forget.",
499
- parameters: {
500
- type: "object",
501
- properties: {
502
- memoryId: {
503
- type: "string",
504
- description: "Memory ID to delete",
505
- },
506
- query: {
507
- type: "string",
508
- description: "Search query to find memory",
509
1045
  },
1046
+ required: ["content"],
510
1047
  },
511
- },
512
- execute: async (_id, { memoryId, query }: { memoryId?: string; query?: string }) => {
513
- if (memoryId) {
1048
+ execute: async (
1049
+ _id,
1050
+ args: {
1051
+ content: string;
1052
+ metadata?: Record<string, string>;
1053
+ deduplicate?: boolean;
1054
+ dedup_threshold?: number;
1055
+ project?: string;
1056
+ importance?: number;
1057
+ tier?: string;
1058
+ },
1059
+ ) => {
514
1060
  try {
515
- await client.delete(memoryId);
1061
+ const { content, metadata, ...opts } = args;
1062
+ if (!opts.project && defaultProject) opts.project = defaultProject;
1063
+ const memory = await client.store(content, metadata, opts);
516
1064
  return {
517
- content: [{ type: "text", text: `Memory ${memoryId.slice(0, 8)}... deleted.` }],
518
- details: { action: "deleted", id: memoryId },
1065
+ content: [
1066
+ {
1067
+ type: "text",
1068
+ text: `Memory stored successfully (id: ${memory.id.slice(0, 8)}...)`,
1069
+ },
1070
+ ],
1071
+ details: { id: memory.id, stored: true },
519
1072
  };
520
1073
  } catch (err) {
521
1074
  return {
522
- content: [{ type: "text", text: `Delete failed: ${String(err)}` }],
1075
+ content: [{ type: "text", text: `Failed to store memory: ${String(err)}` }],
523
1076
  details: { error: String(err) },
524
1077
  };
525
1078
  }
526
- }
1079
+ },
1080
+ },
1081
+ { name: "memory_store" },
1082
+ );
1083
+ }
1084
+
1085
+ // --------------------------------------------------------------------------
1086
+ // 2. memory_recall
1087
+ // --------------------------------------------------------------------------
1088
+ if (isToolEnabled("memory_recall")) {
1089
+ api.registerTool(
1090
+ {
1091
+ name: "memory_recall",
1092
+ description:
1093
+ "Search memories using natural language. Returns the most relevant memories based on semantic similarity to the query." +
1094
+ (defaultProject ? ` Results scoped to project '${defaultProject}' by default; pass project explicitly to override or omit to search all.` : ""),
1095
+ parameters: {
1096
+ type: "object",
1097
+ properties: {
1098
+ query: {
1099
+ type: "string",
1100
+ description: "Natural language search query",
1101
+ },
1102
+ limit: {
1103
+ type: "number",
1104
+ description: "Maximum results (1-50). Default 5.",
1105
+ minimum: 1,
1106
+ maximum: 50,
1107
+ },
1108
+ threshold: {
1109
+ type: "number",
1110
+ description: "Minimum similarity threshold (0-1). Default 0.3.",
1111
+ },
1112
+ project: {
1113
+ type: "string",
1114
+ description: "Filter by project slug.",
1115
+ },
1116
+ tier: {
1117
+ type: "string",
1118
+ description: "Filter by memory tier: hot, warm, or cold.",
1119
+ enum: ["hot", "warm", "cold"],
1120
+ },
1121
+ min_importance: {
1122
+ type: "number",
1123
+ description: "Minimum importance score filter (0-1).",
1124
+ },
1125
+ compress: {
1126
+ type: "boolean",
1127
+ description: "If true, compress results for token efficiency.",
1128
+ },
1129
+ },
1130
+ required: ["query"],
1131
+ },
1132
+ execute: async (
1133
+ _id,
1134
+ args: {
1135
+ query: string;
1136
+ limit?: number;
1137
+ threshold?: number;
1138
+ project?: string;
1139
+ tier?: string;
1140
+ min_importance?: number;
1141
+ compress?: boolean;
1142
+ },
1143
+ ) => {
1144
+ try {
1145
+ const {
1146
+ query,
1147
+ limit = 5,
1148
+ threshold,
1149
+ project,
1150
+ tier,
1151
+ min_importance,
1152
+ compress,
1153
+ } = args;
1154
+ const searchThreshold = threshold ?? cfg?.recallThreshold ?? 0.3;
1155
+ const searchProject = project ?? defaultProject;
1156
+ const results = await client.search(query, limit, searchThreshold, {
1157
+ project: searchProject,
1158
+ tier,
1159
+ min_importance,
1160
+ compress,
1161
+ });
1162
+
1163
+ if (results.length === 0) {
1164
+ return {
1165
+ content: [{ type: "text", text: "No relevant memories found." }],
1166
+ details: { count: 0 },
1167
+ };
1168
+ }
527
1169
 
528
- if (query) {
529
- const results = await client.search(query, 5, 0.5);
1170
+ const formatted = results
1171
+ .map(
1172
+ (r) =>
1173
+ `- [${r.score.toFixed(2)}] ${r.memory.content.slice(0, 200)}${
1174
+ r.memory.content.length > 200 ? "..." : ""
1175
+ }`,
1176
+ )
1177
+ .join("\n");
530
1178
 
531
- if (results.length === 0) {
532
1179
  return {
533
- content: [{ type: "text", text: "No matching memories found." }],
534
- details: { count: 0 },
1180
+ content: [
1181
+ {
1182
+ type: "text",
1183
+ text: `Found ${results.length} relevant memories:\n${formatted}`,
1184
+ },
1185
+ ],
1186
+ details: {
1187
+ count: results.length,
1188
+ memories: results.map((r) => ({
1189
+ id: r.memory.id,
1190
+ content: r.memory.content,
1191
+ score: r.score,
1192
+ })),
1193
+ },
1194
+ };
1195
+ } catch (err) {
1196
+ return {
1197
+ content: [{ type: "text", text: `Search failed: ${String(err)}` }],
1198
+ details: { error: String(err) },
535
1199
  };
536
1200
  }
1201
+ },
1202
+ },
1203
+ { name: "memory_recall" },
1204
+ );
1205
+ }
1206
+
1207
+ // --------------------------------------------------------------------------
1208
+ // 3. memory_forget
1209
+ // --------------------------------------------------------------------------
1210
+ if (isToolEnabled("memory_forget")) {
1211
+ api.registerTool(
1212
+ {
1213
+ name: "memory_forget",
1214
+ description: "Delete a memory by ID, or search by query to find candidates. Provide memoryId for direct deletion, or query to search first. A single high-confidence match (>0.9) is auto-deleted; otherwise candidates are listed for you to choose.",
1215
+ parameters: {
1216
+ type: "object",
1217
+ properties: {
1218
+ memoryId: {
1219
+ type: "string",
1220
+ description: "Memory ID to delete",
1221
+ },
1222
+ query: {
1223
+ type: "string",
1224
+ description: "Search query to find memory",
1225
+ },
1226
+ },
1227
+ },
1228
+ execute: async (_id, { memoryId, query }: { memoryId?: string; query?: string }) => {
1229
+ if (memoryId) {
1230
+ try {
1231
+ await client.delete(memoryId);
1232
+ return {
1233
+ content: [{ type: "text", text: `Memory ${memoryId.slice(0, 8)}... deleted.` }],
1234
+ details: { action: "deleted", id: memoryId },
1235
+ };
1236
+ } catch (err) {
1237
+ return {
1238
+ content: [{ type: "text", text: `Delete failed: ${String(err)}` }],
1239
+ details: { error: String(err) },
1240
+ };
1241
+ }
1242
+ }
1243
+
1244
+ if (query) {
1245
+ const results = await client.search(query, 5, 0.5, { project: defaultProject });
1246
+
1247
+ if (results.length === 0) {
1248
+ return {
1249
+ content: [{ type: "text", text: "No matching memories found." }],
1250
+ details: { count: 0 },
1251
+ };
1252
+ }
1253
+
1254
+ // If single high-confidence match, delete it
1255
+ if (results.length === 1 && results[0].score > 0.9) {
1256
+ await client.delete(results[0].memory.id);
1257
+ return {
1258
+ content: [
1259
+ { type: "text", text: `Forgotten: "${results[0].memory.content.slice(0, 60)}..."` },
1260
+ ],
1261
+ details: { action: "deleted", id: results[0].memory.id },
1262
+ };
1263
+ }
1264
+
1265
+ const list = results
1266
+ .map((r) => `- [${r.memory.id.slice(0, 8)}] ${r.memory.content.slice(0, 60)}...`)
1267
+ .join("\n");
537
1268
 
538
- // If single high-confidence match, delete it
539
- if (results.length === 1 && results[0].score > 0.9) {
540
- await client.delete(results[0].memory.id);
541
1269
  return {
542
1270
  content: [
543
- { type: "text", text: `Forgotten: "${results[0].memory.content.slice(0, 60)}..."` },
1271
+ {
1272
+ type: "text",
1273
+ text: `Found ${results.length} candidates. Specify memoryId:\n${list}`,
1274
+ },
544
1275
  ],
545
- details: { action: "deleted", id: results[0].memory.id },
1276
+ details: { action: "candidates", count: results.length },
546
1277
  };
547
1278
  }
548
1279
 
549
- const list = results
550
- .map((r) => `- [${r.memory.id.slice(0, 8)}] ${r.memory.content.slice(0, 60)}...`)
551
- .join("\n");
552
-
553
1280
  return {
554
- content: [
555
- {
556
- type: "text",
557
- text: `Found ${results.length} candidates. Specify memoryId:\n${list}`,
558
- },
559
- ],
560
- details: { action: "candidates", count: results.length },
1281
+ content: [{ type: "text", text: "Provide query or memoryId." }],
1282
+ details: { error: "missing_param" },
561
1283
  };
562
- }
563
-
564
- return {
565
- content: [{ type: "text", text: "Provide query or memoryId." }],
566
- details: { error: "missing_param" },
567
- };
1284
+ },
1285
+ },
1286
+ { name: "memory_forget" },
1287
+ );
1288
+ }
1289
+
1290
+ // --------------------------------------------------------------------------
1291
+ // 4. memory_list
1292
+ // --------------------------------------------------------------------------
1293
+ if (isToolEnabled("memory_list")) {
1294
+ api.registerTool(
1295
+ {
1296
+ name: "memory_list",
1297
+ description: "List recent memories chronologically for this agent. Use to review what has been stored or to find memory IDs for update/delete operations.",
1298
+ parameters: {
1299
+ type: "object",
1300
+ properties: {
1301
+ limit: {
1302
+ type: "number",
1303
+ description: "Number of memories to return (1-100). Default 20.",
1304
+ minimum: 1,
1305
+ maximum: 100,
1306
+ },
1307
+ offset: {
1308
+ type: "number",
1309
+ description: "Offset for pagination. Default 0.",
1310
+ minimum: 0,
1311
+ },
1312
+ },
1313
+ },
1314
+ execute: async (_id, args: { limit?: number; offset?: number }) => {
1315
+ try {
1316
+ const memories = await client.list(args.limit ?? 20, args.offset ?? 0);
1317
+ if (memories.length === 0) {
1318
+ return {
1319
+ content: [{ type: "text", text: "No memories found." }],
1320
+ details: { count: 0 },
1321
+ };
1322
+ }
1323
+ const formatted = memories
1324
+ .map((m) => `- [${m.id.slice(0, 8)}] ${m.content.slice(0, 120)}`)
1325
+ .join("\n");
1326
+ return {
1327
+ content: [{ type: "text", text: `${memories.length} memories:\n${formatted}` }],
1328
+ details: { count: memories.length, memories },
1329
+ };
1330
+ } catch (err) {
1331
+ return {
1332
+ content: [{ type: "text", text: `Failed to list memories: ${String(err)}` }],
1333
+ details: { error: String(err) },
1334
+ };
1335
+ }
1336
+ },
1337
+ },
1338
+ { name: "memory_list" },
1339
+ );
1340
+ }
1341
+
1342
+ // --------------------------------------------------------------------------
1343
+ // 5. memory_get
1344
+ // --------------------------------------------------------------------------
1345
+ if (isToolEnabled("memory_get")) {
1346
+ api.registerTool(
1347
+ {
1348
+ name: "memory_get",
1349
+ description: "Retrieve a specific memory by its ID.",
1350
+ parameters: {
1351
+ type: "object",
1352
+ properties: {
1353
+ id: {
1354
+ type: "string",
1355
+ description: "The memory ID (UUID) to retrieve.",
1356
+ },
1357
+ },
1358
+ required: ["id"],
1359
+ },
1360
+ execute: async (_id, args: { id: string }) => {
1361
+ try {
1362
+ const memory = await client.get(args.id);
1363
+ return {
1364
+ content: [{ type: "text", text: JSON.stringify(memory, null, 2) }],
1365
+ details: { memory },
1366
+ };
1367
+ } catch (err) {
1368
+ return {
1369
+ content: [{ type: "text", text: `Failed to get memory: ${String(err)}` }],
1370
+ details: { error: String(err) },
1371
+ };
1372
+ }
1373
+ },
1374
+ },
1375
+ { name: "memory_get" },
1376
+ );
1377
+ }
1378
+
1379
+ // --------------------------------------------------------------------------
1380
+ // 6. memory_update
1381
+ // --------------------------------------------------------------------------
1382
+ if (isToolEnabled("memory_update")) {
1383
+ api.registerTool(
1384
+ {
1385
+ name: "memory_update",
1386
+ description: "Update the content of an existing memory. Use to correct or expand stored information.",
1387
+ parameters: {
1388
+ type: "object",
1389
+ properties: {
1390
+ id: {
1391
+ type: "string",
1392
+ description: "The memory ID (UUID) to update.",
1393
+ },
1394
+ content: {
1395
+ type: "string",
1396
+ description: "The new content to replace the existing memory.",
1397
+ },
1398
+ metadata: {
1399
+ type: "object",
1400
+ description: "Updated metadata (replaces existing).",
1401
+ additionalProperties: { type: "string" },
1402
+ },
1403
+ },
1404
+ required: ["id", "content"],
1405
+ },
1406
+ execute: async (_id, args: { id: string; content: string; metadata?: Record<string, string> }) => {
1407
+ try {
1408
+ const memory = await client.update(args.id, args.content, args.metadata);
1409
+ return {
1410
+ content: [{ type: "text", text: `Memory ${args.id.slice(0, 8)}... updated.` }],
1411
+ details: { id: memory.id, updated: true },
1412
+ };
1413
+ } catch (err) {
1414
+ return {
1415
+ content: [{ type: "text", text: `Failed to update memory: ${String(err)}` }],
1416
+ details: { error: String(err) },
1417
+ };
1418
+ }
1419
+ },
1420
+ },
1421
+ { name: "memory_update" },
1422
+ );
1423
+ }
1424
+
1425
+ // --------------------------------------------------------------------------
1426
+ // 7. memory_batch_store
1427
+ // --------------------------------------------------------------------------
1428
+ if (isToolEnabled("memory_batch_store")) {
1429
+ api.registerTool(
1430
+ {
1431
+ name: "memory_batch_store",
1432
+ description: "Store multiple memories at once. More efficient than individual calls for bulk storage.",
1433
+ parameters: {
1434
+ type: "object",
1435
+ properties: {
1436
+ memories: {
1437
+ type: "array",
1438
+ description: "Array of memories to store.",
1439
+ items: {
1440
+ type: "object",
1441
+ properties: {
1442
+ content: { type: "string", description: "Memory content." },
1443
+ metadata: {
1444
+ type: "object",
1445
+ description: "Optional metadata.",
1446
+ additionalProperties: { type: "string" },
1447
+ },
1448
+ },
1449
+ required: ["content"],
1450
+ },
1451
+ },
1452
+ },
1453
+ required: ["memories"],
1454
+ },
1455
+ execute: async (
1456
+ _id,
1457
+ args: { memories: Array<{ content: string; metadata?: Record<string, string> }> },
1458
+ ) => {
1459
+ try {
1460
+ const result = await client.batchStore(args.memories);
1461
+ return {
1462
+ content: [
1463
+ {
1464
+ type: "text",
1465
+ text: `Batch stored ${args.memories.length} memories successfully.`,
1466
+ },
1467
+ ],
1468
+ details: { count: args.memories.length, result },
1469
+ };
1470
+ } catch (err) {
1471
+ return {
1472
+ content: [{ type: "text", text: `Batch store failed: ${String(err)}` }],
1473
+ details: { error: String(err) },
1474
+ };
1475
+ }
1476
+ },
1477
+ },
1478
+ { name: "memory_batch_store" },
1479
+ );
1480
+ }
1481
+
1482
+ // --------------------------------------------------------------------------
1483
+ // 8. memory_context
1484
+ // --------------------------------------------------------------------------
1485
+ if (isToolEnabled("memory_context")) {
1486
+ api.registerTool(
1487
+ {
1488
+ name: "memory_context",
1489
+ description:
1490
+ "Build a context window from relevant memories, optimized for injecting into agent prompts with token budget awareness." +
1491
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : ""),
1492
+ parameters: {
1493
+ type: "object",
1494
+ properties: {
1495
+ query: {
1496
+ type: "string",
1497
+ description: "The query to build context around.",
1498
+ },
1499
+ limit: {
1500
+ type: "number",
1501
+ description: "Maximum number of memories to include.",
1502
+ },
1503
+ threshold: {
1504
+ type: "number",
1505
+ description: "Minimum similarity threshold (0-1).",
1506
+ },
1507
+ max_tokens: {
1508
+ type: "number",
1509
+ description: "Maximum token budget for the context.",
1510
+ },
1511
+ project: {
1512
+ type: "string",
1513
+ description: "Project slug to scope the context.",
1514
+ },
1515
+ },
1516
+ required: ["query"],
1517
+ },
1518
+ execute: async (
1519
+ _id,
1520
+ args: { query: string; limit?: number; threshold?: number; max_tokens?: number; project?: string },
1521
+ ) => {
1522
+ try {
1523
+ const project = args.project ?? defaultProject;
1524
+ const result = await client.buildContext(
1525
+ args.query,
1526
+ args.limit,
1527
+ args.threshold,
1528
+ args.max_tokens,
1529
+ project,
1530
+ );
1531
+ return {
1532
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1533
+ details: { result },
1534
+ };
1535
+ } catch (err) {
1536
+ return {
1537
+ content: [{ type: "text", text: `Context build failed: ${String(err)}` }],
1538
+ details: { error: String(err) },
1539
+ };
1540
+ }
1541
+ },
1542
+ },
1543
+ { name: "memory_context" },
1544
+ );
1545
+ }
1546
+
1547
+ // --------------------------------------------------------------------------
1548
+ // 9. memory_promote
1549
+ // --------------------------------------------------------------------------
1550
+ if (isToolEnabled("memory_promote")) {
1551
+ api.registerTool(
1552
+ {
1553
+ name: "memory_promote",
1554
+ description:
1555
+ "Promote a memory by updating its importance score and/or tier. Use to ensure critical memories are retained longer.",
1556
+ parameters: {
1557
+ type: "object",
1558
+ properties: {
1559
+ memory_id: {
1560
+ type: "string",
1561
+ description: "The memory ID to promote.",
1562
+ },
1563
+ importance: {
1564
+ type: "number",
1565
+ description: "New importance score (0-1).",
1566
+ minimum: 0,
1567
+ maximum: 1,
1568
+ },
1569
+ tier: {
1570
+ type: "string",
1571
+ description: "Target tier: hot, warm, or cold.",
1572
+ enum: ["hot", "warm", "cold"],
1573
+ },
1574
+ },
1575
+ required: ["memory_id", "importance"],
1576
+ },
1577
+ execute: async (_id, args: { memory_id: string; importance: number; tier?: string }) => {
1578
+ try {
1579
+ const result = await client.promote(args.memory_id, args.importance, args.tier);
1580
+ return {
1581
+ content: [
1582
+ {
1583
+ type: "text",
1584
+ text: `Memory ${args.memory_id.slice(0, 8)}... promoted (importance: ${args.importance}${args.tier ? `, tier: ${args.tier}` : ""}).`,
1585
+ },
1586
+ ],
1587
+ details: { result },
1588
+ };
1589
+ } catch (err) {
1590
+ return {
1591
+ content: [{ type: "text", text: `Promote failed: ${String(err)}` }],
1592
+ details: { error: String(err) },
1593
+ };
1594
+ }
1595
+ },
1596
+ },
1597
+ { name: "memory_promote" },
1598
+ );
1599
+ }
1600
+
1601
+ // --------------------------------------------------------------------------
1602
+ // 10. entity_create
1603
+ // --------------------------------------------------------------------------
1604
+ if (isToolEnabled("entity_create")) {
1605
+ api.registerTool(
1606
+ {
1607
+ name: "entity_create",
1608
+ description:
1609
+ "Create a named entity (person, place, organization, project, concept) for the knowledge graph. Entities help organize and connect memories.",
1610
+ parameters: {
1611
+ type: "object",
1612
+ properties: {
1613
+ name: {
1614
+ type: "string",
1615
+ description: "Entity name (1-200 characters).",
1616
+ },
1617
+ type: {
1618
+ type: "string",
1619
+ description: "Entity type classification.",
1620
+ enum: ["person", "place", "organization", "project", "concept", "other"],
1621
+ },
1622
+ metadata: {
1623
+ type: "object",
1624
+ description: "Optional key-value metadata.",
1625
+ additionalProperties: { type: "string" },
1626
+ },
1627
+ },
1628
+ required: ["name", "type"],
1629
+ },
1630
+ execute: async (
1631
+ _id,
1632
+ args: { name: string; type: string; metadata?: Record<string, string> },
1633
+ ) => {
1634
+ try {
1635
+ const result = await client.createEntity(args.name, args.type, args.metadata);
1636
+ return {
1637
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1638
+ details: { result },
1639
+ };
1640
+ } catch (err) {
1641
+ return {
1642
+ content: [{ type: "text", text: `Failed to create entity: ${String(err)}` }],
1643
+ details: { error: String(err) },
1644
+ };
1645
+ }
1646
+ },
1647
+ },
1648
+ { name: "entity_create" },
1649
+ );
1650
+ }
1651
+
1652
+ // --------------------------------------------------------------------------
1653
+ // 11. entity_link
1654
+ // --------------------------------------------------------------------------
1655
+ if (isToolEnabled("entity_link")) {
1656
+ api.registerTool(
1657
+ {
1658
+ name: "entity_link",
1659
+ description: "Link an entity to a memory to establish relationships in the knowledge graph.",
1660
+ parameters: {
1661
+ type: "object",
1662
+ properties: {
1663
+ entity_id: {
1664
+ type: "string",
1665
+ description: "Entity UUID.",
1666
+ },
1667
+ memory_id: {
1668
+ type: "string",
1669
+ description: "Memory UUID.",
1670
+ },
1671
+ relationship: {
1672
+ type: "string",
1673
+ description:
1674
+ 'Relationship type (e.g., "mentioned_in", "created_by", "relates_to"). Default "mentioned_in".',
1675
+ },
1676
+ },
1677
+ required: ["entity_id", "memory_id"],
1678
+ },
1679
+ execute: async (
1680
+ _id,
1681
+ args: { entity_id: string; memory_id: string; relationship?: string },
1682
+ ) => {
1683
+ try {
1684
+ const result = await client.linkEntity(
1685
+ args.entity_id,
1686
+ args.memory_id,
1687
+ args.relationship,
1688
+ );
1689
+ return {
1690
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1691
+ details: { result },
1692
+ };
1693
+ } catch (err) {
1694
+ return {
1695
+ content: [{ type: "text", text: `Failed to link entity: ${String(err)}` }],
1696
+ details: { error: String(err) },
1697
+ };
1698
+ }
1699
+ },
1700
+ },
1701
+ { name: "entity_link" },
1702
+ );
1703
+ }
1704
+
1705
+ // --------------------------------------------------------------------------
1706
+ // 12. entity_list
1707
+ // --------------------------------------------------------------------------
1708
+ if (isToolEnabled("entity_list")) {
1709
+ api.registerTool(
1710
+ {
1711
+ name: "entity_list",
1712
+ description: "List entities in the knowledge graph.",
1713
+ parameters: {
1714
+ type: "object",
1715
+ properties: {
1716
+ limit: {
1717
+ type: "number",
1718
+ description: "Maximum entities to return. Default 20.",
1719
+ minimum: 1,
1720
+ maximum: 100,
1721
+ },
1722
+ offset: {
1723
+ type: "number",
1724
+ description: "Offset for pagination. Default 0.",
1725
+ minimum: 0,
1726
+ },
1727
+ },
1728
+ },
1729
+ execute: async (_id, args: { limit?: number; offset?: number }) => {
1730
+ try {
1731
+ const result = await client.listEntities(args.limit, args.offset);
1732
+ return {
1733
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1734
+ details: { result },
1735
+ };
1736
+ } catch (err) {
1737
+ return {
1738
+ content: [{ type: "text", text: `Failed to list entities: ${String(err)}` }],
1739
+ details: { error: String(err) },
1740
+ };
1741
+ }
1742
+ },
1743
+ },
1744
+ { name: "entity_list" },
1745
+ );
1746
+ }
1747
+
1748
+ // --------------------------------------------------------------------------
1749
+ // 13. entity_graph
1750
+ // --------------------------------------------------------------------------
1751
+ if (isToolEnabled("entity_graph")) {
1752
+ api.registerTool(
1753
+ {
1754
+ name: "entity_graph",
1755
+ description:
1756
+ "Explore the knowledge graph around an entity. Returns the entity and its neighborhood of connected entities and memories.",
1757
+ parameters: {
1758
+ type: "object",
1759
+ properties: {
1760
+ entity_id: {
1761
+ type: "string",
1762
+ description: "Entity UUID to explore from.",
1763
+ },
1764
+ depth: {
1765
+ type: "number",
1766
+ description: "How many hops to traverse. Default 2.",
1767
+ minimum: 1,
1768
+ maximum: 5,
1769
+ },
1770
+ max_neighbors: {
1771
+ type: "number",
1772
+ description: "Maximum neighbors per node. Default 10.",
1773
+ minimum: 1,
1774
+ maximum: 50,
1775
+ },
1776
+ },
1777
+ required: ["entity_id"],
1778
+ },
1779
+ execute: async (
1780
+ _id,
1781
+ args: { entity_id: string; depth?: number; max_neighbors?: number },
1782
+ ) => {
1783
+ try {
1784
+ const result = await client.entityGraph(
1785
+ args.entity_id,
1786
+ args.depth,
1787
+ args.max_neighbors,
1788
+ );
1789
+ return {
1790
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1791
+ details: { result },
1792
+ };
1793
+ } catch (err) {
1794
+ return {
1795
+ content: [{ type: "text", text: `Failed to get entity graph: ${String(err)}` }],
1796
+ details: { error: String(err) },
1797
+ };
1798
+ }
1799
+ },
1800
+ },
1801
+ { name: "entity_graph" },
1802
+ );
1803
+ }
1804
+
1805
+ // --------------------------------------------------------------------------
1806
+ // 14. agent_list
1807
+ // --------------------------------------------------------------------------
1808
+ if (isToolEnabled("agent_list")) {
1809
+ api.registerTool(
1810
+ {
1811
+ name: "agent_list",
1812
+ description: "List available agents.",
1813
+ parameters: {
1814
+ type: "object",
1815
+ properties: {
1816
+ limit: {
1817
+ type: "number",
1818
+ description: "Maximum agents to return. Default 20.",
1819
+ minimum: 1,
1820
+ maximum: 100,
1821
+ },
1822
+ },
1823
+ },
1824
+ execute: async (_id, args: { limit?: number }) => {
1825
+ try {
1826
+ const result = await client.listAgents(args.limit);
1827
+ return {
1828
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1829
+ details: { result },
1830
+ };
1831
+ } catch (err) {
1832
+ return {
1833
+ content: [{ type: "text", text: `Failed to list agents: ${String(err)}` }],
1834
+ details: { error: String(err) },
1835
+ };
1836
+ }
1837
+ },
1838
+ },
1839
+ { name: "agent_list" },
1840
+ );
1841
+ }
1842
+
1843
+ // --------------------------------------------------------------------------
1844
+ // 15. agent_create
1845
+ // --------------------------------------------------------------------------
1846
+ if (isToolEnabled("agent_create")) {
1847
+ api.registerTool(
1848
+ {
1849
+ name: "agent_create",
1850
+ description: "Create a new agent. Agents serve as memory namespaces and isolation boundaries.",
1851
+ parameters: {
1852
+ type: "object",
1853
+ properties: {
1854
+ name: {
1855
+ type: "string",
1856
+ description: "Agent name.",
1857
+ },
1858
+ description: {
1859
+ type: "string",
1860
+ description: "Optional agent description.",
1861
+ },
1862
+ },
1863
+ required: ["name"],
1864
+ },
1865
+ execute: async (_id, args: { name: string; description?: string }) => {
1866
+ try {
1867
+ const result = await client.createAgent(args.name, args.description);
1868
+ return {
1869
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1870
+ details: { result },
1871
+ };
1872
+ } catch (err) {
1873
+ return {
1874
+ content: [{ type: "text", text: `Failed to create agent: ${String(err)}` }],
1875
+ details: { error: String(err) },
1876
+ };
1877
+ }
1878
+ },
1879
+ },
1880
+ { name: "agent_create" },
1881
+ );
1882
+ }
1883
+
1884
+ // --------------------------------------------------------------------------
1885
+ // 16. agent_get
1886
+ // --------------------------------------------------------------------------
1887
+ if (isToolEnabled("agent_get")) {
1888
+ api.registerTool(
1889
+ {
1890
+ name: "agent_get",
1891
+ description: "Get details about a specific agent by ID.",
1892
+ parameters: {
1893
+ type: "object",
1894
+ properties: {
1895
+ id: {
1896
+ type: "string",
1897
+ description: "Agent UUID.",
1898
+ },
1899
+ },
1900
+ required: ["id"],
1901
+ },
1902
+ execute: async (_id, args: { id: string }) => {
1903
+ try {
1904
+ const result = await client.getAgent(args.id);
1905
+ return {
1906
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1907
+ details: { result },
1908
+ };
1909
+ } catch (err) {
1910
+ return {
1911
+ content: [{ type: "text", text: `Failed to get agent: ${String(err)}` }],
1912
+ details: { error: String(err) },
1913
+ };
1914
+ }
1915
+ },
1916
+ },
1917
+ { name: "agent_get" },
1918
+ );
1919
+ }
1920
+
1921
+ // --------------------------------------------------------------------------
1922
+ // 17. session_start
1923
+ // --------------------------------------------------------------------------
1924
+ if (isToolEnabled("session_start")) {
1925
+ api.registerTool(
1926
+ {
1927
+ name: "session_start",
1928
+ description:
1929
+ "Start a new work session. Sessions track the lifecycle of a task or conversation for later review. Call this early in your workflow and save the returned session ID for session_end later." +
1930
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : ""),
1931
+ parameters: {
1932
+ type: "object",
1933
+ properties: {
1934
+ title: {
1935
+ type: "string",
1936
+ description: "Session title describing the goal or task.",
1937
+ },
1938
+ project: {
1939
+ type: "string",
1940
+ description: "Project slug to associate this session with.",
1941
+ },
1942
+ metadata: {
1943
+ type: "object",
1944
+ description: "Optional key-value metadata.",
1945
+ additionalProperties: { type: "string" },
1946
+ },
1947
+ },
1948
+ },
1949
+ execute: async (
1950
+ _id,
1951
+ args: { title?: string; project?: string; metadata?: Record<string, string> },
1952
+ ) => {
1953
+ try {
1954
+ const project = args.project ?? defaultProject;
1955
+ const result = await client.startSession(args.title, project, args.metadata);
1956
+ return {
1957
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1958
+ details: { result },
1959
+ };
1960
+ } catch (err) {
1961
+ return {
1962
+ content: [{ type: "text", text: `Failed to start session: ${String(err)}` }],
1963
+ details: { error: String(err) },
1964
+ };
1965
+ }
1966
+ },
1967
+ },
1968
+ { name: "session_start" },
1969
+ );
1970
+ }
1971
+
1972
+ // --------------------------------------------------------------------------
1973
+ // 18. session_end
1974
+ // --------------------------------------------------------------------------
1975
+ if (isToolEnabled("session_end")) {
1976
+ api.registerTool(
1977
+ {
1978
+ name: "session_end",
1979
+ description: "End an active session with a summary of what was accomplished. Always include a meaningful summary — it serves as the historical record of the session.",
1980
+ parameters: {
1981
+ type: "object",
1982
+ properties: {
1983
+ id: {
1984
+ type: "string",
1985
+ description: "Session ID to end.",
1986
+ },
1987
+ summary: {
1988
+ type: "string",
1989
+ description: "Summary of what was accomplished during this session.",
1990
+ },
1991
+ },
1992
+ required: ["id"],
1993
+ },
1994
+ execute: async (_id, args: { id: string; summary?: string }) => {
1995
+ try {
1996
+ const result = await client.endSession(args.id, args.summary);
1997
+ return {
1998
+ content: [{ type: "text", text: `Session ${args.id.slice(0, 8)}... ended.` }],
1999
+ details: { result },
2000
+ };
2001
+ } catch (err) {
2002
+ return {
2003
+ content: [{ type: "text", text: `Failed to end session: ${String(err)}` }],
2004
+ details: { error: String(err) },
2005
+ };
2006
+ }
2007
+ },
2008
+ },
2009
+ { name: "session_end" },
2010
+ );
2011
+ }
2012
+
2013
+ // --------------------------------------------------------------------------
2014
+ // 19. session_recall
2015
+ // --------------------------------------------------------------------------
2016
+ if (isToolEnabled("session_recall")) {
2017
+ api.registerTool(
2018
+ {
2019
+ name: "session_recall",
2020
+ description: "Retrieve details of a specific session including its timeline and associated memories.",
2021
+ parameters: {
2022
+ type: "object",
2023
+ properties: {
2024
+ id: {
2025
+ type: "string",
2026
+ description: "Session ID to retrieve.",
2027
+ },
2028
+ },
2029
+ required: ["id"],
2030
+ },
2031
+ execute: async (_id, args: { id: string }) => {
2032
+ try {
2033
+ const result = await client.getSession(args.id);
2034
+ return {
2035
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2036
+ details: { result },
2037
+ };
2038
+ } catch (err) {
2039
+ return {
2040
+ content: [{ type: "text", text: `Failed to recall session: ${String(err)}` }],
2041
+ details: { error: String(err) },
2042
+ };
2043
+ }
2044
+ },
2045
+ },
2046
+ { name: "session_recall" },
2047
+ );
2048
+ }
2049
+
2050
+ // --------------------------------------------------------------------------
2051
+ // 20. session_list
2052
+ // --------------------------------------------------------------------------
2053
+ if (isToolEnabled("session_list")) {
2054
+ api.registerTool(
2055
+ {
2056
+ name: "session_list",
2057
+ description: "List sessions, optionally filtered by project or status." +
2058
+ (defaultProject ? ` Scoped to project '${defaultProject}' by default.` : ""),
2059
+ parameters: {
2060
+ type: "object",
2061
+ properties: {
2062
+ limit: {
2063
+ type: "number",
2064
+ description: "Maximum sessions to return. Default 20.",
2065
+ minimum: 1,
2066
+ maximum: 100,
2067
+ },
2068
+ project: {
2069
+ type: "string",
2070
+ description: "Filter by project slug.",
2071
+ },
2072
+ status: {
2073
+ type: "string",
2074
+ description: "Filter by status (active, ended).",
2075
+ enum: ["active", "ended"],
2076
+ },
2077
+ },
2078
+ },
2079
+ execute: async (
2080
+ _id,
2081
+ args: { limit?: number; project?: string; status?: string },
2082
+ ) => {
2083
+ try {
2084
+ const project = args.project ?? defaultProject;
2085
+ const result = await client.listSessions(args.limit, project, args.status);
2086
+ return {
2087
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2088
+ details: { result },
2089
+ };
2090
+ } catch (err) {
2091
+ return {
2092
+ content: [{ type: "text", text: `Failed to list sessions: ${String(err)}` }],
2093
+ details: { error: String(err) },
2094
+ };
2095
+ }
2096
+ },
2097
+ },
2098
+ { name: "session_list" },
2099
+ );
2100
+ }
2101
+
2102
+ // --------------------------------------------------------------------------
2103
+ // 21. decision_record
2104
+ // --------------------------------------------------------------------------
2105
+ if (isToolEnabled("decision_record")) {
2106
+ api.registerTool(
2107
+ {
2108
+ name: "decision_record",
2109
+ description:
2110
+ "Record an architectural or design decision. Captures the rationale and alternatives considered for future reference. Always check existing decisions with decision_check first to avoid contradictions." +
2111
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : ""),
2112
+ parameters: {
2113
+ type: "object",
2114
+ properties: {
2115
+ title: {
2116
+ type: "string",
2117
+ description: "Short title summarizing the decision.",
2118
+ },
2119
+ rationale: {
2120
+ type: "string",
2121
+ description: "Why this decision was made. Include context and reasoning.",
2122
+ },
2123
+ alternatives: {
2124
+ type: "string",
2125
+ description: "What alternatives were considered and why they were rejected.",
2126
+ },
2127
+ project: {
2128
+ type: "string",
2129
+ description: "Project slug this decision applies to.",
2130
+ },
2131
+ tags: {
2132
+ type: "array",
2133
+ description: "Tags for categorizing the decision.",
2134
+ items: { type: "string" },
2135
+ },
2136
+ status: {
2137
+ type: "string",
2138
+ description: "Decision status.",
2139
+ enum: ["active", "experimental"],
2140
+ },
2141
+ },
2142
+ required: ["title", "rationale"],
2143
+ },
2144
+ execute: async (
2145
+ _id,
2146
+ args: {
2147
+ title: string;
2148
+ rationale: string;
2149
+ alternatives?: string;
2150
+ project?: string;
2151
+ tags?: string[];
2152
+ status?: string;
2153
+ },
2154
+ ) => {
2155
+ try {
2156
+ const project = args.project ?? defaultProject;
2157
+ const result = await client.recordDecision(
2158
+ args.title,
2159
+ args.rationale,
2160
+ args.alternatives,
2161
+ project,
2162
+ args.tags,
2163
+ args.status,
2164
+ );
2165
+ return {
2166
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2167
+ details: { result },
2168
+ };
2169
+ } catch (err) {
2170
+ return {
2171
+ content: [{ type: "text", text: `Failed to record decision: ${String(err)}` }],
2172
+ details: { error: String(err) },
2173
+ };
2174
+ }
2175
+ },
2176
+ },
2177
+ { name: "decision_record" },
2178
+ );
2179
+ }
2180
+
2181
+ // --------------------------------------------------------------------------
2182
+ // 22. decision_list
2183
+ // --------------------------------------------------------------------------
2184
+ if (isToolEnabled("decision_list")) {
2185
+ api.registerTool(
2186
+ {
2187
+ name: "decision_list",
2188
+ description: "List recorded decisions, optionally filtered by project, status, or tags." +
2189
+ (defaultProject ? ` Scoped to project '${defaultProject}' by default.` : ""),
2190
+ parameters: {
2191
+ type: "object",
2192
+ properties: {
2193
+ limit: {
2194
+ type: "number",
2195
+ description: "Maximum decisions to return. Default 20.",
2196
+ minimum: 1,
2197
+ maximum: 100,
2198
+ },
2199
+ project: {
2200
+ type: "string",
2201
+ description: "Filter by project slug.",
2202
+ },
2203
+ status: {
2204
+ type: "string",
2205
+ description: "Filter by status.",
2206
+ enum: ["active", "superseded", "reverted", "experimental"],
2207
+ },
2208
+ tags: {
2209
+ type: "string",
2210
+ description: "Comma-separated tags to filter by.",
2211
+ },
2212
+ },
2213
+ },
2214
+ execute: async (
2215
+ _id,
2216
+ args: { limit?: number; project?: string; status?: string; tags?: string },
2217
+ ) => {
2218
+ try {
2219
+ const project = args.project ?? defaultProject;
2220
+ const result = await client.listDecisions(args.limit, project, args.status, args.tags);
2221
+ return {
2222
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2223
+ details: { result },
2224
+ };
2225
+ } catch (err) {
2226
+ return {
2227
+ content: [{ type: "text", text: `Failed to list decisions: ${String(err)}` }],
2228
+ details: { error: String(err) },
2229
+ };
2230
+ }
2231
+ },
2232
+ },
2233
+ { name: "decision_list" },
2234
+ );
2235
+ }
2236
+
2237
+ // --------------------------------------------------------------------------
2238
+ // 23. decision_supersede
2239
+ // --------------------------------------------------------------------------
2240
+ if (isToolEnabled("decision_supersede")) {
2241
+ api.registerTool(
2242
+ {
2243
+ name: "decision_supersede",
2244
+ description:
2245
+ "Supersede an existing decision with a new one. The old decision is marked as superseded and linked to the replacement.",
2246
+ parameters: {
2247
+ type: "object",
2248
+ properties: {
2249
+ id: {
2250
+ type: "string",
2251
+ description: "ID of the decision to supersede.",
2252
+ },
2253
+ title: {
2254
+ type: "string",
2255
+ description: "Title of the new replacement decision.",
2256
+ },
2257
+ rationale: {
2258
+ type: "string",
2259
+ description: "Why the previous decision is being replaced.",
2260
+ },
2261
+ alternatives: {
2262
+ type: "string",
2263
+ description: "Alternatives considered for the new decision.",
2264
+ },
2265
+ tags: {
2266
+ type: "array",
2267
+ description: "Tags for the new decision.",
2268
+ items: { type: "string" },
2269
+ },
2270
+ },
2271
+ required: ["id", "title", "rationale"],
2272
+ },
2273
+ execute: async (
2274
+ _id,
2275
+ args: {
2276
+ id: string;
2277
+ title: string;
2278
+ rationale: string;
2279
+ alternatives?: string;
2280
+ tags?: string[];
2281
+ },
2282
+ ) => {
2283
+ try {
2284
+ const result = await client.supersedeDecision(
2285
+ args.id,
2286
+ args.title,
2287
+ args.rationale,
2288
+ args.alternatives,
2289
+ args.tags,
2290
+ );
2291
+ return {
2292
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2293
+ details: { result },
2294
+ };
2295
+ } catch (err) {
2296
+ return {
2297
+ content: [{ type: "text", text: `Failed to supersede decision: ${String(err)}` }],
2298
+ details: { error: String(err) },
2299
+ };
2300
+ }
2301
+ },
2302
+ },
2303
+ { name: "decision_supersede" },
2304
+ );
2305
+ }
2306
+
2307
+ // --------------------------------------------------------------------------
2308
+ // 24. decision_check
2309
+ // --------------------------------------------------------------------------
2310
+ if (isToolEnabled("decision_check")) {
2311
+ api.registerTool(
2312
+ {
2313
+ name: "decision_check",
2314
+ description:
2315
+ "Check if there are existing decisions relevant to a topic. ALWAYS call this before making architectural choices to avoid contradicting past decisions." +
2316
+ (defaultProject ? ` Scoped to project '${defaultProject}' by default.` : ""),
2317
+ parameters: {
2318
+ type: "object",
2319
+ properties: {
2320
+ query: {
2321
+ type: "string",
2322
+ description: "Natural language description of the topic or decision area.",
2323
+ },
2324
+ project: {
2325
+ type: "string",
2326
+ description: "Project slug to scope the search.",
2327
+ },
2328
+ limit: {
2329
+ type: "number",
2330
+ description: "Maximum results. Default 5.",
2331
+ },
2332
+ threshold: {
2333
+ type: "number",
2334
+ description: "Minimum similarity threshold (0-1). Default 0.3.",
2335
+ },
2336
+ include_superseded: {
2337
+ type: "boolean",
2338
+ description: "Include superseded decisions in results. Default false.",
2339
+ },
2340
+ },
2341
+ required: ["query"],
2342
+ },
2343
+ execute: async (
2344
+ _id,
2345
+ args: {
2346
+ query: string;
2347
+ project?: string;
2348
+ limit?: number;
2349
+ threshold?: number;
2350
+ include_superseded?: boolean;
2351
+ },
2352
+ ) => {
2353
+ try {
2354
+ const project = args.project ?? defaultProject;
2355
+ const result = await client.checkDecisions(
2356
+ args.query,
2357
+ project,
2358
+ args.limit,
2359
+ args.threshold,
2360
+ args.include_superseded,
2361
+ );
2362
+ return {
2363
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2364
+ details: { result },
2365
+ };
2366
+ } catch (err) {
2367
+ return {
2368
+ content: [{ type: "text", text: `Failed to check decisions: ${String(err)}` }],
2369
+ details: { error: String(err) },
2370
+ };
2371
+ }
2372
+ },
2373
+ },
2374
+ { name: "decision_check" },
2375
+ );
2376
+ }
2377
+
2378
+ // --------------------------------------------------------------------------
2379
+ // 25. pattern_create
2380
+ // --------------------------------------------------------------------------
2381
+ if (isToolEnabled("pattern_create")) {
2382
+ api.registerTool(
2383
+ {
2384
+ name: "pattern_create",
2385
+ description:
2386
+ "Create a reusable pattern (coding convention, architecture pattern, or best practice) that can be shared across projects. Include example_code for maximum usefulness." +
2387
+ (defaultProject ? ` Source project defaults to '${defaultProject}' if not specified.` : ""),
2388
+ parameters: {
2389
+ type: "object",
2390
+ properties: {
2391
+ title: {
2392
+ type: "string",
2393
+ description: "Pattern title.",
2394
+ },
2395
+ description: {
2396
+ type: "string",
2397
+ description: "Detailed description of the pattern, when to use it, and why.",
2398
+ },
2399
+ category: {
2400
+ type: "string",
2401
+ description: "Category (e.g., architecture, testing, error-handling, naming).",
2402
+ },
2403
+ example_code: {
2404
+ type: "string",
2405
+ description: "Example code demonstrating the pattern.",
2406
+ },
2407
+ scope: {
2408
+ type: "string",
2409
+ description: "Scope: global (visible to all projects) or project (visible to source project only).",
2410
+ enum: ["global", "project"],
2411
+ },
2412
+ tags: {
2413
+ type: "array",
2414
+ description: "Tags for categorization.",
2415
+ items: { type: "string" },
2416
+ },
2417
+ source_project: {
2418
+ type: "string",
2419
+ description: "Project slug where this pattern originated.",
2420
+ },
2421
+ },
2422
+ required: ["title", "description"],
2423
+ },
2424
+ execute: async (
2425
+ _id,
2426
+ args: {
2427
+ title: string;
2428
+ description: string;
2429
+ category?: string;
2430
+ example_code?: string;
2431
+ scope?: string;
2432
+ tags?: string[];
2433
+ source_project?: string;
2434
+ },
2435
+ ) => {
2436
+ try {
2437
+ const sourceProject = args.source_project ?? defaultProject;
2438
+ const result = await client.createPattern(
2439
+ args.title,
2440
+ args.description,
2441
+ args.category,
2442
+ args.example_code,
2443
+ args.scope,
2444
+ args.tags,
2445
+ sourceProject,
2446
+ );
2447
+ return {
2448
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2449
+ details: { result },
2450
+ };
2451
+ } catch (err) {
2452
+ return {
2453
+ content: [{ type: "text", text: `Failed to create pattern: ${String(err)}` }],
2454
+ details: { error: String(err) },
2455
+ };
2456
+ }
2457
+ },
2458
+ },
2459
+ { name: "pattern_create" },
2460
+ );
2461
+ }
2462
+
2463
+ // --------------------------------------------------------------------------
2464
+ // 26. pattern_search
2465
+ // --------------------------------------------------------------------------
2466
+ if (isToolEnabled("pattern_search")) {
2467
+ api.registerTool(
2468
+ {
2469
+ name: "pattern_search",
2470
+ description: "Search for established patterns by natural language query. Call this before writing code to find and follow existing conventions." +
2471
+ (defaultProject ? ` Scoped to project '${defaultProject}' by default.` : ""),
2472
+ parameters: {
2473
+ type: "object",
2474
+ properties: {
2475
+ query: {
2476
+ type: "string",
2477
+ description: "Natural language search query.",
2478
+ },
2479
+ category: {
2480
+ type: "string",
2481
+ description: "Filter by category.",
2482
+ },
2483
+ project: {
2484
+ type: "string",
2485
+ description: "Filter by project slug.",
2486
+ },
2487
+ limit: {
2488
+ type: "number",
2489
+ description: "Maximum results. Default 10.",
2490
+ },
2491
+ threshold: {
2492
+ type: "number",
2493
+ description: "Minimum similarity threshold (0-1). Default 0.3.",
2494
+ },
2495
+ },
2496
+ required: ["query"],
2497
+ },
2498
+ execute: async (
2499
+ _id,
2500
+ args: {
2501
+ query: string;
2502
+ category?: string;
2503
+ project?: string;
2504
+ limit?: number;
2505
+ threshold?: number;
2506
+ },
2507
+ ) => {
2508
+ try {
2509
+ const project = args.project ?? defaultProject;
2510
+ const result = await client.searchPatterns(
2511
+ args.query,
2512
+ args.category,
2513
+ project,
2514
+ args.limit,
2515
+ args.threshold,
2516
+ );
2517
+ return {
2518
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2519
+ details: { result },
2520
+ };
2521
+ } catch (err) {
2522
+ return {
2523
+ content: [{ type: "text", text: `Failed to search patterns: ${String(err)}` }],
2524
+ details: { error: String(err) },
2525
+ };
2526
+ }
2527
+ },
2528
+ },
2529
+ { name: "pattern_search" },
2530
+ );
2531
+ }
2532
+
2533
+ // --------------------------------------------------------------------------
2534
+ // 27. pattern_adopt
2535
+ // --------------------------------------------------------------------------
2536
+ if (isToolEnabled("pattern_adopt")) {
2537
+ api.registerTool(
2538
+ {
2539
+ name: "pattern_adopt",
2540
+ description: "Adopt an existing pattern for use in a project. Creates a link between the pattern and the project.",
2541
+ parameters: {
2542
+ type: "object",
2543
+ properties: {
2544
+ id: {
2545
+ type: "string",
2546
+ description: "Pattern ID to adopt.",
2547
+ },
2548
+ project: {
2549
+ type: "string",
2550
+ description: "Project slug adopting the pattern.",
2551
+ },
2552
+ },
2553
+ required: ["id", "project"],
2554
+ },
2555
+ execute: async (_id, args: { id: string; project: string }) => {
2556
+ try {
2557
+ const result = await client.adoptPattern(args.id, args.project);
2558
+ return {
2559
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2560
+ details: { result },
2561
+ };
2562
+ } catch (err) {
2563
+ return {
2564
+ content: [{ type: "text", text: `Failed to adopt pattern: ${String(err)}` }],
2565
+ details: { error: String(err) },
2566
+ };
2567
+ }
2568
+ },
2569
+ },
2570
+ { name: "pattern_adopt" },
2571
+ );
2572
+ }
2573
+
2574
+ // --------------------------------------------------------------------------
2575
+ // 28. pattern_suggest
2576
+ // --------------------------------------------------------------------------
2577
+ if (isToolEnabled("pattern_suggest")) {
2578
+ api.registerTool(
2579
+ {
2580
+ name: "pattern_suggest",
2581
+ description:
2582
+ "Get pattern suggestions for a project based on its stack and existing patterns from related projects.",
2583
+ parameters: {
2584
+ type: "object",
2585
+ properties: {
2586
+ project: {
2587
+ type: "string",
2588
+ description: "Project slug to get suggestions for.",
2589
+ },
2590
+ limit: {
2591
+ type: "number",
2592
+ description: "Maximum suggestions. Default 10.",
2593
+ },
2594
+ },
2595
+ required: ["project"],
2596
+ },
2597
+ execute: async (_id, args: { project: string; limit?: number }) => {
2598
+ try {
2599
+ const result = await client.suggestPatterns(args.project, args.limit);
2600
+ return {
2601
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2602
+ details: { result },
2603
+ };
2604
+ } catch (err) {
2605
+ return {
2606
+ content: [{ type: "text", text: `Failed to suggest patterns: ${String(err)}` }],
2607
+ details: { error: String(err) },
2608
+ };
2609
+ }
2610
+ },
2611
+ },
2612
+ { name: "pattern_suggest" },
2613
+ );
2614
+ }
2615
+
2616
+ // --------------------------------------------------------------------------
2617
+ // 29. project_register
2618
+ // --------------------------------------------------------------------------
2619
+ if (isToolEnabled("project_register")) {
2620
+ api.registerTool(
2621
+ {
2622
+ name: "project_register",
2623
+ description: "Register a new project in MemoryRelay. Projects organize memories, decisions, patterns, and sessions.",
2624
+ parameters: {
2625
+ type: "object",
2626
+ properties: {
2627
+ slug: {
2628
+ type: "string",
2629
+ description: "URL-friendly project identifier (e.g., 'my-api', 'frontend-app').",
2630
+ },
2631
+ name: {
2632
+ type: "string",
2633
+ description: "Human-readable project name.",
2634
+ },
2635
+ description: {
2636
+ type: "string",
2637
+ description: "Project description.",
2638
+ },
2639
+ stack: {
2640
+ type: "object",
2641
+ description: "Technology stack details (e.g., {language: 'python', framework: 'fastapi'}).",
2642
+ },
2643
+ repo_url: {
2644
+ type: "string",
2645
+ description: "Repository URL.",
2646
+ },
2647
+ },
2648
+ required: ["slug", "name"],
2649
+ },
2650
+ execute: async (
2651
+ _id,
2652
+ args: {
2653
+ slug: string;
2654
+ name: string;
2655
+ description?: string;
2656
+ stack?: Record<string, unknown>;
2657
+ repo_url?: string;
2658
+ },
2659
+ ) => {
2660
+ try {
2661
+ const result = await client.registerProject(
2662
+ args.slug,
2663
+ args.name,
2664
+ args.description,
2665
+ args.stack,
2666
+ args.repo_url,
2667
+ );
2668
+ return {
2669
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2670
+ details: { result },
2671
+ };
2672
+ } catch (err) {
2673
+ return {
2674
+ content: [{ type: "text", text: `Failed to register project: ${String(err)}` }],
2675
+ details: { error: String(err) },
2676
+ };
2677
+ }
2678
+ },
2679
+ },
2680
+ { name: "project_register" },
2681
+ );
2682
+ }
2683
+
2684
+ // --------------------------------------------------------------------------
2685
+ // 30. project_list
2686
+ // --------------------------------------------------------------------------
2687
+ if (isToolEnabled("project_list")) {
2688
+ api.registerTool(
2689
+ {
2690
+ name: "project_list",
2691
+ description: "List all registered projects.",
2692
+ parameters: {
2693
+ type: "object",
2694
+ properties: {
2695
+ limit: {
2696
+ type: "number",
2697
+ description: "Maximum projects to return. Default 20.",
2698
+ minimum: 1,
2699
+ maximum: 100,
2700
+ },
2701
+ },
2702
+ },
2703
+ execute: async (_id, args: { limit?: number }) => {
2704
+ try {
2705
+ const result = await client.listProjects(args.limit);
2706
+ return {
2707
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2708
+ details: { result },
2709
+ };
2710
+ } catch (err) {
2711
+ return {
2712
+ content: [{ type: "text", text: `Failed to list projects: ${String(err)}` }],
2713
+ details: { error: String(err) },
2714
+ };
2715
+ }
2716
+ },
2717
+ },
2718
+ { name: "project_list" },
2719
+ );
2720
+ }
2721
+
2722
+ // --------------------------------------------------------------------------
2723
+ // 31. project_info
2724
+ // --------------------------------------------------------------------------
2725
+ if (isToolEnabled("project_info")) {
2726
+ api.registerTool(
2727
+ {
2728
+ name: "project_info",
2729
+ description: "Get detailed information about a specific project.",
2730
+ parameters: {
2731
+ type: "object",
2732
+ properties: {
2733
+ slug: {
2734
+ type: "string",
2735
+ description: "Project slug.",
2736
+ },
2737
+ },
2738
+ required: ["slug"],
2739
+ },
2740
+ execute: async (_id, args: { slug: string }) => {
2741
+ try {
2742
+ const result = await client.getProject(args.slug);
2743
+ return {
2744
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2745
+ details: { result },
2746
+ };
2747
+ } catch (err) {
2748
+ return {
2749
+ content: [{ type: "text", text: `Failed to get project: ${String(err)}` }],
2750
+ details: { error: String(err) },
2751
+ };
2752
+ }
2753
+ },
568
2754
  },
569
- },
570
- { name: "memory_forget" },
571
- );
2755
+ { name: "project_info" },
2756
+ );
2757
+ }
2758
+
2759
+ // --------------------------------------------------------------------------
2760
+ // 32. project_add_relationship
2761
+ // --------------------------------------------------------------------------
2762
+ if (isToolEnabled("project_add_relationship")) {
2763
+ api.registerTool(
2764
+ {
2765
+ name: "project_add_relationship",
2766
+ description:
2767
+ "Add a relationship between two projects (e.g., depends_on, api_consumer, shares_schema, shares_infra, pattern_source, forked_from).",
2768
+ parameters: {
2769
+ type: "object",
2770
+ properties: {
2771
+ from: {
2772
+ type: "string",
2773
+ description: "Source project slug.",
2774
+ },
2775
+ to: {
2776
+ type: "string",
2777
+ description: "Target project slug.",
2778
+ },
2779
+ type: {
2780
+ type: "string",
2781
+ description: "Relationship type (e.g., depends_on, api_consumer, shares_schema, shares_infra, pattern_source, forked_from).",
2782
+ },
2783
+ metadata: {
2784
+ type: "object",
2785
+ description: "Optional metadata about the relationship.",
2786
+ },
2787
+ },
2788
+ required: ["from", "to", "type"],
2789
+ },
2790
+ execute: async (
2791
+ _id,
2792
+ args: { from: string; to: string; type: string; metadata?: Record<string, unknown> },
2793
+ ) => {
2794
+ try {
2795
+ const result = await client.addProjectRelationship(
2796
+ args.from,
2797
+ args.to,
2798
+ args.type,
2799
+ args.metadata,
2800
+ );
2801
+ return {
2802
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2803
+ details: { result },
2804
+ };
2805
+ } catch (err) {
2806
+ return {
2807
+ content: [{ type: "text", text: `Failed to add relationship: ${String(err)}` }],
2808
+ details: { error: String(err) },
2809
+ };
2810
+ }
2811
+ },
2812
+ },
2813
+ { name: "project_add_relationship" },
2814
+ );
2815
+ }
2816
+
2817
+ // --------------------------------------------------------------------------
2818
+ // 33. project_dependencies
2819
+ // --------------------------------------------------------------------------
2820
+ if (isToolEnabled("project_dependencies")) {
2821
+ api.registerTool(
2822
+ {
2823
+ name: "project_dependencies",
2824
+ description: "List projects that a given project depends on.",
2825
+ parameters: {
2826
+ type: "object",
2827
+ properties: {
2828
+ project: {
2829
+ type: "string",
2830
+ description: "Project slug.",
2831
+ },
2832
+ },
2833
+ required: ["project"],
2834
+ },
2835
+ execute: async (_id, args: { project: string }) => {
2836
+ try {
2837
+ const result = await client.getProjectDependencies(args.project);
2838
+ return {
2839
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2840
+ details: { result },
2841
+ };
2842
+ } catch (err) {
2843
+ return {
2844
+ content: [{ type: "text", text: `Failed to get dependencies: ${String(err)}` }],
2845
+ details: { error: String(err) },
2846
+ };
2847
+ }
2848
+ },
2849
+ },
2850
+ { name: "project_dependencies" },
2851
+ );
2852
+ }
2853
+
2854
+ // --------------------------------------------------------------------------
2855
+ // 34. project_dependents
2856
+ // --------------------------------------------------------------------------
2857
+ if (isToolEnabled("project_dependents")) {
2858
+ api.registerTool(
2859
+ {
2860
+ name: "project_dependents",
2861
+ description: "List projects that depend on a given project.",
2862
+ parameters: {
2863
+ type: "object",
2864
+ properties: {
2865
+ project: {
2866
+ type: "string",
2867
+ description: "Project slug.",
2868
+ },
2869
+ },
2870
+ required: ["project"],
2871
+ },
2872
+ execute: async (_id, args: { project: string }) => {
2873
+ try {
2874
+ const result = await client.getProjectDependents(args.project);
2875
+ return {
2876
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2877
+ details: { result },
2878
+ };
2879
+ } catch (err) {
2880
+ return {
2881
+ content: [{ type: "text", text: `Failed to get dependents: ${String(err)}` }],
2882
+ details: { error: String(err) },
2883
+ };
2884
+ }
2885
+ },
2886
+ },
2887
+ { name: "project_dependents" },
2888
+ );
2889
+ }
2890
+
2891
+ // --------------------------------------------------------------------------
2892
+ // 35. project_related
2893
+ // --------------------------------------------------------------------------
2894
+ if (isToolEnabled("project_related")) {
2895
+ api.registerTool(
2896
+ {
2897
+ name: "project_related",
2898
+ description: "List all projects related to a given project (any relationship direction).",
2899
+ parameters: {
2900
+ type: "object",
2901
+ properties: {
2902
+ project: {
2903
+ type: "string",
2904
+ description: "Project slug.",
2905
+ },
2906
+ },
2907
+ required: ["project"],
2908
+ },
2909
+ execute: async (_id, args: { project: string }) => {
2910
+ try {
2911
+ const result = await client.getProjectRelated(args.project);
2912
+ return {
2913
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2914
+ details: { result },
2915
+ };
2916
+ } catch (err) {
2917
+ return {
2918
+ content: [{ type: "text", text: `Failed to get related projects: ${String(err)}` }],
2919
+ details: { error: String(err) },
2920
+ };
2921
+ }
2922
+ },
2923
+ },
2924
+ { name: "project_related" },
2925
+ );
2926
+ }
2927
+
2928
+ // --------------------------------------------------------------------------
2929
+ // 36. project_impact
2930
+ // --------------------------------------------------------------------------
2931
+ if (isToolEnabled("project_impact")) {
2932
+ api.registerTool(
2933
+ {
2934
+ name: "project_impact",
2935
+ description:
2936
+ "Analyze the impact of a proposed change on a project and its dependents. Helps understand blast radius before making changes.",
2937
+ parameters: {
2938
+ type: "object",
2939
+ properties: {
2940
+ project: {
2941
+ type: "string",
2942
+ description: "Project slug to analyze.",
2943
+ },
2944
+ change_description: {
2945
+ type: "string",
2946
+ description: "Description of the proposed change.",
2947
+ },
2948
+ },
2949
+ required: ["project", "change_description"],
2950
+ },
2951
+ execute: async (_id, args: { project: string; change_description: string }) => {
2952
+ try {
2953
+ const result = await client.projectImpact(args.project, args.change_description);
2954
+ return {
2955
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2956
+ details: { result },
2957
+ };
2958
+ } catch (err) {
2959
+ return {
2960
+ content: [{ type: "text", text: `Failed to analyze impact: ${String(err)}` }],
2961
+ details: { error: String(err) },
2962
+ };
2963
+ }
2964
+ },
2965
+ },
2966
+ { name: "project_impact" },
2967
+ );
2968
+ }
2969
+
2970
+ // --------------------------------------------------------------------------
2971
+ // 37. project_shared_patterns
2972
+ // --------------------------------------------------------------------------
2973
+ if (isToolEnabled("project_shared_patterns")) {
2974
+ api.registerTool(
2975
+ {
2976
+ name: "project_shared_patterns",
2977
+ description: "Find patterns shared between two projects. Useful for maintaining consistency across related projects.",
2978
+ parameters: {
2979
+ type: "object",
2980
+ properties: {
2981
+ project_a: {
2982
+ type: "string",
2983
+ description: "First project slug.",
2984
+ },
2985
+ project_b: {
2986
+ type: "string",
2987
+ description: "Second project slug.",
2988
+ },
2989
+ },
2990
+ required: ["project_a", "project_b"],
2991
+ },
2992
+ execute: async (_id, args: { project_a: string; project_b: string }) => {
2993
+ try {
2994
+ const result = await client.getSharedPatterns(args.project_a, args.project_b);
2995
+ return {
2996
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2997
+ details: { result },
2998
+ };
2999
+ } catch (err) {
3000
+ return {
3001
+ content: [{ type: "text", text: `Failed to get shared patterns: ${String(err)}` }],
3002
+ details: { error: String(err) },
3003
+ };
3004
+ }
3005
+ },
3006
+ },
3007
+ { name: "project_shared_patterns" },
3008
+ );
3009
+ }
3010
+
3011
+ // --------------------------------------------------------------------------
3012
+ // 38. project_context
3013
+ // --------------------------------------------------------------------------
3014
+ if (isToolEnabled("project_context")) {
3015
+ api.registerTool(
3016
+ {
3017
+ name: "project_context",
3018
+ description:
3019
+ "Load full project context including hot-tier memories, active decisions, adopted patterns, and recent sessions. Call this FIRST when starting work on a project to understand existing context before making changes.",
3020
+ parameters: {
3021
+ type: "object",
3022
+ properties: {
3023
+ project: {
3024
+ type: "string",
3025
+ description: "Project slug.",
3026
+ },
3027
+ },
3028
+ required: ["project"],
3029
+ },
3030
+ execute: async (_id, args: { project: string }) => {
3031
+ try {
3032
+ const result = await client.getProjectContext(args.project);
3033
+ return {
3034
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3035
+ details: { result },
3036
+ };
3037
+ } catch (err) {
3038
+ return {
3039
+ content: [{ type: "text", text: `Failed to load project context: ${String(err)}` }],
3040
+ details: { error: String(err) },
3041
+ };
3042
+ }
3043
+ },
3044
+ },
3045
+ { name: "project_context" },
3046
+ );
3047
+ }
3048
+
3049
+ // --------------------------------------------------------------------------
3050
+ // 39. memory_health
3051
+ // --------------------------------------------------------------------------
3052
+ if (isToolEnabled("memory_health")) {
3053
+ api.registerTool(
3054
+ {
3055
+ name: "memory_health",
3056
+ description: "Check the MemoryRelay API connectivity and health status.",
3057
+ parameters: {
3058
+ type: "object",
3059
+ properties: {},
3060
+ },
3061
+ execute: async () => {
3062
+ try {
3063
+ const health = await client.health();
3064
+ return {
3065
+ content: [{ type: "text", text: JSON.stringify(health, null, 2) }],
3066
+ details: { health },
3067
+ };
3068
+ } catch (err) {
3069
+ return {
3070
+ content: [{ type: "text", text: `Health check failed: ${String(err)}` }],
3071
+ details: { error: String(err) },
3072
+ };
3073
+ }
3074
+ },
3075
+ },
3076
+ { name: "memory_health" },
3077
+ );
3078
+ }
572
3079
 
573
3080
  // ========================================================================
574
- // CLI Commands (Enhanced)
3081
+ // CLI Commands
575
3082
  // ========================================================================
576
3083
 
577
3084
  api.registerCli(
@@ -680,24 +3187,106 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
680
3187
  // Lifecycle Hooks
681
3188
  // ========================================================================
682
3189
 
683
- // Auto-recall: inject relevant memories before agent starts
684
- if (cfg?.autoRecall) {
685
- api.on("before_agent_start", async (event) => {
686
- if (!event.prompt || event.prompt.length < 10) {
3190
+ // Workflow instructions + auto-recall: always inject workflow guidance,
3191
+ // optionally recall relevant memories if autoRecall is enabled
3192
+ api.on("before_agent_start", async (event) => {
3193
+ if (!event.prompt || event.prompt.length < 10) {
3194
+ return;
3195
+ }
3196
+
3197
+ // Check if current channel is excluded
3198
+ if (cfg?.excludeChannels && event.channel) {
3199
+ const channelId = String(event.channel);
3200
+ if (cfg.excludeChannels.some((excluded) => channelId.includes(excluded))) {
3201
+ api.logger.debug?.(
3202
+ `memory-memoryrelay: skipping for excluded channel: ${channelId}`,
3203
+ );
687
3204
  return;
688
3205
  }
3206
+ }
689
3207
 
690
- // NEW: Check if current channel is excluded
691
- if (cfg.excludeChannels && event.channel) {
692
- const channelId = String(event.channel);
693
- if (cfg.excludeChannels.some((excluded) => channelId.includes(excluded))) {
694
- api.logger.debug?.(
695
- `memory-memoryrelay: skipping auto-recall for excluded channel: ${channelId}`,
696
- );
697
- return;
698
- }
3208
+ // Build workflow instructions dynamically based on enabled tools
3209
+ const lines: string[] = [
3210
+ "You have MemoryRelay tools available for persistent memory across sessions.",
3211
+ ];
3212
+
3213
+ if (defaultProject) {
3214
+ lines.push(`Default project: \`${defaultProject}\` (auto-applied when you omit the project parameter).`);
3215
+ }
3216
+
3217
+ lines.push("", "## Recommended Workflow", "");
3218
+
3219
+ // Starting work section — only include steps for enabled tools
3220
+ const startSteps: string[] = [];
3221
+ if (isToolEnabled("project_context")) {
3222
+ startSteps.push(`**Load context**: Call \`project_context(${defaultProject ? `"${defaultProject}"` : "project"})\` to load hot-tier memories, active decisions, and adopted patterns`);
3223
+ }
3224
+ if (isToolEnabled("session_start")) {
3225
+ startSteps.push(`**Start session**: Call \`session_start(title${defaultProject ? "" : ", project"})\` to begin tracking your work`);
3226
+ }
3227
+ if (isToolEnabled("decision_check")) {
3228
+ startSteps.push(`**Check decisions**: Call \`decision_check(query${defaultProject ? "" : ", project"})\` before making architectural choices`);
3229
+ }
3230
+ if (isToolEnabled("pattern_search")) {
3231
+ startSteps.push("**Find patterns**: Call `pattern_search(query)` to find established conventions before writing code");
3232
+ }
3233
+
3234
+ if (startSteps.length > 0) {
3235
+ lines.push("When starting work on a project:");
3236
+ startSteps.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
3237
+ lines.push("");
3238
+ }
3239
+
3240
+ // While working section
3241
+ const workSteps: string[] = [];
3242
+ if (isToolEnabled("memory_store")) {
3243
+ workSteps.push("**Store findings**: Call `memory_store(content, metadata)` for important information worth remembering");
3244
+ }
3245
+ if (isToolEnabled("decision_record")) {
3246
+ workSteps.push(`**Record decisions**: Call \`decision_record(title, rationale${defaultProject ? "" : ", project"})\` when making significant architectural choices`);
3247
+ }
3248
+ if (isToolEnabled("pattern_create")) {
3249
+ workSteps.push("**Create patterns**: Call `pattern_create(title, description)` when establishing reusable conventions");
3250
+ }
3251
+
3252
+ if (workSteps.length > 0) {
3253
+ lines.push("While working:");
3254
+ const offset = startSteps.length;
3255
+ workSteps.forEach((step, i) => lines.push(`${offset + i + 1}. ${step}`));
3256
+ lines.push("");
3257
+ }
3258
+
3259
+ // When done section
3260
+ if (isToolEnabled("session_end")) {
3261
+ const offset = startSteps.length + workSteps.length;
3262
+ lines.push("When done:");
3263
+ lines.push(`${offset + 1}. **End session**: Call \`session_end(session_id, summary)\` with a summary of what was accomplished`);
3264
+ lines.push("");
3265
+ }
3266
+
3267
+ // First-time setup — only if project tools are enabled
3268
+ if (isToolEnabled("project_register")) {
3269
+ lines.push("## First-Time Setup", "");
3270
+ lines.push("If the project is not yet registered, start with:");
3271
+ lines.push("1. `project_register(slug, name, description, stack)` to register the project");
3272
+ lines.push("2. Then follow the workflow above");
3273
+ lines.push("");
3274
+ if (isToolEnabled("project_list")) {
3275
+ lines.push("Use `project_list()` to see existing projects before registering a new one.");
699
3276
  }
3277
+ }
3278
+
3279
+ // Memory-only fallback — if no session/decision/project tools are enabled
3280
+ if (startSteps.length === 0 && workSteps.length === 0) {
3281
+ lines.push("Use `memory_store(content)` to save important information and `memory_recall(query)` to find relevant memories.");
3282
+ }
700
3283
 
3284
+ const workflowInstructions = lines.join("\n");
3285
+
3286
+ let prependContext = `<memoryrelay-workflow>\n${workflowInstructions}\n</memoryrelay-workflow>`;
3287
+
3288
+ // Auto-recall: search and inject relevant memories
3289
+ if (cfg?.autoRecall) {
701
3290
  try {
702
3291
  const results = await client.search(
703
3292
  event.prompt,
@@ -705,24 +3294,23 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
705
3294
  cfg.recallThreshold || 0.3,
706
3295
  );
707
3296
 
708
- if (results.length === 0) {
709
- return;
710
- }
711
-
712
- const memoryContext = results.map((r) => `- ${r.memory.content}`).join("\n");
3297
+ if (results.length > 0) {
3298
+ const memoryContext = results.map((r) => `- ${r.memory.content}`).join("\n");
713
3299
 
714
- api.logger.info?.(
715
- `memory-memoryrelay: injecting ${results.length} memories into context`,
716
- );
3300
+ api.logger.info?.(
3301
+ `memory-memoryrelay: injecting ${results.length} memories into context`,
3302
+ );
717
3303
 
718
- return {
719
- prependContext: `<relevant-memories>\nThe following memories from MemoryRelay may be relevant:\n${memoryContext}\n</relevant-memories>`,
720
- };
3304
+ prependContext +=
3305
+ `\n\n<relevant-memories>\nThe following memories from MemoryRelay may be relevant:\n${memoryContext}\n</relevant-memories>`;
3306
+ }
721
3307
  } catch (err) {
722
3308
  api.logger.warn?.(`memory-memoryrelay: recall failed: ${String(err)}`);
723
3309
  }
724
- });
725
- }
3310
+ }
3311
+
3312
+ return { prependContext };
3313
+ });
726
3314
 
727
3315
  // Auto-capture: analyze and store important information after agent ends
728
3316
  if (cfg?.autoCapture) {
@@ -780,6 +3368,230 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
780
3368
  }
781
3369
 
782
3370
  api.logger.info?.(
783
- `memory-memoryrelay: plugin loaded (autoRecall: ${cfg?.autoRecall}, autoCapture: ${cfg?.autoCapture})`,
3371
+ `memory-memoryrelay: plugin v0.8.0 loaded (39 tools, autoRecall: ${cfg?.autoRecall}, autoCapture: ${cfg?.autoCapture}, debug: ${debugEnabled})`,
784
3372
  );
3373
+
3374
+ // ========================================================================
3375
+ // CLI Helper Tools (v0.8.0)
3376
+ // ========================================================================
3377
+
3378
+ // Register CLI-accessible tools for debugging and diagnostics
3379
+
3380
+ // memoryrelay:logs - Get debug logs
3381
+ if (debugLogger) {
3382
+ api.registerGatewayMethod?.("memoryrelay.logs", async ({ respond, args }) => {
3383
+ try {
3384
+ const limit = args?.limit || 20;
3385
+ const toolName = args?.tool;
3386
+ const errorsOnly = args?.errorsOnly || false;
3387
+
3388
+ let logs: LogEntry[];
3389
+ if (toolName) {
3390
+ logs = debugLogger.getToolLogs(toolName, limit);
3391
+ } else if (errorsOnly) {
3392
+ logs = debugLogger.getErrorLogs(limit);
3393
+ } else {
3394
+ logs = debugLogger.getRecentLogs(limit);
3395
+ }
3396
+
3397
+ const formatted = DebugLogger.formatTable(logs);
3398
+ respond(true, {
3399
+ logs,
3400
+ formatted,
3401
+ count: logs.length,
3402
+ });
3403
+ } catch (err) {
3404
+ respond(false, { error: String(err) });
3405
+ }
3406
+ });
3407
+ }
3408
+
3409
+ // memoryrelay:health - Comprehensive health check
3410
+ api.registerGatewayMethod?.("memoryrelay.health", async ({ respond }) => {
3411
+ try {
3412
+ const startTime = Date.now();
3413
+ const health = await client.health();
3414
+ const healthDuration = Date.now() - startTime;
3415
+
3416
+ const results: any = {
3417
+ api: {
3418
+ status: health.status,
3419
+ endpoint: apiUrl,
3420
+ responseTime: healthDuration,
3421
+ reachable: true,
3422
+ },
3423
+ authentication: {
3424
+ status: "valid",
3425
+ apiKey: apiKey.substring(0, 16) + "...",
3426
+ },
3427
+ tools: {},
3428
+ };
3429
+
3430
+ // Test critical tools
3431
+ const toolTests = [
3432
+ { name: "memory_store", test: async () => {
3433
+ const testMem = await client.store("Plugin health check test", { test: "true" });
3434
+ await client.delete(testMem.id);
3435
+ return { success: true };
3436
+ }},
3437
+ { name: "memory_recall", test: async () => {
3438
+ await client.search("test", 1, 0.5);
3439
+ return { success: true };
3440
+ }},
3441
+ { name: "memory_list", test: async () => {
3442
+ await client.list(1);
3443
+ return { success: true };
3444
+ }},
3445
+ ];
3446
+
3447
+ for (const { name, test } of toolTests) {
3448
+ const testStart = Date.now();
3449
+ try {
3450
+ await test();
3451
+ results.tools[name] = {
3452
+ status: "working",
3453
+ duration: Date.now() - testStart,
3454
+ };
3455
+ } catch (err) {
3456
+ results.tools[name] = {
3457
+ status: "error",
3458
+ error: String(err),
3459
+ duration: Date.now() - testStart,
3460
+ };
3461
+ }
3462
+ }
3463
+
3464
+ // Overall status
3465
+ const allToolsWorking = Object.values(results.tools).every(
3466
+ (t: any) => t.status === "working"
3467
+ );
3468
+ results.overall = allToolsWorking ? "healthy" : "degraded";
3469
+
3470
+ respond(true, results);
3471
+ } catch (err) {
3472
+ respond(false, {
3473
+ overall: "unhealthy",
3474
+ error: String(err),
3475
+ });
3476
+ }
3477
+ });
3478
+
3479
+ // memoryrelay:metrics - Performance metrics
3480
+ if (debugLogger) {
3481
+ api.registerGatewayMethod?.("memoryrelay.metrics", async ({ respond }) => {
3482
+ try {
3483
+ const stats = debugLogger.getStats();
3484
+ const allLogs = debugLogger.getAllLogs();
3485
+
3486
+ // Calculate per-tool metrics
3487
+ const toolMetrics: Record<string, any> = {};
3488
+ for (const log of allLogs) {
3489
+ if (!toolMetrics[log.tool]) {
3490
+ toolMetrics[log.tool] = {
3491
+ calls: 0,
3492
+ successes: 0,
3493
+ failures: 0,
3494
+ totalDuration: 0,
3495
+ durations: [],
3496
+ };
3497
+ }
3498
+ const metric = toolMetrics[log.tool];
3499
+ metric.calls++;
3500
+ if (log.status === "success") {
3501
+ metric.successes++;
3502
+ } else {
3503
+ metric.failures++;
3504
+ }
3505
+ metric.totalDuration += log.duration;
3506
+ metric.durations.push(log.duration);
3507
+ }
3508
+
3509
+ // Calculate averages and percentiles
3510
+ for (const tool in toolMetrics) {
3511
+ const metric = toolMetrics[tool];
3512
+ metric.avgDuration = Math.round(metric.totalDuration / metric.calls);
3513
+ metric.successRate = Math.round((metric.successes / metric.calls) * 100);
3514
+
3515
+ // Calculate p95 and p99
3516
+ const sorted = metric.durations.sort((a: number, b: number) => a - b);
3517
+ const p95Index = Math.floor(sorted.length * 0.95);
3518
+ const p99Index = Math.floor(sorted.length * 0.99);
3519
+ metric.p95Duration = sorted[p95Index] || 0;
3520
+ metric.p99Duration = sorted[p99Index] || 0;
3521
+
3522
+ delete metric.durations; // Don't include raw data in response
3523
+ }
3524
+
3525
+ respond(true, {
3526
+ summary: stats,
3527
+ toolMetrics,
3528
+ });
3529
+ } catch (err) {
3530
+ respond(false, { error: String(err) });
3531
+ }
3532
+ });
3533
+ }
3534
+
3535
+ // memoryrelay:test - Test individual tool
3536
+ api.registerGatewayMethod?.("memoryrelay.test", async ({ respond, args }) => {
3537
+ try {
3538
+ const toolName = args?.tool;
3539
+ if (!toolName) {
3540
+ respond(false, { error: "Missing required argument: tool" });
3541
+ return;
3542
+ }
3543
+
3544
+ const startTime = Date.now();
3545
+ let result: any;
3546
+ let error: string | undefined;
3547
+
3548
+ // Test the specified tool
3549
+ try {
3550
+ switch (toolName) {
3551
+ case "memory_store":
3552
+ const mem = await client.store("Test memory", { test: "true" });
3553
+ await client.delete(mem.id);
3554
+ result = { success: true, message: "Memory stored and deleted successfully" };
3555
+ break;
3556
+
3557
+ case "memory_recall":
3558
+ const searchResults = await client.search("test", 1, 0.5);
3559
+ result = { success: true, results: searchResults.length, message: "Search completed" };
3560
+ break;
3561
+
3562
+ case "memory_list":
3563
+ const list = await client.list(5);
3564
+ result = { success: true, count: list.length, message: "List retrieved" };
3565
+ break;
3566
+
3567
+ case "project_list":
3568
+ const projects = await client.listProjects(5);
3569
+ result = { success: true, count: projects.length, message: "Projects listed" };
3570
+ break;
3571
+
3572
+ case "memory_health":
3573
+ const health = await client.health();
3574
+ result = { success: true, status: health.status, message: "Health check passed" };
3575
+ break;
3576
+
3577
+ default:
3578
+ result = { success: false, message: `Unknown tool: ${toolName}` };
3579
+ }
3580
+ } catch (err) {
3581
+ error = String(err);
3582
+ result = { success: false, error };
3583
+ }
3584
+
3585
+ const duration = Date.now() - startTime;
3586
+
3587
+ respond(true, {
3588
+ tool: toolName,
3589
+ duration,
3590
+ result,
3591
+ error,
3592
+ });
3593
+ } catch (err) {
3594
+ respond(false, { error: String(err) });
3595
+ }
3596
+ });
785
3597
  }