@memoryrelay/plugin-memoryrelay-ai 0.15.6 → 0.16.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.
Files changed (46) hide show
  1. package/README.md +2 -3
  2. package/index.ts +472 -4849
  3. package/openclaw.plugin.json +41 -3
  4. package/package.json +1 -1
  5. package/skills/decision-tracking/SKILL.md +1 -1
  6. package/skills/entity-and-context/SKILL.md +1 -1
  7. package/skills/memory-workflow/SKILL.md +1 -1
  8. package/src/client/memoryrelay-client.ts +816 -0
  9. package/src/context/namespace-router.ts +19 -0
  10. package/src/context/request-context.ts +39 -0
  11. package/src/context/session-resolver.ts +93 -0
  12. package/src/filters/content-patterns.ts +32 -0
  13. package/src/filters/noise-patterns.ts +33 -0
  14. package/src/filters/non-interactive.ts +30 -0
  15. package/src/hooks/activity.ts +51 -0
  16. package/src/hooks/agent-end.ts +48 -0
  17. package/src/hooks/before-agent-start.ts +109 -0
  18. package/src/hooks/before-prompt-build.ts +46 -0
  19. package/src/hooks/compaction.ts +51 -0
  20. package/src/hooks/privacy.ts +44 -0
  21. package/src/hooks/session-lifecycle.ts +47 -0
  22. package/src/hooks/subagent.ts +62 -0
  23. package/src/pipelines/capture/content-strip.ts +14 -0
  24. package/src/pipelines/capture/dedup.ts +17 -0
  25. package/src/pipelines/capture/index.ts +13 -0
  26. package/src/pipelines/capture/message-filter.ts +16 -0
  27. package/src/pipelines/capture/store.ts +33 -0
  28. package/src/pipelines/capture/trigger-gate.ts +21 -0
  29. package/src/pipelines/capture/truncate.ts +16 -0
  30. package/src/pipelines/recall/format.ts +30 -0
  31. package/src/pipelines/recall/index.ts +12 -0
  32. package/src/pipelines/recall/rank.ts +40 -0
  33. package/src/pipelines/recall/scope-resolver.ts +20 -0
  34. package/src/pipelines/recall/search.ts +43 -0
  35. package/src/pipelines/recall/trigger-gate.ts +17 -0
  36. package/src/pipelines/runner.ts +25 -0
  37. package/src/pipelines/types.ts +157 -0
  38. package/src/tools/agent-tools.ts +127 -0
  39. package/src/tools/decision-tools.ts +309 -0
  40. package/src/tools/entity-tools.ts +215 -0
  41. package/src/tools/health-tools.ts +42 -0
  42. package/src/tools/memory-tools.ts +690 -0
  43. package/src/tools/pattern-tools.ts +250 -0
  44. package/src/tools/project-tools.ts +444 -0
  45. package/src/tools/session-tools.ts +195 -0
  46. package/src/tools/v2-tools.ts +228 -0
@@ -0,0 +1,309 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { PluginConfig } from "../pipelines/types.js";
3
+ import type { MemoryRelayClient } from "../client/memoryrelay-client.js";
4
+
5
+ export function registerDecisionTools(
6
+ api: OpenClawPluginApi,
7
+ config: PluginConfig,
8
+ client: MemoryRelayClient,
9
+ isToolEnabled: (name: string) => boolean,
10
+ ): void {
11
+ const defaultProject = config.defaultProject;
12
+
13
+ // --------------------------------------------------------------------------
14
+ // 21. decision_record
15
+ // --------------------------------------------------------------------------
16
+ if (isToolEnabled("decision_record")) {
17
+ api.registerTool((ctx) => ({
18
+
19
+ name: "decision_record",
20
+ description:
21
+ "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." +
22
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : ""),
23
+ parameters: {
24
+ type: "object",
25
+ properties: {
26
+ title: {
27
+ type: "string",
28
+ description: "Short title summarizing the decision.",
29
+ },
30
+ rationale: {
31
+ type: "string",
32
+ description: "Why this decision was made. Include context and reasoning.",
33
+ },
34
+ alternatives: {
35
+ type: "string",
36
+ description: "What alternatives were considered and why they were rejected.",
37
+ },
38
+ project: {
39
+ type: "string",
40
+ description: "Project slug this decision applies to.",
41
+ },
42
+ tags: {
43
+ type: "array",
44
+ description: "Tags for categorizing the decision.",
45
+ items: { type: "string" },
46
+ },
47
+ status: {
48
+ type: "string",
49
+ description: "Decision status.",
50
+ enum: ["active", "experimental"],
51
+ },
52
+ metadata: {
53
+ type: "object",
54
+ description: "Optional key-value metadata to attach to the decision.",
55
+ additionalProperties: { type: "string" },
56
+ },
57
+ },
58
+ required: ["title", "rationale"],
59
+ },
60
+ execute: async (
61
+ _id,
62
+ args: {
63
+ title: string;
64
+ rationale: string;
65
+ alternatives?: string;
66
+ project?: string;
67
+ tags?: string[];
68
+ status?: string;
69
+ metadata?: Record<string, string>;
70
+ },
71
+ ) => {
72
+ try {
73
+ const project = args.project ?? defaultProject;
74
+
75
+ // Merge user-provided metadata with sender identity from tool context
76
+ const metadata: Record<string, string> = { ...(args.metadata ?? {}) };
77
+ if (ctx.requesterSenderId) {
78
+ metadata.sender_id = ctx.requesterSenderId;
79
+ }
80
+
81
+ const result = await client.recordDecision(
82
+ args.title,
83
+ args.rationale,
84
+ args.alternatives,
85
+ project,
86
+ args.tags,
87
+ args.status,
88
+ Object.keys(metadata).length > 0 ? metadata : undefined,
89
+ );
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
92
+ details: { result },
93
+ };
94
+ } catch (err) {
95
+ return {
96
+ content: [{ type: "text", text: `Failed to record decision: ${String(err)}` }],
97
+ details: { error: String(err) },
98
+ };
99
+ }
100
+ },
101
+ }),
102
+ { name: "decision_record" },
103
+ );
104
+ }
105
+
106
+ // --------------------------------------------------------------------------
107
+ // 22. decision_list
108
+ // --------------------------------------------------------------------------
109
+ if (isToolEnabled("decision_list")) {
110
+ api.registerTool((ctx) => ({
111
+
112
+ name: "decision_list",
113
+ description: "List recorded decisions, optionally filtered by project, status, or tags." +
114
+ (defaultProject ? ` Scoped to project '${defaultProject}' by default.` : ""),
115
+ parameters: {
116
+ type: "object",
117
+ properties: {
118
+ limit: {
119
+ type: "number",
120
+ description: "Maximum decisions to return. Default 20.",
121
+ minimum: 1,
122
+ maximum: 100,
123
+ },
124
+ project: {
125
+ type: "string",
126
+ description: "Filter by project slug.",
127
+ },
128
+ status: {
129
+ type: "string",
130
+ description: "Filter by status.",
131
+ enum: ["active", "superseded", "reverted", "experimental"],
132
+ },
133
+ tags: {
134
+ type: "string",
135
+ description: "Comma-separated tags to filter by.",
136
+ },
137
+ },
138
+ },
139
+ execute: async (
140
+ _id,
141
+ args: { limit?: number; project?: string; status?: string; tags?: string },
142
+ ) => {
143
+ try {
144
+ const project = args.project ?? defaultProject;
145
+ const result = await client.listDecisions(args.limit, project, args.status, args.tags);
146
+ return {
147
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
148
+ details: { result },
149
+ };
150
+ } catch (err) {
151
+ return {
152
+ content: [{ type: "text", text: `Failed to list decisions: ${String(err)}` }],
153
+ details: { error: String(err) },
154
+ };
155
+ }
156
+ },
157
+ }),
158
+ { name: "decision_list" },
159
+ );
160
+ }
161
+
162
+ // --------------------------------------------------------------------------
163
+ // 23. decision_supersede
164
+ // --------------------------------------------------------------------------
165
+ if (isToolEnabled("decision_supersede")) {
166
+ api.registerTool((ctx) => ({
167
+
168
+ name: "decision_supersede",
169
+ description:
170
+ "Supersede an existing decision with a new one. The old decision is marked as superseded and linked to the replacement.",
171
+ parameters: {
172
+ type: "object",
173
+ properties: {
174
+ id: {
175
+ type: "string",
176
+ description: "ID of the decision to supersede.",
177
+ },
178
+ title: {
179
+ type: "string",
180
+ description: "Title of the new replacement decision.",
181
+ },
182
+ rationale: {
183
+ type: "string",
184
+ description: "Why the previous decision is being replaced.",
185
+ },
186
+ alternatives: {
187
+ type: "string",
188
+ description: "Alternatives considered for the new decision.",
189
+ },
190
+ tags: {
191
+ type: "array",
192
+ description: "Tags for the new decision.",
193
+ items: { type: "string" },
194
+ },
195
+ metadata: {
196
+ type: "object",
197
+ description: "Optional key-value metadata to attach to the new decision.",
198
+ additionalProperties: { type: "string" },
199
+ },
200
+ },
201
+ required: ["id", "title", "rationale"],
202
+ },
203
+ execute: async (
204
+ _id,
205
+ args: {
206
+ id: string;
207
+ title: string;
208
+ rationale: string;
209
+ alternatives?: string;
210
+ tags?: string[];
211
+ metadata?: Record<string, string>;
212
+ },
213
+ ) => {
214
+ try {
215
+ const result = await client.supersedeDecision(
216
+ args.id,
217
+ args.title,
218
+ args.rationale,
219
+ args.alternatives,
220
+ args.tags,
221
+ args.metadata,
222
+ );
223
+ return {
224
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
225
+ details: { result },
226
+ };
227
+ } catch (err) {
228
+ return {
229
+ content: [{ type: "text", text: `Failed to supersede decision: ${String(err)}` }],
230
+ details: { error: String(err) },
231
+ };
232
+ }
233
+ },
234
+ }),
235
+ { name: "decision_supersede" },
236
+ );
237
+ }
238
+
239
+ // --------------------------------------------------------------------------
240
+ // 24. decision_check
241
+ // --------------------------------------------------------------------------
242
+ if (isToolEnabled("decision_check")) {
243
+ api.registerTool((ctx) => ({
244
+
245
+ name: "decision_check",
246
+ description:
247
+ "Check if there are existing decisions relevant to a topic. ALWAYS call this before making architectural choices to avoid contradicting past decisions." +
248
+ (defaultProject ? ` Scoped to project '${defaultProject}' by default.` : ""),
249
+ parameters: {
250
+ type: "object",
251
+ properties: {
252
+ query: {
253
+ type: "string",
254
+ description: "Natural language description of the topic or decision area.",
255
+ },
256
+ project: {
257
+ type: "string",
258
+ description: "Project slug to scope the search.",
259
+ },
260
+ limit: {
261
+ type: "number",
262
+ description: "Maximum results. Default 5.",
263
+ },
264
+ threshold: {
265
+ type: "number",
266
+ description: "Minimum similarity threshold (0-1). Default 0.3.",
267
+ },
268
+ include_superseded: {
269
+ type: "boolean",
270
+ description: "Include superseded decisions in results. Default false.",
271
+ },
272
+ },
273
+ required: ["query"],
274
+ },
275
+ execute: async (
276
+ _id,
277
+ args: {
278
+ query: string;
279
+ project?: string;
280
+ limit?: number;
281
+ threshold?: number;
282
+ include_superseded?: boolean;
283
+ },
284
+ ) => {
285
+ try {
286
+ const project = args.project ?? defaultProject;
287
+ const result = await client.checkDecisions(
288
+ args.query,
289
+ project,
290
+ args.limit,
291
+ args.threshold,
292
+ args.include_superseded,
293
+ );
294
+ return {
295
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
296
+ details: { result },
297
+ };
298
+ } catch (err) {
299
+ return {
300
+ content: [{ type: "text", text: `Failed to check decisions: ${String(err)}` }],
301
+ details: { error: String(err) },
302
+ };
303
+ }
304
+ },
305
+ }),
306
+ { name: "decision_check" },
307
+ );
308
+ }
309
+ }
@@ -0,0 +1,215 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { PluginConfig } from "../pipelines/types.js";
3
+ import type { MemoryRelayClient } from "../client/memoryrelay-client.js";
4
+
5
+ export function registerEntityTools(
6
+ api: OpenClawPluginApi,
7
+ config: PluginConfig,
8
+ client: MemoryRelayClient,
9
+ isToolEnabled: (name: string) => boolean,
10
+ ): void {
11
+
12
+ // --------------------------------------------------------------------------
13
+ // 10. entity_create
14
+ // --------------------------------------------------------------------------
15
+ if (isToolEnabled("entity_create")) {
16
+ api.registerTool((ctx) => ({
17
+
18
+ name: "entity_create",
19
+ description:
20
+ "Create a named entity (person, place, organization, project, concept) for the knowledge graph. Entities help organize and connect memories.",
21
+ parameters: {
22
+ type: "object",
23
+ properties: {
24
+ name: {
25
+ type: "string",
26
+ description: "Entity name (1-200 characters).",
27
+ },
28
+ type: {
29
+ type: "string",
30
+ description: "Entity type classification.",
31
+ enum: ["person", "place", "organization", "project", "concept", "other"],
32
+ },
33
+ metadata: {
34
+ type: "object",
35
+ description: "Optional key-value metadata.",
36
+ additionalProperties: { type: "string" },
37
+ },
38
+ },
39
+ required: ["name", "type"],
40
+ },
41
+ execute: async (
42
+ _id,
43
+ args: { name: string; type: string; metadata?: Record<string, string> },
44
+ ) => {
45
+ try {
46
+ const result = await client.createEntity(args.name, args.type, args.metadata);
47
+ return {
48
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
49
+ details: { result },
50
+ };
51
+ } catch (err) {
52
+ return {
53
+ content: [{ type: "text", text: `Failed to create entity: ${String(err)}` }],
54
+ details: { error: String(err) },
55
+ };
56
+ }
57
+ },
58
+ }),
59
+ { name: "entity_create" },
60
+ );
61
+ }
62
+
63
+ // --------------------------------------------------------------------------
64
+ // 11. entity_link
65
+ // --------------------------------------------------------------------------
66
+ if (isToolEnabled("entity_link")) {
67
+ api.registerTool((ctx) => ({
68
+
69
+ name: "entity_link",
70
+ description: "Link an entity to a memory to establish relationships in the knowledge graph.",
71
+ parameters: {
72
+ type: "object",
73
+ properties: {
74
+ entity_id: {
75
+ type: "string",
76
+ description: "Entity UUID.",
77
+ },
78
+ memory_id: {
79
+ type: "string",
80
+ description: "Memory UUID.",
81
+ },
82
+ relationship: {
83
+ type: "string",
84
+ description:
85
+ 'Relationship type (e.g., "mentioned_in", "created_by", "relates_to"). Default "mentioned_in".',
86
+ },
87
+ },
88
+ required: ["entity_id", "memory_id"],
89
+ },
90
+ execute: async (
91
+ _id,
92
+ args: { entity_id: string; memory_id: string; relationship?: string },
93
+ ) => {
94
+ try {
95
+ const result = await client.linkEntity(
96
+ args.entity_id,
97
+ args.memory_id,
98
+ args.relationship,
99
+ );
100
+ return {
101
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
102
+ details: { result },
103
+ };
104
+ } catch (err) {
105
+ return {
106
+ content: [{ type: "text", text: `Failed to link entity: ${String(err)}` }],
107
+ details: { error: String(err) },
108
+ };
109
+ }
110
+ },
111
+ }),
112
+ { name: "entity_link" },
113
+ );
114
+ }
115
+
116
+ // --------------------------------------------------------------------------
117
+ // 12. entity_list
118
+ // --------------------------------------------------------------------------
119
+ if (isToolEnabled("entity_list")) {
120
+ api.registerTool((ctx) => ({
121
+
122
+ name: "entity_list",
123
+ description: "List entities in the knowledge graph.",
124
+ parameters: {
125
+ type: "object",
126
+ properties: {
127
+ limit: {
128
+ type: "number",
129
+ description: "Maximum entities to return. Default 20.",
130
+ minimum: 1,
131
+ maximum: 100,
132
+ },
133
+ offset: {
134
+ type: "number",
135
+ description: "Offset for pagination. Default 0.",
136
+ minimum: 0,
137
+ },
138
+ },
139
+ },
140
+ execute: async (_id, args: { limit?: number; offset?: number }) => {
141
+ try {
142
+ const result = await client.listEntities(args.limit, args.offset);
143
+ return {
144
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
145
+ details: { result },
146
+ };
147
+ } catch (err) {
148
+ return {
149
+ content: [{ type: "text", text: `Failed to list entities: ${String(err)}` }],
150
+ details: { error: String(err) },
151
+ };
152
+ }
153
+ },
154
+ }),
155
+ { name: "entity_list" },
156
+ );
157
+ }
158
+
159
+ // --------------------------------------------------------------------------
160
+ // 13. entity_graph
161
+ // --------------------------------------------------------------------------
162
+ if (isToolEnabled("entity_graph")) {
163
+ api.registerTool((ctx) => ({
164
+
165
+ name: "entity_graph",
166
+ description:
167
+ "Explore the knowledge graph around an entity. Returns the entity and its neighborhood of connected entities and memories.",
168
+ parameters: {
169
+ type: "object",
170
+ properties: {
171
+ entity_id: {
172
+ type: "string",
173
+ description: "Entity UUID to explore from.",
174
+ },
175
+ depth: {
176
+ type: "number",
177
+ description: "How many hops to traverse. Default 2.",
178
+ minimum: 1,
179
+ maximum: 5,
180
+ },
181
+ max_neighbors: {
182
+ type: "number",
183
+ description: "Maximum neighbors per node. Default 10.",
184
+ minimum: 1,
185
+ maximum: 50,
186
+ },
187
+ },
188
+ required: ["entity_id"],
189
+ },
190
+ execute: async (
191
+ _id,
192
+ args: { entity_id: string; depth?: number; max_neighbors?: number },
193
+ ) => {
194
+ try {
195
+ const result = await client.entityGraph(
196
+ args.entity_id,
197
+ args.depth,
198
+ args.max_neighbors,
199
+ );
200
+ return {
201
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
202
+ details: { result },
203
+ };
204
+ } catch (err) {
205
+ return {
206
+ content: [{ type: "text", text: `Failed to get entity graph: ${String(err)}` }],
207
+ details: { error: String(err) },
208
+ };
209
+ }
210
+ },
211
+ }),
212
+ { name: "entity_graph" },
213
+ );
214
+ }
215
+ }
@@ -0,0 +1,42 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { PluginConfig } from "../pipelines/types.js";
3
+ import type { MemoryRelayClient } from "../client/memoryrelay-client.js";
4
+
5
+ export function registerHealthTools(
6
+ api: OpenClawPluginApi,
7
+ config: PluginConfig,
8
+ client: MemoryRelayClient,
9
+ isToolEnabled: (name: string) => boolean,
10
+ ): void {
11
+
12
+ // --------------------------------------------------------------------------
13
+ // 39. memory_health
14
+ // --------------------------------------------------------------------------
15
+ if (isToolEnabled("memory_health")) {
16
+ api.registerTool((ctx) => ({
17
+
18
+ name: "memory_health",
19
+ description: "Check the MemoryRelay API connectivity and health status.",
20
+ parameters: {
21
+ type: "object",
22
+ properties: {},
23
+ },
24
+ execute: async () => {
25
+ try {
26
+ const health = await client.health();
27
+ return {
28
+ content: [{ type: "text", text: JSON.stringify(health, null, 2) }],
29
+ details: { health },
30
+ };
31
+ } catch (err) {
32
+ return {
33
+ content: [{ type: "text", text: `Health check failed: ${String(err)}` }],
34
+ details: { error: String(err) },
35
+ };
36
+ }
37
+ },
38
+ }),
39
+ { name: "memory_health" },
40
+ );
41
+ }
42
+ }