@memoryrelay/plugin-memoryrelay-ai 0.12.0 → 0.12.2

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.
@@ -0,0 +1,233 @@
1
+ /**
2
+ * DebugLogger Tests (Corrected)
3
+ *
4
+ * Tests matching actual implementation
5
+ */
6
+
7
+ import { describe, test, expect, beforeEach, vi } from "vitest";
8
+ import { DebugLogger, type LogEntry } from "./debug-logger";
9
+ import * as fs from "fs";
10
+
11
+ vi.mock("fs");
12
+
13
+ describe("DebugLogger", () => {
14
+ let logger: DebugLogger;
15
+
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ logger = new DebugLogger({
19
+ enabled: true,
20
+ verbose: false,
21
+ maxEntries: 5,
22
+ });
23
+ });
24
+
25
+ test("logs are stored when enabled", () => {
26
+ logger.log({
27
+ timestamp: new Date().toISOString(),
28
+ tool: "memory_store",
29
+ method: "POST",
30
+ path: "/v1/memories",
31
+ duration: 142,
32
+ status: "success",
33
+ });
34
+
35
+ const logs = logger.getAllLogs();
36
+ expect(logs).toHaveLength(1);
37
+ });
38
+
39
+ test("logs are not stored when disabled", () => {
40
+ const disabledLogger = new DebugLogger({
41
+ enabled: false,
42
+ verbose: false,
43
+ maxEntries: 5,
44
+ });
45
+
46
+ disabledLogger.log({
47
+ timestamp: new Date().toISOString(),
48
+ tool: "memory_store",
49
+ method: "POST",
50
+ path: "/v1/memories",
51
+ duration: 142,
52
+ status: "success",
53
+ });
54
+
55
+ const logs = disabledLogger.getAllLogs();
56
+ expect(logs).toHaveLength(0);
57
+ });
58
+
59
+ test("respects circular buffer limit (FIFO)", () => {
60
+ for (let i = 0; i < 10; i++) {
61
+ logger.log({
62
+ timestamp: new Date(Date.now() + i).toISOString(),
63
+ tool: `tool_${i}`,
64
+ method: "GET",
65
+ path: `/test/${i}`,
66
+ duration: 100,
67
+ status: "success",
68
+ });
69
+ }
70
+
71
+ const logs = logger.getAllLogs();
72
+ expect(logs).toHaveLength(5); // maxEntries = 5
73
+ expect(logs[0].tool).toBe("tool_5"); // Oldest kept
74
+ expect(logs[4].tool).toBe("tool_9"); // Newest
75
+ });
76
+
77
+ test("getRecentLogs returns last N entries", () => {
78
+ for (let i = 0; i < 3; i++) {
79
+ logger.log({
80
+ timestamp: new Date(Date.now() + i).toISOString(),
81
+ tool: `tool_${i}`,
82
+ method: "GET",
83
+ path: `/test/${i}`,
84
+ duration: 100,
85
+ status: "success",
86
+ });
87
+ }
88
+
89
+ const logs = logger.getRecentLogs(2);
90
+ expect(logs).toHaveLength(2);
91
+ expect(logs[0].tool).toBe("tool_1"); // Second-to-last
92
+ expect(logs[1].tool).toBe("tool_2"); // Last
93
+ });
94
+
95
+ test("getToolLogs filters by tool name", () => {
96
+ logger.log({
97
+ timestamp: new Date().toISOString(),
98
+ tool: "memory_store",
99
+ method: "POST",
100
+ path: "/v1/memories",
101
+ duration: 142,
102
+ status: "success",
103
+ });
104
+ logger.log({
105
+ timestamp: new Date().toISOString(),
106
+ tool: "memory_recall",
107
+ method: "POST",
108
+ path: "/v1/memories/search",
109
+ duration: 78,
110
+ status: "success",
111
+ });
112
+ logger.log({
113
+ timestamp: new Date().toISOString(),
114
+ tool: "memory_store",
115
+ method: "POST",
116
+ path: "/v1/memories",
117
+ duration: 156,
118
+ status: "error",
119
+ error: "500",
120
+ });
121
+
122
+ const logs = logger.getToolLogs("memory_store", 10);
123
+ expect(logs).toHaveLength(2);
124
+ expect(logs.every(l => l.tool === "memory_store")).toBe(true);
125
+ });
126
+
127
+ test("getErrorLogs filters by error status", () => {
128
+ logger.log({
129
+ timestamp: new Date().toISOString(),
130
+ tool: "memory_store",
131
+ method: "POST",
132
+ path: "/v1/memories",
133
+ duration: 142,
134
+ status: "success",
135
+ });
136
+ logger.log({
137
+ timestamp: new Date().toISOString(),
138
+ tool: "memory_recall",
139
+ method: "POST",
140
+ path: "/v1/memories/search",
141
+ duration: 78,
142
+ status: "error",
143
+ error: "404",
144
+ });
145
+
146
+ const logs = logger.getErrorLogs(10);
147
+ expect(logs).toHaveLength(1);
148
+ expect(logs[0].status).toBe("error");
149
+ });
150
+
151
+ test("getStats calculates correctly", () => {
152
+ logger.log({
153
+ timestamp: new Date().toISOString(),
154
+ tool: "memory_store",
155
+ method: "POST",
156
+ path: "/v1/memories",
157
+ duration: 100,
158
+ status: "success",
159
+ });
160
+ logger.log({
161
+ timestamp: new Date().toISOString(),
162
+ tool: "memory_recall",
163
+ method: "POST",
164
+ path: "/v1/memories/search",
165
+ duration: 200,
166
+ status: "success",
167
+ });
168
+ logger.log({
169
+ timestamp: new Date().toISOString(),
170
+ tool: "memory_store",
171
+ method: "POST",
172
+ path: "/v1/memories",
173
+ duration: 150,
174
+ status: "error",
175
+ error: "500",
176
+ });
177
+
178
+ const stats = logger.getStats();
179
+ expect(stats.total).toBe(3);
180
+ expect(stats.successful).toBe(2);
181
+ expect(stats.failed).toBe(1);
182
+ expect(stats.avgDuration).toBe(150); // (100+200+150)/3 = 150
183
+ });
184
+
185
+ test("clear() empties logs", () => {
186
+ logger.log({
187
+ timestamp: new Date().toISOString(),
188
+ tool: "memory_store",
189
+ method: "POST",
190
+ path: "/v1/memories",
191
+ duration: 142,
192
+ status: "success",
193
+ });
194
+
195
+ logger.clear();
196
+ const logs = logger.getAllLogs();
197
+ expect(logs).toHaveLength(0);
198
+ });
199
+
200
+ test("formatEntry creates human-readable output", () => {
201
+ const entry: LogEntry = {
202
+ timestamp: new Date().toISOString(),
203
+ tool: "memory_store",
204
+ method: "POST",
205
+ path: "/v1/memories",
206
+ duration: 142,
207
+ status: "success",
208
+ };
209
+
210
+ const formatted = DebugLogger.formatEntry(entry);
211
+ expect(formatted).toContain("memory_store");
212
+ expect(formatted).toContain("142ms");
213
+ expect(formatted).toContain("✓");
214
+ });
215
+
216
+ test("formatEntry shows error", () => {
217
+ const entry: LogEntry = {
218
+ timestamp: new Date().toISOString(),
219
+ tool: "memory_store",
220
+ method: "POST",
221
+ path: "/v1/memories",
222
+ duration: 156,
223
+ status: "error",
224
+ error: "500 Internal Server Error",
225
+ };
226
+
227
+ const formatted = DebugLogger.formatEntry(entry);
228
+ expect(formatted).toContain("memory_store");
229
+ expect(formatted).toContain("156ms");
230
+ expect(formatted).toContain("✗");
231
+ expect(formatted).toContain("500 Internal Server Error");
232
+ });
233
+ });
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Debug Logger for MemoryRelay OpenClaw Plugin
3
+ *
4
+ * Provides comprehensive logging of API calls with request/response capture
5
+ * for troubleshooting and performance analysis.
6
+ *
7
+ * Note: File logging has been removed in v0.8.4 to pass OpenClaw security validation.
8
+ * All logs are kept in-memory only. Use gateway methods (coming in v0.9.0) to access logs.
9
+ */
10
+
11
+ export interface LogEntry {
12
+ timestamp: string;
13
+ tool: string;
14
+ method: string;
15
+ path: string;
16
+ duration: number;
17
+ status: "success" | "error";
18
+ requestBody?: unknown;
19
+ responseBody?: unknown;
20
+ responseStatus?: number;
21
+ error?: string;
22
+ retries?: number;
23
+ }
24
+
25
+ export interface DebugLoggerConfig {
26
+ enabled: boolean;
27
+ verbose: boolean;
28
+ maxEntries: number;
29
+ logFile?: string; // Deprecated: File logging removed for security compliance
30
+ }
31
+
32
+ export class DebugLogger {
33
+ private logs: LogEntry[] = [];
34
+ private config: DebugLoggerConfig;
35
+
36
+ constructor(config: DebugLoggerConfig) {
37
+ this.config = config;
38
+
39
+ // logFile is no longer supported (v0.8.4)
40
+ if (config.logFile) {
41
+ console.warn(
42
+ "memoryrelay: logFile is deprecated and ignored. " +
43
+ "Use gateway methods to access debug logs (coming in v0.9.0)"
44
+ );
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Log an API call
50
+ */
51
+ log(entry: LogEntry): void {
52
+ if (!this.config.enabled) return;
53
+
54
+ // Add to in-memory buffer
55
+ this.logs.push(entry);
56
+
57
+ // Trim if exceeds max
58
+ if (this.logs.length > this.config.maxEntries) {
59
+ this.logs.shift();
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get recent logs
65
+ */
66
+ getRecentLogs(limit: number = 10): LogEntry[] {
67
+ return this.logs.slice(-limit);
68
+ }
69
+
70
+ /**
71
+ * Get logs for specific tool
72
+ */
73
+ getToolLogs(toolName: string, limit: number = 10): LogEntry[] {
74
+ return this.logs
75
+ .filter(log => log.tool === toolName)
76
+ .slice(-limit);
77
+ }
78
+
79
+ /**
80
+ * Get error logs only
81
+ */
82
+ getErrorLogs(limit: number = 10): LogEntry[] {
83
+ return this.logs
84
+ .filter(log => log.status === "error")
85
+ .slice(-limit);
86
+ }
87
+
88
+ /**
89
+ * Get all logs
90
+ */
91
+ getAllLogs(): LogEntry[] {
92
+ return [...this.logs];
93
+ }
94
+
95
+ /**
96
+ * Clear logs
97
+ */
98
+ clear(): void {
99
+ this.logs = [];
100
+ }
101
+
102
+ /**
103
+ * Get statistics
104
+ */
105
+ getStats() {
106
+ const total = this.logs.length;
107
+ const successful = this.logs.filter(l => l.status === "success").length;
108
+ const failed = total - successful;
109
+ const avgDuration = total > 0
110
+ ? this.logs.reduce((sum, l) => sum + l.duration, 0) / total
111
+ : 0;
112
+
113
+ return {
114
+ total,
115
+ successful,
116
+ failed,
117
+ successRate: total > 0 ? (successful / total) * 100 : 0,
118
+ avgDuration: Math.round(avgDuration),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Format log entry for display
124
+ */
125
+ static formatEntry(entry: LogEntry): string {
126
+ const timestamp = new Date(entry.timestamp).toLocaleTimeString();
127
+ const status = entry.status === "success" ? "✓" : "✗";
128
+ const duration = `${entry.duration}ms`;
129
+
130
+ let output = `${timestamp} ${entry.tool.padEnd(20)} ${duration.padStart(6)} ${status}`;
131
+
132
+ if (entry.error) {
133
+ output += `\n Error: ${entry.error}`;
134
+ }
135
+
136
+ if (entry.retries && entry.retries > 0) {
137
+ output += ` (${entry.retries} retries)`;
138
+ }
139
+
140
+ return output;
141
+ }
142
+
143
+ /**
144
+ * Format logs as table
145
+ */
146
+ static formatTable(logs: LogEntry[]): string {
147
+ if (logs.length === 0) {
148
+ return "No logs available";
149
+ }
150
+
151
+ const lines = [
152
+ "TIMESTAMP TOOL DURATION STATUS ERROR",
153
+ "━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━ ━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━",
154
+ ];
155
+
156
+ for (const entry of logs) {
157
+ const timestamp = new Date(entry.timestamp).toLocaleTimeString();
158
+ const status = entry.status === "success" ? "✓" : "✗";
159
+ const duration = `${entry.duration}ms`;
160
+ const error = entry.error ? entry.error.substring(0, 30) : "";
161
+
162
+ lines.push(
163
+ `${timestamp} ${entry.tool.padEnd(20)} ${duration.padStart(8)} ${status.padEnd(6)} ${error}`
164
+ );
165
+ }
166
+
167
+ return lines.join("\n");
168
+ }
169
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Daily Memory Stats for Heartbeat (Phase 1 - Issue #10)
3
+ *
4
+ * Provides morning and evening memory stat summaries for agent heartbeat checks.
5
+ * Shows growth trends, categories, and most valuable memories.
6
+ */
7
+
8
+ export interface DailyStatsConfig {
9
+ enabled: boolean;
10
+ morningTime?: string; // HH:MM format (default: "09:00")
11
+ eveningTime?: string; // HH:MM format (default: "20:00")
12
+ timezone?: string; // IANA timezone (default: system timezone)
13
+ }
14
+
15
+ export interface MemoryStats {
16
+ total: number;
17
+ today: number;
18
+ thisWeek: number;
19
+ weeklyGrowth: number; // Percentage change from last week
20
+ topCategories: Array<{ category: string; count: number }>;
21
+ mostValuable?: {
22
+ id: string;
23
+ content: string;
24
+ recallCount: number;
25
+ };
26
+ }
27
+
28
+ export interface HeartbeatResult {
29
+ shouldNotify: boolean;
30
+ message?: string;
31
+ stats?: MemoryStats;
32
+ }
33
+
34
+ /**
35
+ * Calculate memory statistics for the current period
36
+ */
37
+ export async function calculateStats(
38
+ getAllMemories: () => Promise<Array<{ id: string; content: string; metadata: Record<string, string>; created_at: number }>>,
39
+ getRecallCount: (memoryId: string) => number = () => 0
40
+ ): Promise<MemoryStats> {
41
+ const memories = await getAllMemories();
42
+ const now = Date.now();
43
+ const todayStart = new Date().setHours(0, 0, 0, 0);
44
+ const weekStart = now - 7 * 24 * 60 * 60 * 1000;
45
+ const lastWeekStart = now - 14 * 24 * 60 * 60 * 1000;
46
+
47
+ // Count memories by period
48
+ const total = memories.length;
49
+ const today = memories.filter((m) => m.created_at >= todayStart).length;
50
+ const thisWeek = memories.filter((m) => m.created_at >= weekStart).length;
51
+ const lastWeek = memories.filter(
52
+ (m) => m.created_at >= lastWeekStart && m.created_at < weekStart
53
+ ).length;
54
+
55
+ // Calculate weekly growth
56
+ const weeklyGrowth = lastWeek > 0 ? ((thisWeek - lastWeek) / lastWeek) * 100 : 0;
57
+
58
+ // Top categories
59
+ const categoryCount = new Map<string, number>();
60
+ for (const memory of memories) {
61
+ const category = memory.metadata.category || "uncategorized";
62
+ categoryCount.set(category, (categoryCount.get(category) || 0) + 1);
63
+ }
64
+
65
+ const topCategories = Array.from(categoryCount.entries())
66
+ .map(([category, count]) => ({ category, count }))
67
+ .sort((a, b) => b.count - a.count)
68
+ .slice(0, 5);
69
+
70
+ // Most valuable memory (by recall count - simulated for now)
71
+ let mostValuable: MemoryStats["mostValuable"];
72
+ if (memories.length > 0) {
73
+ const memoriesWithRecalls = memories.map((m) => ({
74
+ ...m,
75
+ recallCount: getRecallCount(m.id),
76
+ }));
77
+ memoriesWithRecalls.sort((a, b) => b.recallCount - a.recallCount);
78
+
79
+ if (memoriesWithRecalls[0].recallCount > 0) {
80
+ mostValuable = {
81
+ id: memoriesWithRecalls[0].id,
82
+ content: memoriesWithRecalls[0].content.slice(0, 100) + "...",
83
+ recallCount: memoriesWithRecalls[0].recallCount,
84
+ };
85
+ }
86
+ }
87
+
88
+ return {
89
+ total,
90
+ today,
91
+ thisWeek,
92
+ weeklyGrowth,
93
+ topCategories,
94
+ mostValuable,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Morning check: Show memory stats and growth
100
+ */
101
+ export async function morningCheck(
102
+ stats: MemoryStats
103
+ ): Promise<HeartbeatResult> {
104
+ // Don't notify if no activity or very early in adoption
105
+ if (stats.total < 5 && stats.today === 0) {
106
+ return { shouldNotify: false };
107
+ }
108
+
109
+ // Build morning message
110
+ const lines: string[] = [];
111
+ lines.push("📊 Morning Memory Check");
112
+ lines.push("");
113
+ lines.push(`📚 Total memories: ${stats.total}`);
114
+
115
+ if (stats.today > 0) {
116
+ lines.push(`✨ Added today: ${stats.today}`);
117
+ }
118
+
119
+ if (stats.thisWeek > 0) {
120
+ const growthIndicator = stats.weeklyGrowth > 0 ? "📈" : stats.weeklyGrowth < 0 ? "📉" : "➡️";
121
+ lines.push(`${growthIndicator} This week: ${stats.thisWeek} (${stats.weeklyGrowth > 0 ? '+' : ''}${stats.weeklyGrowth.toFixed(0)}%)`);
122
+ }
123
+
124
+ if (stats.topCategories.length > 0) {
125
+ lines.push("");
126
+ lines.push("🏆 Top categories:");
127
+ for (const cat of stats.topCategories.slice(0, 3)) {
128
+ lines.push(` • ${cat.category}: ${cat.count}`);
129
+ }
130
+ }
131
+
132
+ return {
133
+ shouldNotify: true,
134
+ message: lines.join("\n"),
135
+ stats,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Evening review: Show today's activity and most valuable memory
141
+ */
142
+ export async function eveningReview(
143
+ stats: MemoryStats
144
+ ): Promise<HeartbeatResult> {
145
+ // Don't notify if no activity today
146
+ if (stats.today === 0 && !stats.mostValuable) {
147
+ return { shouldNotify: false };
148
+ }
149
+
150
+ const lines: string[] = [];
151
+ lines.push("🌙 Evening Memory Review");
152
+ lines.push("");
153
+
154
+ if (stats.today > 0) {
155
+ lines.push(`✅ Stored today: ${stats.today} memories`);
156
+ } else {
157
+ lines.push("📝 No new memories today");
158
+ }
159
+
160
+ if (stats.mostValuable) {
161
+ lines.push("");
162
+ lines.push("💎 Most valuable memory:");
163
+ lines.push(` "${stats.mostValuable.content}"`);
164
+ lines.push(` Recalled ${stats.mostValuable.recallCount} times`);
165
+ }
166
+
167
+ return {
168
+ shouldNotify: stats.today > 0 || (stats.mostValuable !== undefined),
169
+ message: lines.join("\n"),
170
+ stats,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Check if it's time for a heartbeat notification
176
+ */
177
+ export function shouldRunHeartbeat(
178
+ config: DailyStatsConfig,
179
+ currentTime: Date = new Date()
180
+ ): "morning" | "evening" | null {
181
+ if (!config.enabled) return null;
182
+
183
+ const morningTime = config.morningTime || "09:00";
184
+ const eveningTime = config.eveningTime || "20:00";
185
+
186
+ const [morningHour, morningMin] = morningTime.split(":").map(Number);
187
+ const [eveningHour, eveningMin] = eveningTime.split(":").map(Number);
188
+
189
+ const currentHour = currentTime.getHours();
190
+ const currentMin = currentTime.getMinutes();
191
+
192
+ // Check if within 5-minute window of morning time
193
+ const morningStart = morningHour * 60 + morningMin;
194
+ const morningEnd = morningStart + 5;
195
+ const currentMinutes = currentHour * 60 + currentMin;
196
+
197
+ if (currentMinutes >= morningStart && currentMinutes < morningEnd) {
198
+ return "morning";
199
+ }
200
+
201
+ // Check if within 5-minute window of evening time
202
+ const eveningStart = eveningHour * 60 + eveningMin;
203
+ const eveningEnd = eveningStart + 5;
204
+
205
+ if (currentMinutes >= eveningStart && currentMinutes < eveningEnd) {
206
+ return "evening";
207
+ }
208
+
209
+ return null;
210
+ }
211
+
212
+ /**
213
+ * Format stats for console/log output
214
+ */
215
+ export function formatStatsForDisplay(stats: MemoryStats): string {
216
+ const lines: string[] = [];
217
+ lines.push(`Total: ${stats.total} | Today: ${stats.today} | Week: ${stats.thisWeek}`);
218
+
219
+ if (stats.weeklyGrowth !== 0) {
220
+ const sign = stats.weeklyGrowth > 0 ? "+" : "";
221
+ lines.push(`Growth: ${sign}${stats.weeklyGrowth.toFixed(0)}%`);
222
+ }
223
+
224
+ if (stats.topCategories.length > 0) {
225
+ const topCats = stats.topCategories
226
+ .slice(0, 3)
227
+ .map((c) => `${c.category}(${c.count})`)
228
+ .join(", ");
229
+ lines.push(`Top: ${topCats}`);
230
+ }
231
+
232
+ return lines.join(" | ");
233
+ }