@memoryrelay/plugin-memoryrelay-ai 0.12.11 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -178
- package/index.ts +1035 -25
- package/openclaw.plugin.json +82 -4
- package/package.json +4 -3
- package/skills/codebase-navigation/SKILL.md +105 -0
- package/skills/decision-tracking/SKILL.md +50 -0
- package/skills/entity-and-context/SKILL.md +62 -0
- package/skills/memory-workflow/SKILL.md +84 -0
- package/skills/pattern-management/SKILL.md +57 -0
- package/skills/project-orchestration/SKILL.md +72 -0
- package/skills/release-process/SKILL.md +83 -0
- package/skills/testing-memoryrelay/SKILL.md +87 -0
package/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenClaw Memory Plugin - MemoryRelay
|
|
3
|
-
* Version: 0.
|
|
3
|
+
* Version: 0.13.0 (SDK Enhancements)
|
|
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
7
|
* Includes: memories, entities, agents, sessions, decisions, patterns, projects.
|
|
8
|
-
* New in v0.
|
|
9
|
-
* New in v0.12.
|
|
8
|
+
* New in v0.13.0: External session IDs, get-or-create sessions, multi-agent collaboration
|
|
9
|
+
* New in v0.12.7: OpenClaw session context integration for session tracking
|
|
10
|
+
* New in v0.12.0: Smart auto-capture, daily stats, CLI commands, onboarding
|
|
10
11
|
*
|
|
11
12
|
* API: https://api.memoryrelay.net
|
|
12
13
|
* Docs: https://memoryrelay.ai
|
|
@@ -583,6 +584,44 @@ function isBlocklisted(content: string, blocklist: string[]): boolean {
|
|
|
583
584
|
});
|
|
584
585
|
}
|
|
585
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Redact sensitive patterns from content using the blocklist.
|
|
589
|
+
* Returns the content with matches replaced by [REDACTED].
|
|
590
|
+
*/
|
|
591
|
+
function redactSensitive(content: string, blocklist: string[]): string {
|
|
592
|
+
let redacted = content;
|
|
593
|
+
for (const pattern of blocklist) {
|
|
594
|
+
try {
|
|
595
|
+
redacted = redacted.replace(new RegExp(pattern, "gi"), "[REDACTED]");
|
|
596
|
+
} catch {
|
|
597
|
+
// Invalid regex, skip
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return redacted;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Extract storable content from messages about to be lost (compaction/reset).
|
|
605
|
+
* Only keeps assistant messages longer than 200 chars.
|
|
606
|
+
* Respects the privacy blocklist.
|
|
607
|
+
*/
|
|
608
|
+
function extractRescueContent(
|
|
609
|
+
messages: unknown[],
|
|
610
|
+
blocklist: string[]
|
|
611
|
+
): string[] {
|
|
612
|
+
const rescued: string[] = [];
|
|
613
|
+
for (const msg of messages) {
|
|
614
|
+
if (!msg || typeof msg !== "object") continue;
|
|
615
|
+
const m = msg as Record<string, unknown>;
|
|
616
|
+
if (m.role !== "assistant") continue;
|
|
617
|
+
const content = typeof m.content === "string" ? m.content : "";
|
|
618
|
+
if (content.length < 200) continue;
|
|
619
|
+
if (isBlocklisted(content, blocklist)) continue;
|
|
620
|
+
rescued.push(content.slice(0, 500));
|
|
621
|
+
}
|
|
622
|
+
return rescued.slice(0, 3);
|
|
623
|
+
}
|
|
624
|
+
|
|
586
625
|
/**
|
|
587
626
|
* Mask sensitive data in content (API keys, tokens, etc.)
|
|
588
627
|
*/
|
|
@@ -670,7 +709,7 @@ class MemoryRelayClient {
|
|
|
670
709
|
headers: {
|
|
671
710
|
"Content-Type": "application/json",
|
|
672
711
|
Authorization: `Bearer ${this.apiKey}`,
|
|
673
|
-
"User-Agent": "openclaw-memory-memoryrelay/0.
|
|
712
|
+
"User-Agent": "openclaw-memory-memoryrelay/0.13.0",
|
|
674
713
|
},
|
|
675
714
|
body: body ? JSON.stringify(body) : undefined,
|
|
676
715
|
},
|
|
@@ -1020,6 +1059,7 @@ class MemoryRelayClient {
|
|
|
1020
1059
|
project?: string,
|
|
1021
1060
|
tags?: string[],
|
|
1022
1061
|
status?: string,
|
|
1062
|
+
metadata?: Record<string, string>,
|
|
1023
1063
|
): Promise<any> {
|
|
1024
1064
|
return this.request("POST", "/v1/decisions", {
|
|
1025
1065
|
title,
|
|
@@ -1028,6 +1068,7 @@ class MemoryRelayClient {
|
|
|
1028
1068
|
project_slug: project,
|
|
1029
1069
|
tags,
|
|
1030
1070
|
status,
|
|
1071
|
+
metadata,
|
|
1031
1072
|
agent_id: this.agentId,
|
|
1032
1073
|
});
|
|
1033
1074
|
}
|
|
@@ -1314,7 +1355,9 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
1314
1355
|
const verboseEnabled = cfg?.verbose || false;
|
|
1315
1356
|
const logFile = cfg?.logFile;
|
|
1316
1357
|
const maxLogEntries = cfg?.maxLogEntries || 100;
|
|
1317
|
-
|
|
1358
|
+
const sessionTimeoutMs = ((cfg?.sessionTimeoutMinutes as number) || 120) * 60 * 1000;
|
|
1359
|
+
const sessionCleanupIntervalMs = ((cfg?.sessionCleanupIntervalMinutes as number) || 30) * 60 * 1000;
|
|
1360
|
+
|
|
1318
1361
|
let debugLogger: DebugLogger | undefined;
|
|
1319
1362
|
let statusReporter: StatusReporter | undefined;
|
|
1320
1363
|
|
|
@@ -1333,14 +1376,14 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
1333
1376
|
const client = new MemoryRelayClient(apiKey, agentId, apiUrl, debugLogger, statusReporter);
|
|
1334
1377
|
|
|
1335
1378
|
// ========================================================================
|
|
1336
|
-
// Session Cache for External Session IDs (v0.
|
|
1379
|
+
// Session Cache for External Session IDs (v0.13.0)
|
|
1337
1380
|
// ========================================================================
|
|
1338
1381
|
|
|
1339
1382
|
/**
|
|
1340
1383
|
* Cache mapping: external_id → MemoryRelay session_id
|
|
1341
1384
|
* Enables multi-agent collaboration and conversation-spanning sessions
|
|
1342
1385
|
*/
|
|
1343
|
-
const sessionCache = new Map<string, string>();
|
|
1386
|
+
const sessionCache = new Map<string, { sessionId: string; lastActivityAt: number }>();
|
|
1344
1387
|
|
|
1345
1388
|
/**
|
|
1346
1389
|
* Get or create MemoryRelay session for current workspace/project context.
|
|
@@ -1369,10 +1412,9 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
1369
1412
|
|
|
1370
1413
|
// Check cache first
|
|
1371
1414
|
if (sessionCache.has(externalId)) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
return sessionCache.get(externalId)!;
|
|
1415
|
+
api.logger.debug?.(`Session: Cache hit for external_id="${externalId}"`);
|
|
1416
|
+
touchSession(externalId);
|
|
1417
|
+
return sessionCache.get(externalId)!.sessionId;
|
|
1376
1418
|
}
|
|
1377
1419
|
|
|
1378
1420
|
try {
|
|
@@ -1386,24 +1428,26 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
1386
1428
|
);
|
|
1387
1429
|
|
|
1388
1430
|
// Cache the mapping
|
|
1389
|
-
sessionCache.set(externalId, response.id);
|
|
1431
|
+
sessionCache.set(externalId, { sessionId: response.id, lastActivityAt: Date.now() });
|
|
1390
1432
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
"info"
|
|
1395
|
-
);
|
|
1396
|
-
}
|
|
1433
|
+
api.logger.debug?.(
|
|
1434
|
+
`Session: ${response.created ? 'Created' : 'Retrieved'} session ${response.id} for external_id="${externalId}"`
|
|
1435
|
+
);
|
|
1397
1436
|
|
|
1398
1437
|
return response.id;
|
|
1399
1438
|
} catch (err) {
|
|
1400
|
-
|
|
1401
|
-
debugLogger.log(`Session: Failed to get-or-create session for ${externalId}: ${String(err)}`, "error");
|
|
1402
|
-
}
|
|
1439
|
+
api.logger.debug?.(`Session: Failed to get-or-create session for ${externalId}: ${String(err)}`);
|
|
1403
1440
|
return null;
|
|
1404
1441
|
}
|
|
1405
1442
|
}
|
|
1406
1443
|
|
|
1444
|
+
function touchSession(externalId: string): void {
|
|
1445
|
+
const entry = sessionCache.get(externalId);
|
|
1446
|
+
if (entry) {
|
|
1447
|
+
entry.lastActivityAt = Date.now();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1407
1451
|
// Verify connection on startup (with timeout)
|
|
1408
1452
|
try {
|
|
1409
1453
|
await client.health();
|
|
@@ -1632,8 +1676,14 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
1632
1676
|
},
|
|
1633
1677
|
) => {
|
|
1634
1678
|
try {
|
|
1635
|
-
const { content, metadata, session_id: explicitSessionId, ...opts } = args;
|
|
1636
|
-
|
|
1679
|
+
const { content, metadata: rawMetadata, session_id: explicitSessionId, ...opts } = args;
|
|
1680
|
+
|
|
1681
|
+
// Auto-tag with sender identity from tool context
|
|
1682
|
+
const metadata = rawMetadata || {};
|
|
1683
|
+
if (ctx.requesterSenderId && !metadata.sender_id) {
|
|
1684
|
+
metadata.sender_id = ctx.requesterSenderId;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1637
1687
|
// Apply defaultProject fallback before session resolution
|
|
1638
1688
|
if (!opts.project && defaultProject) opts.project = defaultProject;
|
|
1639
1689
|
|
|
@@ -2051,6 +2101,17 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
2051
2101
|
args: { memories: Array<{ content: string; metadata?: Record<string, string> }> },
|
|
2052
2102
|
) => {
|
|
2053
2103
|
try {
|
|
2104
|
+
// Auto-tag each memory with sender identity from tool context
|
|
2105
|
+
if (ctx.requesterSenderId) {
|
|
2106
|
+
for (const mem of args.memories) {
|
|
2107
|
+
const metadata = mem.metadata || {};
|
|
2108
|
+
if (!metadata.sender_id) {
|
|
2109
|
+
metadata.sender_id = ctx.requesterSenderId;
|
|
2110
|
+
}
|
|
2111
|
+
mem.metadata = metadata;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2054
2115
|
const result = await client.batchStore(args.memories);
|
|
2055
2116
|
return {
|
|
2056
2117
|
content: [
|
|
@@ -2732,6 +2793,11 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
2732
2793
|
description: "Decision status.",
|
|
2733
2794
|
enum: ["active", "experimental"],
|
|
2734
2795
|
},
|
|
2796
|
+
metadata: {
|
|
2797
|
+
type: "object",
|
|
2798
|
+
description: "Optional key-value metadata to attach to the decision.",
|
|
2799
|
+
additionalProperties: { type: "string" },
|
|
2800
|
+
},
|
|
2735
2801
|
},
|
|
2736
2802
|
required: ["title", "rationale"],
|
|
2737
2803
|
},
|
|
@@ -2744,10 +2810,18 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
2744
2810
|
project?: string;
|
|
2745
2811
|
tags?: string[];
|
|
2746
2812
|
status?: string;
|
|
2813
|
+
metadata?: Record<string, string>;
|
|
2747
2814
|
},
|
|
2748
2815
|
) => {
|
|
2749
2816
|
try {
|
|
2750
2817
|
const project = args.project ?? defaultProject;
|
|
2818
|
+
|
|
2819
|
+
// Merge user-provided metadata with sender identity from tool context
|
|
2820
|
+
const metadata: Record<string, string> = { ...(args.metadata ?? {}) };
|
|
2821
|
+
if (ctx.requesterSenderId) {
|
|
2822
|
+
metadata.sender_id = ctx.requesterSenderId;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2751
2825
|
const result = await client.recordDecision(
|
|
2752
2826
|
args.title,
|
|
2753
2827
|
args.rationale,
|
|
@@ -2755,6 +2829,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
2755
2829
|
project,
|
|
2756
2830
|
args.tags,
|
|
2757
2831
|
args.status,
|
|
2832
|
+
Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
2758
2833
|
);
|
|
2759
2834
|
return {
|
|
2760
2835
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -3969,8 +4044,199 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
3969
4044
|
});
|
|
3970
4045
|
}
|
|
3971
4046
|
|
|
4047
|
+
// Session sync: auto-create MemoryRelay session when OpenClaw session starts
|
|
4048
|
+
api.on("session_start", async (event, _ctx) => {
|
|
4049
|
+
try {
|
|
4050
|
+
const externalId = event.sessionKey || event.sessionId;
|
|
4051
|
+
if (!externalId) return;
|
|
4052
|
+
|
|
4053
|
+
const response = await client.getOrCreateSession(
|
|
4054
|
+
externalId,
|
|
4055
|
+
agentId,
|
|
4056
|
+
`OpenClaw session ${externalId}`,
|
|
4057
|
+
defaultProject || undefined,
|
|
4058
|
+
{ source: "openclaw-plugin", agent: agentId, trigger: "session_start_hook" },
|
|
4059
|
+
);
|
|
4060
|
+
|
|
4061
|
+
sessionCache.set(externalId, {
|
|
4062
|
+
sessionId: response.id,
|
|
4063
|
+
lastActivityAt: Date.now(),
|
|
4064
|
+
});
|
|
4065
|
+
|
|
4066
|
+
api.logger.debug?.(`memory-memoryrelay: auto-created session ${response.id} for OpenClaw session ${externalId}`);
|
|
4067
|
+
} catch (err) {
|
|
4068
|
+
api.logger.warn?.(`memory-memoryrelay: session_start hook failed: ${String(err)}`);
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
|
|
4072
|
+
// Session sync: auto-end MemoryRelay session when OpenClaw session ends
|
|
4073
|
+
api.on("session_end", async (event, _ctx) => {
|
|
4074
|
+
try {
|
|
4075
|
+
const externalId = event.sessionKey || event.sessionId;
|
|
4076
|
+
if (!externalId) return;
|
|
4077
|
+
|
|
4078
|
+
const entry = sessionCache.get(externalId);
|
|
4079
|
+
if (!entry) return;
|
|
4080
|
+
|
|
4081
|
+
await client.endSession(entry.sessionId, `Session ended after ${event.messageCount} messages`);
|
|
4082
|
+
sessionCache.delete(externalId);
|
|
4083
|
+
|
|
4084
|
+
api.logger.debug?.(`memory-memoryrelay: auto-ended session ${entry.sessionId}`);
|
|
4085
|
+
} catch (err) {
|
|
4086
|
+
api.logger.warn?.(`memory-memoryrelay: session_end hook failed: ${String(err)}`);
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
|
|
4090
|
+
// ==========================================================================
|
|
4091
|
+
// Tool Observation Hooks
|
|
4092
|
+
// ==========================================================================
|
|
4093
|
+
|
|
4094
|
+
// Tool observation: no-op, registered for future extensibility
|
|
4095
|
+
api.on("before_tool_call", (_event, _ctx) => {
|
|
4096
|
+
// Reserved for future: tool blocking, param injection, audit
|
|
4097
|
+
});
|
|
4098
|
+
|
|
4099
|
+
// Tool observation: update session activity + log metrics
|
|
4100
|
+
api.on("after_tool_call", (event, _ctx) => {
|
|
4101
|
+
// Update activity timestamp on all active sessions
|
|
4102
|
+
for (const entry of sessionCache.values()) {
|
|
4103
|
+
entry.lastActivityAt = Date.now();
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
// Log to debug logger if enabled
|
|
4107
|
+
if (debugLogger) {
|
|
4108
|
+
debugLogger.log({
|
|
4109
|
+
timestamp: new Date().toISOString(),
|
|
4110
|
+
tool: event.toolName,
|
|
4111
|
+
method: "tool_call",
|
|
4112
|
+
path: "",
|
|
4113
|
+
duration: event.durationMs || 0,
|
|
4114
|
+
status: event.error ? "error" : "success",
|
|
4115
|
+
error: event.error,
|
|
4116
|
+
});
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
4119
|
+
|
|
4120
|
+
// Compaction rescue: save key context before it's lost
|
|
4121
|
+
api.on("before_compaction", async (event, _ctx) => {
|
|
4122
|
+
if (!event.messages || event.messages.length === 0) return;
|
|
4123
|
+
try {
|
|
4124
|
+
const rescued = extractRescueContent(event.messages, autoCaptureConfig.blocklist || []);
|
|
4125
|
+
for (const content of rescued) {
|
|
4126
|
+
await client.store(content, {
|
|
4127
|
+
category: "compaction-rescue",
|
|
4128
|
+
source: "auto-compaction",
|
|
4129
|
+
agent: agentId,
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
if (rescued.length > 0) {
|
|
4133
|
+
api.logger.info?.(`memory-memoryrelay: rescued ${rescued.length} memories before compaction`);
|
|
4134
|
+
}
|
|
4135
|
+
} catch (err) {
|
|
4136
|
+
api.logger.warn?.(`memory-memoryrelay: compaction rescue failed: ${String(err)}`);
|
|
4137
|
+
}
|
|
4138
|
+
});
|
|
4139
|
+
|
|
4140
|
+
// Session reset rescue: save key context before session is cleared
|
|
4141
|
+
api.on("before_reset", async (event, _ctx) => {
|
|
4142
|
+
if (!event.messages || event.messages.length === 0) return;
|
|
4143
|
+
try {
|
|
4144
|
+
const rescued = extractRescueContent(event.messages, autoCaptureConfig.blocklist || []);
|
|
4145
|
+
for (const content of rescued) {
|
|
4146
|
+
await client.store(content, {
|
|
4147
|
+
category: "session-reset-rescue",
|
|
4148
|
+
source: "auto-reset",
|
|
4149
|
+
agent: agentId,
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
if (rescued.length > 0) {
|
|
4153
|
+
api.logger.info?.(`memory-memoryrelay: rescued ${rescued.length} memories before reset`);
|
|
4154
|
+
}
|
|
4155
|
+
} catch (err) {
|
|
4156
|
+
api.logger.warn?.(`memory-memoryrelay: reset rescue failed: ${String(err)}`);
|
|
4157
|
+
}
|
|
4158
|
+
});
|
|
4159
|
+
|
|
4160
|
+
// Message processing hooks: activity tracking and privacy redaction
|
|
4161
|
+
api.on("message_received", (_event, _ctx) => {
|
|
4162
|
+
// Update activity timestamps on active sessions
|
|
4163
|
+
for (const entry of sessionCache.values()) {
|
|
4164
|
+
entry.lastActivityAt = Date.now();
|
|
4165
|
+
}
|
|
4166
|
+
});
|
|
4167
|
+
|
|
4168
|
+
api.on("message_sending", (_event, _ctx) => {
|
|
4169
|
+
// No-op: registered for future extensibility
|
|
4170
|
+
});
|
|
4171
|
+
|
|
4172
|
+
api.on("before_message_write", (event, _ctx) => {
|
|
4173
|
+
const blocklist = autoCaptureConfig.blocklist || [];
|
|
4174
|
+
if (blocklist.length === 0) return;
|
|
4175
|
+
|
|
4176
|
+
const msg = event.message;
|
|
4177
|
+
if (!msg || typeof msg !== "object") return;
|
|
4178
|
+
|
|
4179
|
+
const m = msg as Record<string, unknown>;
|
|
4180
|
+
if (typeof m.content === "string" && isBlocklisted(m.content, blocklist)) {
|
|
4181
|
+
return {
|
|
4182
|
+
message: {
|
|
4183
|
+
...msg,
|
|
4184
|
+
content: redactSensitive(m.content as string, blocklist),
|
|
4185
|
+
} as typeof msg,
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
4188
|
+
});
|
|
4189
|
+
|
|
4190
|
+
// Subagent lifecycle hooks: track multi-agent collaboration
|
|
4191
|
+
api.on("subagent_spawned", async (event, _ctx) => {
|
|
4192
|
+
try {
|
|
4193
|
+
api.logger.debug?.(
|
|
4194
|
+
`memory-memoryrelay: subagent spawned: ${event.agentId} (session: ${event.childSessionKey}, label: ${event.label || "none"})`
|
|
4195
|
+
);
|
|
4196
|
+
} catch (err) {
|
|
4197
|
+
api.logger.warn?.(`memory-memoryrelay: subagent_spawned hook failed: ${String(err)}`);
|
|
4198
|
+
}
|
|
4199
|
+
});
|
|
4200
|
+
|
|
4201
|
+
api.on("subagent_ended", async (event, _ctx) => {
|
|
4202
|
+
try {
|
|
4203
|
+
const outcome = event.outcome || "unknown";
|
|
4204
|
+
const summary = `Subagent ${event.targetSessionKey} ended: ${event.reason} (outcome: ${outcome})`;
|
|
4205
|
+
|
|
4206
|
+
await client.store(summary, {
|
|
4207
|
+
category: "subagent-activity",
|
|
4208
|
+
source: "subagent_ended_hook",
|
|
4209
|
+
agent: agentId,
|
|
4210
|
+
outcome,
|
|
4211
|
+
});
|
|
4212
|
+
|
|
4213
|
+
api.logger.debug?.(`memory-memoryrelay: stored subagent completion: ${summary}`);
|
|
4214
|
+
} catch (err) {
|
|
4215
|
+
api.logger.warn?.(`memory-memoryrelay: subagent_ended hook failed: ${String(err)}`);
|
|
4216
|
+
}
|
|
4217
|
+
});
|
|
4218
|
+
|
|
4219
|
+
// Tool result redaction: apply privacy blocklist before persistence
|
|
4220
|
+
api.on("tool_result_persist", (event, _ctx) => {
|
|
4221
|
+
const blocklist = autoCaptureConfig.blocklist || [];
|
|
4222
|
+
if (blocklist.length === 0) return;
|
|
4223
|
+
|
|
4224
|
+
const msg = event.message;
|
|
4225
|
+
if (!msg || typeof msg !== "object") return;
|
|
4226
|
+
|
|
4227
|
+
const m = msg as Record<string, unknown>;
|
|
4228
|
+
if (typeof m.content === "string" && isBlocklisted(m.content, blocklist)) {
|
|
4229
|
+
return {
|
|
4230
|
+
message: {
|
|
4231
|
+
...msg,
|
|
4232
|
+
content: redactSensitive(m.content as string, blocklist),
|
|
4233
|
+
} as typeof msg,
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
});
|
|
4237
|
+
|
|
3972
4238
|
api.logger.info?.(
|
|
3973
|
-
`memory-memoryrelay: plugin v0.
|
|
4239
|
+
`memory-memoryrelay: plugin v0.13.0 loaded (39 tools, autoRecall: ${cfg?.autoRecall}, autoCapture: ${autoCaptureConfig.enabled ? autoCaptureConfig.tier : 'off'}, debug: ${debugEnabled})`,
|
|
3974
4240
|
);
|
|
3975
4241
|
|
|
3976
4242
|
// ========================================================================
|
|
@@ -4030,7 +4296,9 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
4030
4296
|
logs = debugLogger.getRecentLogs(limit);
|
|
4031
4297
|
}
|
|
4032
4298
|
|
|
4033
|
-
const formatted =
|
|
4299
|
+
const formatted = logs.map((l) =>
|
|
4300
|
+
`[${new Date(l.timestamp).toISOString()}] ${l.level.toUpperCase()} ${l.tool ?? "-"}: ${l.message}`
|
|
4301
|
+
).join("\n");
|
|
4034
4302
|
respond(true, {
|
|
4035
4303
|
logs,
|
|
4036
4304
|
formatted,
|
|
@@ -4316,4 +4584,746 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
4316
4584
|
respond(false, { error: String(err) });
|
|
4317
4585
|
}
|
|
4318
4586
|
});
|
|
4587
|
+
|
|
4588
|
+
// ========================================================================
|
|
4589
|
+
// Command Argument Parser (v0.14.0)
|
|
4590
|
+
// ========================================================================
|
|
4591
|
+
|
|
4592
|
+
function parseCommandArgs(input: string | undefined): { positional: string[]; flags: Record<string, string | boolean> } {
|
|
4593
|
+
const positional: string[] = [];
|
|
4594
|
+
const flags: Record<string, string | boolean> = {};
|
|
4595
|
+
|
|
4596
|
+
if (!input || input.trim() === "") {
|
|
4597
|
+
return { positional, flags };
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
const tokens: string[] = [];
|
|
4601
|
+
let current = "";
|
|
4602
|
+
let inQuote: string | null = null;
|
|
4603
|
+
|
|
4604
|
+
for (const ch of input) {
|
|
4605
|
+
if (inQuote) {
|
|
4606
|
+
if (ch === inQuote) {
|
|
4607
|
+
inQuote = null;
|
|
4608
|
+
} else {
|
|
4609
|
+
current += ch;
|
|
4610
|
+
}
|
|
4611
|
+
} else if (ch === '"' || ch === "'") {
|
|
4612
|
+
inQuote = ch;
|
|
4613
|
+
} else if (ch === " " || ch === "\t") {
|
|
4614
|
+
if (current) {
|
|
4615
|
+
tokens.push(current);
|
|
4616
|
+
current = "";
|
|
4617
|
+
}
|
|
4618
|
+
} else {
|
|
4619
|
+
current += ch;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
if (current) tokens.push(current);
|
|
4623
|
+
|
|
4624
|
+
let i = 0;
|
|
4625
|
+
while (i < tokens.length) {
|
|
4626
|
+
const token = tokens[i];
|
|
4627
|
+
if (token.startsWith("--")) {
|
|
4628
|
+
const key = token.slice(2);
|
|
4629
|
+
const next = tokens[i + 1];
|
|
4630
|
+
if (next && !next.startsWith("--")) {
|
|
4631
|
+
flags[key] = next;
|
|
4632
|
+
i += 2;
|
|
4633
|
+
} else {
|
|
4634
|
+
flags[key] = true;
|
|
4635
|
+
i += 1;
|
|
4636
|
+
}
|
|
4637
|
+
} else {
|
|
4638
|
+
positional.push(token);
|
|
4639
|
+
i += 1;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
return { positional, flags };
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
// ========================================================================
|
|
4647
|
+
// Direct Commands (15 total) — bypass LLM, execute immediately
|
|
4648
|
+
// ========================================================================
|
|
4649
|
+
|
|
4650
|
+
// /memory-status — Show full plugin status report
|
|
4651
|
+
api.registerCommand?.({
|
|
4652
|
+
name: "memory-status",
|
|
4653
|
+
description: "Show MemoryRelay connection status, tool counts, and memory stats",
|
|
4654
|
+
requireAuth: true,
|
|
4655
|
+
handler: async (_ctx) => {
|
|
4656
|
+
try {
|
|
4657
|
+
// Get connection status via health check
|
|
4658
|
+
const startTime = Date.now();
|
|
4659
|
+
const healthResult = await client.health();
|
|
4660
|
+
const responseTime = Date.now() - startTime;
|
|
4661
|
+
|
|
4662
|
+
const healthStatus = String(healthResult.status).toLowerCase();
|
|
4663
|
+
const isConnected = VALID_HEALTH_STATUSES.includes(healthStatus);
|
|
4664
|
+
|
|
4665
|
+
const connectionStatus = {
|
|
4666
|
+
status: isConnected ? "connected" as const : "disconnected" as const,
|
|
4667
|
+
endpoint: apiUrl,
|
|
4668
|
+
lastCheck: new Date().toISOString(),
|
|
4669
|
+
responseTime,
|
|
4670
|
+
};
|
|
4671
|
+
|
|
4672
|
+
// Get memory stats
|
|
4673
|
+
let memoryCount = 0;
|
|
4674
|
+
try {
|
|
4675
|
+
const stats = await client.stats();
|
|
4676
|
+
memoryCount = stats.total_memories;
|
|
4677
|
+
} catch (_statsErr) {
|
|
4678
|
+
// stats endpoint may be unavailable
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
const memoryStats = { total_memories: memoryCount };
|
|
4682
|
+
|
|
4683
|
+
const pluginConfig = {
|
|
4684
|
+
agentId,
|
|
4685
|
+
autoRecall: cfg?.autoRecall ?? true,
|
|
4686
|
+
autoCapture: autoCaptureConfig,
|
|
4687
|
+
recallLimit: cfg?.recallLimit ?? 5,
|
|
4688
|
+
recallThreshold: cfg?.recallThreshold ?? 0.3,
|
|
4689
|
+
excludeChannels: cfg?.excludeChannels ?? [],
|
|
4690
|
+
defaultProject,
|
|
4691
|
+
};
|
|
4692
|
+
|
|
4693
|
+
if (statusReporter) {
|
|
4694
|
+
const report = statusReporter.buildReport(
|
|
4695
|
+
connectionStatus,
|
|
4696
|
+
pluginConfig,
|
|
4697
|
+
memoryStats,
|
|
4698
|
+
TOOL_GROUPS,
|
|
4699
|
+
);
|
|
4700
|
+
const formatted = StatusReporter.formatReport(report);
|
|
4701
|
+
return { text: formatted };
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
// Fallback: simple text status
|
|
4705
|
+
return {
|
|
4706
|
+
text: `MemoryRelay: ${isConnected ? "connected" : "disconnected"} | Endpoint: ${apiUrl} | Memories: ${memoryCount} | Agent: ${agentId}`,
|
|
4707
|
+
};
|
|
4708
|
+
} catch (err) {
|
|
4709
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
4710
|
+
}
|
|
4711
|
+
},
|
|
4712
|
+
});
|
|
4713
|
+
|
|
4714
|
+
// /memory-stats — Show daily memory statistics
|
|
4715
|
+
api.registerCommand?.({
|
|
4716
|
+
name: "memory-stats",
|
|
4717
|
+
description: "Show daily memory statistics (total, today, weekly growth, top categories)",
|
|
4718
|
+
requireAuth: true,
|
|
4719
|
+
handler: async (_ctx) => {
|
|
4720
|
+
try {
|
|
4721
|
+
const memories = await client.list(1000);
|
|
4722
|
+
const stats = await calculateStats(
|
|
4723
|
+
async () => memories,
|
|
4724
|
+
() => 0,
|
|
4725
|
+
);
|
|
4726
|
+
const formatted = formatStatsForDisplay(stats);
|
|
4727
|
+
return { text: formatted };
|
|
4728
|
+
} catch (err) {
|
|
4729
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
4730
|
+
}
|
|
4731
|
+
},
|
|
4732
|
+
});
|
|
4733
|
+
|
|
4734
|
+
// /memory-health — Quick health check with response time
|
|
4735
|
+
api.registerCommand?.({
|
|
4736
|
+
name: "memory-health",
|
|
4737
|
+
description: "Check MemoryRelay API health and response time",
|
|
4738
|
+
requireAuth: true,
|
|
4739
|
+
handler: async (_ctx) => {
|
|
4740
|
+
try {
|
|
4741
|
+
const startTime = Date.now();
|
|
4742
|
+
const healthResult = await client.health();
|
|
4743
|
+
const responseTime = Date.now() - startTime;
|
|
4744
|
+
|
|
4745
|
+
const healthStatus = String(healthResult.status).toLowerCase();
|
|
4746
|
+
const isHealthy = VALID_HEALTH_STATUSES.includes(healthStatus);
|
|
4747
|
+
const symbol = isHealthy ? "OK" : "DEGRADED";
|
|
4748
|
+
|
|
4749
|
+
return {
|
|
4750
|
+
text: `MemoryRelay Health: ${symbol}\n Status: ${healthResult.status}\n Response Time: ${responseTime}ms\n Endpoint: ${apiUrl}`,
|
|
4751
|
+
};
|
|
4752
|
+
} catch (err) {
|
|
4753
|
+
return { text: `MemoryRelay Health: UNREACHABLE\n Error: ${String(err)}`, isError: true };
|
|
4754
|
+
}
|
|
4755
|
+
},
|
|
4756
|
+
});
|
|
4757
|
+
|
|
4758
|
+
// /memory-logs — Show recent debug log entries
|
|
4759
|
+
api.registerCommand?.({
|
|
4760
|
+
name: "memory-logs",
|
|
4761
|
+
description: "Show recent MemoryRelay debug log entries",
|
|
4762
|
+
requireAuth: true,
|
|
4763
|
+
handler: async (_ctx) => {
|
|
4764
|
+
try {
|
|
4765
|
+
if (!debugLogger) {
|
|
4766
|
+
return { text: "Debug logging is disabled. Enable it with debug: true in plugin config." };
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
const logs = debugLogger.getRecentLogs(10);
|
|
4770
|
+
if (logs.length === 0) {
|
|
4771
|
+
return { text: "No recent log entries." };
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
const lines: string[] = ["Recent MemoryRelay Logs", "━".repeat(50)];
|
|
4775
|
+
for (const entry of logs) {
|
|
4776
|
+
const statusSymbol = entry.status === "success" ? "OK" : "ERR";
|
|
4777
|
+
lines.push(
|
|
4778
|
+
`[${entry.timestamp}] ${statusSymbol} ${entry.method} ${entry.path} (${entry.duration}ms)` +
|
|
4779
|
+
(entry.error ? ` - ${entry.error}` : ""),
|
|
4780
|
+
);
|
|
4781
|
+
}
|
|
4782
|
+
return { text: lines.join("\n") };
|
|
4783
|
+
} catch (err) {
|
|
4784
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
4785
|
+
}
|
|
4786
|
+
},
|
|
4787
|
+
});
|
|
4788
|
+
|
|
4789
|
+
// /memory-metrics — Show per-tool performance metrics
|
|
4790
|
+
api.registerCommand?.({
|
|
4791
|
+
name: "memory-metrics",
|
|
4792
|
+
description: "Show per-tool call counts, success rates, and latency metrics",
|
|
4793
|
+
requireAuth: true,
|
|
4794
|
+
handler: async (_ctx) => {
|
|
4795
|
+
try {
|
|
4796
|
+
if (!debugLogger) {
|
|
4797
|
+
return { text: "Debug logging is disabled. Enable it with debug: true in plugin config." };
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
const allLogs = debugLogger.getAllLogs();
|
|
4801
|
+
if (allLogs.length === 0) {
|
|
4802
|
+
return { text: "No metrics data available yet." };
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
// Build per-tool metrics
|
|
4806
|
+
const toolMetrics = new Map<string, { calls: number; successes: number; durations: number[] }>();
|
|
4807
|
+
for (const entry of allLogs) {
|
|
4808
|
+
let metrics = toolMetrics.get(entry.tool);
|
|
4809
|
+
if (!metrics) {
|
|
4810
|
+
metrics = { calls: 0, successes: 0, durations: [] };
|
|
4811
|
+
toolMetrics.set(entry.tool, metrics);
|
|
4812
|
+
}
|
|
4813
|
+
metrics.calls++;
|
|
4814
|
+
if (entry.status === "success") metrics.successes++;
|
|
4815
|
+
metrics.durations.push(entry.duration);
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
// Format table
|
|
4819
|
+
const lines: string[] = [
|
|
4820
|
+
"MemoryRelay Tool Metrics",
|
|
4821
|
+
"━".repeat(65),
|
|
4822
|
+
`${"Tool".padEnd(22)} ${"Calls".padStart(6)} ${"Success%".padStart(9)} ${"Avg(ms)".padStart(8)} ${"P95(ms)".padStart(8)}`,
|
|
4823
|
+
"─".repeat(65),
|
|
4824
|
+
];
|
|
4825
|
+
|
|
4826
|
+
for (const [tool, m] of Array.from(toolMetrics.entries()).sort((a, b) => b[1].calls - a[1].calls)) {
|
|
4827
|
+
const successRate = m.calls > 0 ? ((m.successes / m.calls) * 100).toFixed(1) : "0.0";
|
|
4828
|
+
const avg = m.durations.length > 0
|
|
4829
|
+
? Math.round(m.durations.reduce((s, d) => s + d, 0) / m.durations.length)
|
|
4830
|
+
: 0;
|
|
4831
|
+
const sorted = [...m.durations].sort((a, b) => a - b);
|
|
4832
|
+
const p95idx = Math.min(Math.ceil(sorted.length * 0.95) - 1, sorted.length - 1);
|
|
4833
|
+
const p95 = sorted.length > 0 ? sorted[Math.max(0, p95idx)] : 0;
|
|
4834
|
+
|
|
4835
|
+
lines.push(
|
|
4836
|
+
`${tool.padEnd(22)} ${String(m.calls).padStart(6)} ${(successRate + "%").padStart(9)} ${String(avg).padStart(8)} ${String(p95).padStart(8)}`,
|
|
4837
|
+
);
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
lines.push("─".repeat(65));
|
|
4841
|
+
lines.push(`Total entries: ${allLogs.length}`);
|
|
4842
|
+
|
|
4843
|
+
return { text: lines.join("\n") };
|
|
4844
|
+
} catch (err) {
|
|
4845
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
4846
|
+
}
|
|
4847
|
+
},
|
|
4848
|
+
});
|
|
4849
|
+
|
|
4850
|
+
// ========================================================================
|
|
4851
|
+
// Direct Commands (10 new — v0.14.0)
|
|
4852
|
+
// ========================================================================
|
|
4853
|
+
|
|
4854
|
+
// /memory-search — Semantic memory search
|
|
4855
|
+
api.registerCommand?.({
|
|
4856
|
+
name: "memory-search",
|
|
4857
|
+
description: "Semantic search across stored memories",
|
|
4858
|
+
requireAuth: true,
|
|
4859
|
+
acceptsArgs: true,
|
|
4860
|
+
handler: async (ctx) => {
|
|
4861
|
+
try {
|
|
4862
|
+
const { positional, flags } = parseCommandArgs(ctx.args);
|
|
4863
|
+
const query = positional[0];
|
|
4864
|
+
if (!query) {
|
|
4865
|
+
return { text: "Usage: /memory-search <query> [--limit 10] [--project slug] [--threshold 0.3]" };
|
|
4866
|
+
}
|
|
4867
|
+
const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
|
|
4868
|
+
const threshold = flags["threshold"] ? parseFloat(String(flags["threshold"])) : 0.3;
|
|
4869
|
+
const project = flags["project"] ? String(flags["project"]) : undefined;
|
|
4870
|
+
|
|
4871
|
+
const results = await client.search(query, limit, threshold, { project });
|
|
4872
|
+
const items: unknown[] = Array.isArray(results) ? results : (results as { data?: unknown[] }).data ?? [];
|
|
4873
|
+
|
|
4874
|
+
if (items.length === 0) {
|
|
4875
|
+
return { text: `No memories found for: "${query}"` };
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
const lines: string[] = [`Memory Search: "${query}"`, "━".repeat(60)];
|
|
4879
|
+
for (const item of items) {
|
|
4880
|
+
const m = item as Record<string, unknown>;
|
|
4881
|
+
const content = String(m["content"] ?? "").slice(0, 120);
|
|
4882
|
+
const score = typeof m["similarity"] === "number" ? `${Math.round(m["similarity"] as number * 100)}%` : "N/A";
|
|
4883
|
+
const category = String(m["category"] ?? "general");
|
|
4884
|
+
const date = m["created_at"] ? new Date(String(m["created_at"])).toLocaleDateString() : "unknown";
|
|
4885
|
+
const id = String(m["id"] ?? "");
|
|
4886
|
+
lines.push(`[${score}] ${content}`);
|
|
4887
|
+
lines.push(` Category: ${category} | Date: ${date} | ID: ${id}`);
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
return { text: lines.join("\n") };
|
|
4891
|
+
} catch (err) {
|
|
4892
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
4893
|
+
}
|
|
4894
|
+
},
|
|
4895
|
+
});
|
|
4896
|
+
|
|
4897
|
+
// /memory-validate — Production readiness check
|
|
4898
|
+
api.registerCommand?.({
|
|
4899
|
+
name: "memory-validate",
|
|
4900
|
+
description: "Run production readiness checks for the MemoryRelay plugin",
|
|
4901
|
+
requireAuth: true,
|
|
4902
|
+
handler: async (_ctx) => {
|
|
4903
|
+
try {
|
|
4904
|
+
const results: Array<{ label: string; status: "PASS" | "FAIL" | "WARN"; detail: string }> = [];
|
|
4905
|
+
|
|
4906
|
+
// 1. API connectivity
|
|
4907
|
+
try {
|
|
4908
|
+
await client.health();
|
|
4909
|
+
results.push({ label: "API connectivity", status: "PASS", detail: "Health endpoint reachable" });
|
|
4910
|
+
} catch (err) {
|
|
4911
|
+
results.push({ label: "API connectivity", status: "FAIL", detail: String(err) });
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
// 2. API health status
|
|
4915
|
+
try {
|
|
4916
|
+
const h = await client.health();
|
|
4917
|
+
const statusStr = String(h.status).toLowerCase();
|
|
4918
|
+
if (VALID_HEALTH_STATUSES.includes(statusStr)) {
|
|
4919
|
+
results.push({ label: "API health", status: "PASS", detail: `Status: ${h.status}` });
|
|
4920
|
+
} else {
|
|
4921
|
+
results.push({ label: "API health", status: "WARN", detail: `Unexpected status: ${h.status}` });
|
|
4922
|
+
}
|
|
4923
|
+
} catch (err) {
|
|
4924
|
+
results.push({ label: "API health", status: "FAIL", detail: String(err) });
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
// 3. Core tools
|
|
4928
|
+
const allTools = Object.values(TOOL_GROUPS).flat();
|
|
4929
|
+
const coreTools = ["memory_store", "memory_recall", "memory_list"];
|
|
4930
|
+
const missing = coreTools.filter((t) => !allTools.includes(t));
|
|
4931
|
+
if (missing.length === 0) {
|
|
4932
|
+
results.push({ label: "Core tools", status: "PASS", detail: "memory_store, memory_recall, memory_list present" });
|
|
4933
|
+
} else {
|
|
4934
|
+
results.push({ label: "Core tools", status: "FAIL", detail: `Missing: ${missing.join(", ")}` });
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
// 4. Auto-recall enabled
|
|
4938
|
+
const autoRecall = cfg?.autoRecall ?? true;
|
|
4939
|
+
results.push({
|
|
4940
|
+
label: "Auto-recall enabled",
|
|
4941
|
+
status: autoRecall ? "PASS" : "WARN",
|
|
4942
|
+
detail: autoRecall ? "Enabled" : "Disabled in config",
|
|
4943
|
+
});
|
|
4944
|
+
|
|
4945
|
+
// 5. Auto-capture enabled
|
|
4946
|
+
results.push({
|
|
4947
|
+
label: "Auto-capture enabled",
|
|
4948
|
+
status: autoCaptureConfig.enabled ? "PASS" : "WARN",
|
|
4949
|
+
detail: autoCaptureConfig.enabled ? `Enabled (tier: ${autoCaptureConfig.tier})` : "Disabled in config",
|
|
4950
|
+
});
|
|
4951
|
+
|
|
4952
|
+
// 6. Memory storage
|
|
4953
|
+
try {
|
|
4954
|
+
await client.list(1);
|
|
4955
|
+
results.push({ label: "Memory storage", status: "PASS", detail: "Storage accessible" });
|
|
4956
|
+
} catch (err) {
|
|
4957
|
+
results.push({ label: "Memory storage", status: "FAIL", detail: String(err) });
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4960
|
+
// 7. Agent ID configured
|
|
4961
|
+
const agentIdOk = agentId && agentId !== "" && agentId !== "default";
|
|
4962
|
+
results.push({
|
|
4963
|
+
label: "Agent ID configured",
|
|
4964
|
+
status: agentIdOk ? "PASS" : "WARN",
|
|
4965
|
+
detail: agentIdOk ? `ID: ${agentId}` : `Agent ID is "${agentId}" — consider setting a unique ID`,
|
|
4966
|
+
});
|
|
4967
|
+
|
|
4968
|
+
const passes = results.filter((r) => r.status === "PASS").length;
|
|
4969
|
+
const failures = results.filter((r) => r.status === "FAIL").length;
|
|
4970
|
+
let grade: string;
|
|
4971
|
+
if (passes === 7) grade = "A+";
|
|
4972
|
+
else if (passes === 6) grade = "A";
|
|
4973
|
+
else if (passes === 5) grade = "B+";
|
|
4974
|
+
else if (passes === 4) grade = "B";
|
|
4975
|
+
else grade = "F";
|
|
4976
|
+
|
|
4977
|
+
const lines: string[] = ["MemoryRelay Production Readiness", "━".repeat(50)];
|
|
4978
|
+
for (const r of results) {
|
|
4979
|
+
lines.push(`[${r.status.padEnd(4)}] ${r.label}: ${r.detail}`);
|
|
4980
|
+
}
|
|
4981
|
+
lines.push("─".repeat(50));
|
|
4982
|
+
lines.push(`Checks passed: ${passes}/7 | Grade: ${grade} | Production ready: ${failures === 0 ? "Yes" : "No"}`);
|
|
4983
|
+
|
|
4984
|
+
return { text: lines.join("\n") };
|
|
4985
|
+
} catch (err) {
|
|
4986
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
4987
|
+
}
|
|
4988
|
+
},
|
|
4989
|
+
});
|
|
4990
|
+
|
|
4991
|
+
// /memory-config — Read-only config display
|
|
4992
|
+
api.registerCommand?.({
|
|
4993
|
+
name: "memory-config",
|
|
4994
|
+
description: "Display current MemoryRelay plugin configuration",
|
|
4995
|
+
requireAuth: true,
|
|
4996
|
+
handler: async (_ctx) => {
|
|
4997
|
+
try {
|
|
4998
|
+
const lines: string[] = ["MemoryRelay Configuration", "━".repeat(50)];
|
|
4999
|
+
lines.push(`API URL: ${apiUrl}`);
|
|
5000
|
+
lines.push(`Agent ID: ${agentId}`);
|
|
5001
|
+
lines.push(`Default Project: ${defaultProject || "(none)"}`);
|
|
5002
|
+
lines.push(`Enabled Tools: ${cfg?.enabledTools ?? "all"}`);
|
|
5003
|
+
lines.push(`Auto-Recall: ${cfg?.autoRecall ?? true}`);
|
|
5004
|
+
lines.push(`Auto-Capture: ${autoCaptureConfig.enabled} (tier: ${autoCaptureConfig.tier})`);
|
|
5005
|
+
lines.push(`Recall Limit: ${cfg?.recallLimit ?? 5}`);
|
|
5006
|
+
lines.push(`Recall Threshold: ${cfg?.recallThreshold ?? 0.3}`);
|
|
5007
|
+
lines.push(`Exclude Channels: ${(cfg?.excludeChannels ?? []).join(", ") || "(none)"}`);
|
|
5008
|
+
lines.push(`Session Timeout: ${cfg?.sessionTimeoutMinutes ?? 120} min`);
|
|
5009
|
+
lines.push(`Cleanup Interval: ${cfg?.sessionCleanupIntervalMinutes ?? 30} min`);
|
|
5010
|
+
lines.push(`Debug: ${cfg?.debug ?? false}`);
|
|
5011
|
+
lines.push(`Verbose: ${cfg?.verbose ?? false}`);
|
|
5012
|
+
lines.push(`Max Log Entries: ${cfg?.maxLogEntries ?? 100}`);
|
|
5013
|
+
return { text: lines.join("\n") };
|
|
5014
|
+
} catch (err) {
|
|
5015
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
5016
|
+
}
|
|
5017
|
+
},
|
|
5018
|
+
});
|
|
5019
|
+
|
|
5020
|
+
// /memory-sessions — List sessions
|
|
5021
|
+
api.registerCommand?.({
|
|
5022
|
+
name: "memory-sessions",
|
|
5023
|
+
description: "List MemoryRelay sessions",
|
|
5024
|
+
requireAuth: true,
|
|
5025
|
+
acceptsArgs: true,
|
|
5026
|
+
handler: async (ctx) => {
|
|
5027
|
+
try {
|
|
5028
|
+
const { flags } = parseCommandArgs(ctx.args);
|
|
5029
|
+
const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
|
|
5030
|
+
const project = flags["project"] ? String(flags["project"]) : undefined;
|
|
5031
|
+
let status: string | undefined = flags["status"] ? String(flags["status"]) : undefined;
|
|
5032
|
+
if (flags["active"]) status = "active";
|
|
5033
|
+
|
|
5034
|
+
const raw = await client.listSessions(limit, project, status);
|
|
5035
|
+
const sessions: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
|
|
5036
|
+
|
|
5037
|
+
if (sessions.length === 0) {
|
|
5038
|
+
return { text: "No sessions found." };
|
|
5039
|
+
}
|
|
5040
|
+
|
|
5041
|
+
const lines: string[] = ["MemoryRelay Sessions", "━".repeat(60)];
|
|
5042
|
+
for (const session of sessions) {
|
|
5043
|
+
const s = session as Record<string, unknown>;
|
|
5044
|
+
const sid = String(s["id"] ?? "");
|
|
5045
|
+
const sessionStatus = String(s["status"] ?? "unknown").toUpperCase();
|
|
5046
|
+
const startedAt = s["started_at"] ? new Date(String(s["started_at"])).toLocaleString() : "unknown";
|
|
5047
|
+
let duration = "ongoing";
|
|
5048
|
+
if (s["started_at"] && s["ended_at"]) {
|
|
5049
|
+
const diffMs = new Date(String(s["ended_at"])).getTime() - new Date(String(s["started_at"])).getTime();
|
|
5050
|
+
const diffMin = Math.round(diffMs / 60000);
|
|
5051
|
+
duration = `${diffMin}m`;
|
|
5052
|
+
}
|
|
5053
|
+
const summary = String(s["summary"] ?? "").slice(0, 80);
|
|
5054
|
+
lines.push(`[${sessionStatus}] ${sid}`);
|
|
5055
|
+
lines.push(` Started: ${startedAt} | Duration: ${duration}`);
|
|
5056
|
+
if (summary) lines.push(` ${summary}`);
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
return { text: lines.join("\n") };
|
|
5060
|
+
} catch (err) {
|
|
5061
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
5062
|
+
}
|
|
5063
|
+
},
|
|
5064
|
+
});
|
|
5065
|
+
|
|
5066
|
+
// /memory-decisions — List decisions
|
|
5067
|
+
api.registerCommand?.({
|
|
5068
|
+
name: "memory-decisions",
|
|
5069
|
+
description: "List architectural decisions stored in MemoryRelay",
|
|
5070
|
+
requireAuth: true,
|
|
5071
|
+
acceptsArgs: true,
|
|
5072
|
+
handler: async (ctx) => {
|
|
5073
|
+
try {
|
|
5074
|
+
const { flags } = parseCommandArgs(ctx.args);
|
|
5075
|
+
const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
|
|
5076
|
+
const project = flags["project"] ? String(flags["project"]) : undefined;
|
|
5077
|
+
const status = flags["status"] ? String(flags["status"]) : undefined;
|
|
5078
|
+
const tags = flags["tags"] ? String(flags["tags"]) : undefined;
|
|
5079
|
+
|
|
5080
|
+
const raw = await client.listDecisions(limit, project, status, tags);
|
|
5081
|
+
const decisions: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
|
|
5082
|
+
|
|
5083
|
+
if (decisions.length === 0) {
|
|
5084
|
+
return { text: "No decisions found." };
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
const lines: string[] = ["MemoryRelay Decisions", "━".repeat(60)];
|
|
5088
|
+
for (const decision of decisions) {
|
|
5089
|
+
const d = decision as Record<string, unknown>;
|
|
5090
|
+
const decisionStatus = String(d["status"] ?? "unknown").toUpperCase();
|
|
5091
|
+
const title = String(d["title"] ?? "(untitled)");
|
|
5092
|
+
const date = d["created_at"] ? new Date(String(d["created_at"])).toLocaleDateString() : "unknown";
|
|
5093
|
+
const rationale = String(d["rationale"] ?? "").slice(0, 100);
|
|
5094
|
+
lines.push(`[${decisionStatus}] ${title} (${date})`);
|
|
5095
|
+
if (rationale) lines.push(` ${rationale}`);
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
return { text: lines.join("\n") };
|
|
5099
|
+
} catch (err) {
|
|
5100
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
5101
|
+
}
|
|
5102
|
+
},
|
|
5103
|
+
});
|
|
5104
|
+
|
|
5105
|
+
// /memory-patterns — List/search patterns
|
|
5106
|
+
api.registerCommand?.({
|
|
5107
|
+
name: "memory-patterns",
|
|
5108
|
+
description: "List or search memory patterns",
|
|
5109
|
+
requireAuth: true,
|
|
5110
|
+
acceptsArgs: true,
|
|
5111
|
+
handler: async (ctx) => {
|
|
5112
|
+
try {
|
|
5113
|
+
const { positional, flags } = parseCommandArgs(ctx.args);
|
|
5114
|
+
const query = positional[0] ?? "";
|
|
5115
|
+
const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
|
|
5116
|
+
const category = flags["category"] ? String(flags["category"]) : undefined;
|
|
5117
|
+
const project = flags["project"] ? String(flags["project"]) : undefined;
|
|
5118
|
+
|
|
5119
|
+
const raw = await client.searchPatterns(query, category, project, limit);
|
|
5120
|
+
const patterns: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
|
|
5121
|
+
|
|
5122
|
+
if (patterns.length === 0) {
|
|
5123
|
+
return { text: query ? `No patterns found for: "${query}"` : "No patterns found." };
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5126
|
+
const lines: string[] = ["MemoryRelay Patterns", "━".repeat(60)];
|
|
5127
|
+
for (const pattern of patterns) {
|
|
5128
|
+
const p = pattern as Record<string, unknown>;
|
|
5129
|
+
const name = String(p["name"] ?? "(unnamed)");
|
|
5130
|
+
const cat = String(p["category"] ?? "general");
|
|
5131
|
+
const description = String(p["description"] ?? "").slice(0, 100);
|
|
5132
|
+
lines.push(`${name} [${cat}]`);
|
|
5133
|
+
if (description) lines.push(` ${description}`);
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
return { text: lines.join("\n") };
|
|
5137
|
+
} catch (err) {
|
|
5138
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
5139
|
+
}
|
|
5140
|
+
},
|
|
5141
|
+
});
|
|
5142
|
+
|
|
5143
|
+
// /memory-entities — List entities
|
|
5144
|
+
api.registerCommand?.({
|
|
5145
|
+
name: "memory-entities",
|
|
5146
|
+
description: "List entities stored in MemoryRelay",
|
|
5147
|
+
requireAuth: true,
|
|
5148
|
+
acceptsArgs: true,
|
|
5149
|
+
handler: async (ctx) => {
|
|
5150
|
+
try {
|
|
5151
|
+
const { flags } = parseCommandArgs(ctx.args);
|
|
5152
|
+
const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 20;
|
|
5153
|
+
|
|
5154
|
+
const raw = await client.listEntities(limit);
|
|
5155
|
+
const entities: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
|
|
5156
|
+
|
|
5157
|
+
if (entities.length === 0) {
|
|
5158
|
+
return { text: "No entities found." };
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
const lines: string[] = ["MemoryRelay Entities", "━".repeat(60)];
|
|
5162
|
+
for (const entity of entities) {
|
|
5163
|
+
const e = entity as Record<string, unknown>;
|
|
5164
|
+
const name = String(e["name"] ?? "(unnamed)");
|
|
5165
|
+
const type = String(e["type"] ?? "unknown");
|
|
5166
|
+
const relationships = Array.isArray(e["relationships"]) ? e["relationships"].length : (typeof e["relationship_count"] === "number" ? e["relationship_count"] : 0);
|
|
5167
|
+
lines.push(`${name} [${type}] (${relationships} relationships)`);
|
|
5168
|
+
}
|
|
5169
|
+
|
|
5170
|
+
return { text: lines.join("\n") };
|
|
5171
|
+
} catch (err) {
|
|
5172
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
5173
|
+
}
|
|
5174
|
+
},
|
|
5175
|
+
});
|
|
5176
|
+
|
|
5177
|
+
// /memory-projects — List projects
|
|
5178
|
+
api.registerCommand?.({
|
|
5179
|
+
name: "memory-projects",
|
|
5180
|
+
description: "List projects in MemoryRelay",
|
|
5181
|
+
requireAuth: true,
|
|
5182
|
+
acceptsArgs: true,
|
|
5183
|
+
handler: async (ctx) => {
|
|
5184
|
+
try {
|
|
5185
|
+
const { flags } = parseCommandArgs(ctx.args);
|
|
5186
|
+
const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 20;
|
|
5187
|
+
|
|
5188
|
+
const raw = await client.listProjects(limit);
|
|
5189
|
+
const projects: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
|
|
5190
|
+
|
|
5191
|
+
if (projects.length === 0) {
|
|
5192
|
+
return { text: "No projects found." };
|
|
5193
|
+
}
|
|
5194
|
+
|
|
5195
|
+
const lines: string[] = ["MemoryRelay Projects", "━".repeat(60)];
|
|
5196
|
+
for (const project of projects) {
|
|
5197
|
+
const p = project as Record<string, unknown>;
|
|
5198
|
+
const slug = String(p["slug"] ?? "(no-slug)");
|
|
5199
|
+
const description = String(p["description"] ?? "").slice(0, 80);
|
|
5200
|
+
const memoryCount = typeof p["memory_count"] === "number" ? p["memory_count"] : 0;
|
|
5201
|
+
lines.push(`${slug} — ${description || "(no description)"} (${memoryCount} memories)`);
|
|
5202
|
+
}
|
|
5203
|
+
|
|
5204
|
+
return { text: lines.join("\n") };
|
|
5205
|
+
} catch (err) {
|
|
5206
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
5207
|
+
}
|
|
5208
|
+
},
|
|
5209
|
+
});
|
|
5210
|
+
|
|
5211
|
+
// /memory-agents — List agents
|
|
5212
|
+
api.registerCommand?.({
|
|
5213
|
+
name: "memory-agents",
|
|
5214
|
+
description: "List agents registered in MemoryRelay",
|
|
5215
|
+
requireAuth: true,
|
|
5216
|
+
acceptsArgs: true,
|
|
5217
|
+
handler: async (ctx) => {
|
|
5218
|
+
try {
|
|
5219
|
+
const { flags } = parseCommandArgs(ctx.args);
|
|
5220
|
+
const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 20;
|
|
5221
|
+
|
|
5222
|
+
const raw = await client.listAgents(limit);
|
|
5223
|
+
const agents: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
|
|
5224
|
+
|
|
5225
|
+
if (agents.length === 0) {
|
|
5226
|
+
return { text: "No agents found." };
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
const lines: string[] = ["MemoryRelay Agents", "━".repeat(60)];
|
|
5230
|
+
for (const agent of agents) {
|
|
5231
|
+
const a = agent as Record<string, unknown>;
|
|
5232
|
+
const id = String(a["id"] ?? "(no-id)");
|
|
5233
|
+
const name = String(a["name"] ?? "");
|
|
5234
|
+
const description = String(a["description"] ?? "");
|
|
5235
|
+
lines.push(`${id}${name ? ` (${name})` : ""}${description ? `, ${description}` : ""}`);
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
return { text: lines.join("\n") };
|
|
5239
|
+
} catch (err) {
|
|
5240
|
+
return { text: `Error: ${String(err)}`, isError: true };
|
|
5241
|
+
}
|
|
5242
|
+
},
|
|
5243
|
+
});
|
|
5244
|
+
|
|
5245
|
+
// /memory-forget — Delete a memory by ID
|
|
5246
|
+
api.registerCommand?.({
|
|
5247
|
+
name: "memory-forget",
|
|
5248
|
+
description: "Delete a specific memory by ID",
|
|
5249
|
+
requireAuth: true,
|
|
5250
|
+
acceptsArgs: true,
|
|
5251
|
+
handler: async (ctx) => {
|
|
5252
|
+
const { positional } = parseCommandArgs(ctx.args);
|
|
5253
|
+
const memoryId = positional[0];
|
|
5254
|
+
if (!memoryId) {
|
|
5255
|
+
return { text: "Usage: /memory-forget <memory-id>" };
|
|
5256
|
+
}
|
|
5257
|
+
try {
|
|
5258
|
+
let preview = "";
|
|
5259
|
+
try {
|
|
5260
|
+
const existing = await client.get(memoryId);
|
|
5261
|
+
const m = existing as Record<string, unknown>;
|
|
5262
|
+
preview = String(m["content"] ?? "").slice(0, 120);
|
|
5263
|
+
} catch (_) {
|
|
5264
|
+
// preview unavailable — proceed with delete
|
|
5265
|
+
}
|
|
5266
|
+
|
|
5267
|
+
await client.delete(memoryId);
|
|
5268
|
+
|
|
5269
|
+
const lines = [`Memory deleted: ${memoryId}`];
|
|
5270
|
+
if (preview) lines.push(`Content: ${preview}`);
|
|
5271
|
+
return { text: lines.join("\n") };
|
|
5272
|
+
} catch (err) {
|
|
5273
|
+
const msg = String(err);
|
|
5274
|
+
if (msg.toLowerCase().includes("not found") || msg.includes("404")) {
|
|
5275
|
+
return { text: `Memory not found: ${memoryId}`, isError: true };
|
|
5276
|
+
}
|
|
5277
|
+
return { text: `Error: ${msg}`, isError: true };
|
|
5278
|
+
}
|
|
5279
|
+
},
|
|
5280
|
+
});
|
|
5281
|
+
|
|
5282
|
+
// ========================================================================
|
|
5283
|
+
// Stale Session Cleanup Service (v0.13.0)
|
|
5284
|
+
// ========================================================================
|
|
5285
|
+
|
|
5286
|
+
let sessionCleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
5287
|
+
|
|
5288
|
+
api.registerService({
|
|
5289
|
+
id: "memoryrelay-session-cleanup",
|
|
5290
|
+
start: async (_ctx) => {
|
|
5291
|
+
sessionCleanupInterval = setInterval(async () => {
|
|
5292
|
+
const now = Date.now();
|
|
5293
|
+
const staleEntries: string[] = [];
|
|
5294
|
+
|
|
5295
|
+
for (const [externalId, entry] of sessionCache.entries()) {
|
|
5296
|
+
if (now - entry.lastActivityAt > sessionTimeoutMs) {
|
|
5297
|
+
staleEntries.push(externalId);
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
|
|
5301
|
+
for (const externalId of staleEntries) {
|
|
5302
|
+
const entry = sessionCache.get(externalId);
|
|
5303
|
+
if (!entry) continue;
|
|
5304
|
+
|
|
5305
|
+
try {
|
|
5306
|
+
await client.endSession(
|
|
5307
|
+
entry.sessionId,
|
|
5308
|
+
`Auto-closed: inactive for >${Math.round(sessionTimeoutMs / 60000)} minutes`
|
|
5309
|
+
);
|
|
5310
|
+
sessionCache.delete(externalId);
|
|
5311
|
+
api.logger.info?.(
|
|
5312
|
+
`memory-memoryrelay: auto-closed stale session ${entry.sessionId} (external: ${externalId})`
|
|
5313
|
+
);
|
|
5314
|
+
} catch (err) {
|
|
5315
|
+
api.logger.warn?.(
|
|
5316
|
+
`memory-memoryrelay: failed to auto-close session ${entry.sessionId}: ${String(err)}`
|
|
5317
|
+
);
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
}, sessionCleanupIntervalMs);
|
|
5321
|
+
},
|
|
5322
|
+
stop: async (_ctx) => {
|
|
5323
|
+
if (sessionCleanupInterval) {
|
|
5324
|
+
clearInterval(sessionCleanupInterval);
|
|
5325
|
+
sessionCleanupInterval = null;
|
|
5326
|
+
}
|
|
5327
|
+
},
|
|
5328
|
+
});
|
|
4319
5329
|
}
|