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