@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/index.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * OpenClaw Memory Plugin - MemoryRelay
3
- * Version: 0.12.8 (Session Context Integration)
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.12.6: OpenClaw session context integration for session tracking
9
- * New in v0.12.3: Smart auto-capture, daily stats, CLI commands, onboarding
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.8.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.12.11)
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
- if (debugLogger) {
1373
- debugLogger.log(`Session: Cache hit for external_id="${externalId}"`, "info");
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
- if (debugLogger) {
1392
- debugLogger.log(
1393
- `Session: ${response.created ? 'Created' : 'Retrieved'} session ${response.id} for external_id="${externalId}"`,
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
- if (debugLogger) {
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.12.11 loaded (39 tools, autoRecall: ${cfg?.autoRecall}, autoCapture: ${autoCaptureConfig.enabled ? autoCaptureConfig.tier : 'off'}, debug: ${debugEnabled})`,
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 = DebugLogger.formatTable(logs);
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
  }