@memoryrelay/plugin-memoryrelay-ai 0.12.0 → 0.12.1
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/package.json +3 -2
- package/src/cli/stats-command.ts +166 -0
- package/src/debug-logger.test.ts +233 -0
- package/src/debug-logger.ts +169 -0
- package/src/heartbeat/daily-stats.ts +233 -0
- package/src/onboarding/first-run.ts +236 -0
- package/src/status-reporter.test.ts +230 -0
- package/src/status-reporter.ts +284 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memoryrelay/plugin-memoryrelay-ai",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "OpenClaw memory plugin for MemoryRelay API - sessions, decisions, patterns, projects & semantic search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"index.ts",
|
|
48
48
|
"openclaw.plugin.json",
|
|
49
49
|
"README.md",
|
|
50
|
-
"LICENSE"
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"src/"
|
|
51
52
|
],
|
|
52
53
|
"engines": {
|
|
53
54
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Stats Command (Phase 1 - Issue #11)
|
|
3
|
+
*
|
|
4
|
+
* Provides `openclaw memoryrelay stats` command for quick stats access
|
|
5
|
+
* Supports both text and JSON output formats
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface StatsCommandOptions {
|
|
9
|
+
format?: "text" | "json";
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StatsOutput {
|
|
14
|
+
total: number;
|
|
15
|
+
today: number;
|
|
16
|
+
thisWeek: number;
|
|
17
|
+
thisMonth: number;
|
|
18
|
+
weeklyGrowth: number;
|
|
19
|
+
monthlyGrowth: number;
|
|
20
|
+
topCategories: Array<{ category: string; count: number }>;
|
|
21
|
+
recentlyAdded: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
content: string;
|
|
24
|
+
created_at: number;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gather comprehensive stats for CLI output
|
|
30
|
+
*/
|
|
31
|
+
export async function gatherStatsForCLI(
|
|
32
|
+
getAllMemories: () => Promise<Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
content: string;
|
|
35
|
+
metadata: Record<string, string>;
|
|
36
|
+
created_at: number;
|
|
37
|
+
}>>
|
|
38
|
+
): Promise<StatsOutput> {
|
|
39
|
+
const memories = await getAllMemories();
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
|
|
42
|
+
// Time boundaries
|
|
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
|
+
const monthStart = now - 30 * 24 * 60 * 60 * 1000;
|
|
47
|
+
const lastMonthStart = now - 60 * 24 * 60 * 60 * 1000;
|
|
48
|
+
|
|
49
|
+
// Count by period
|
|
50
|
+
const total = memories.length;
|
|
51
|
+
const today = memories.filter((m) => m.created_at >= todayStart).length;
|
|
52
|
+
const thisWeek = memories.filter((m) => m.created_at >= weekStart).length;
|
|
53
|
+
const lastWeek = memories.filter(
|
|
54
|
+
(m) => m.created_at >= lastWeekStart && m.created_at < weekStart
|
|
55
|
+
).length;
|
|
56
|
+
const thisMonth = memories.filter((m) => m.created_at >= monthStart).length;
|
|
57
|
+
const lastMonth = memories.filter(
|
|
58
|
+
(m) => m.created_at >= lastMonthStart && m.created_at < monthStart
|
|
59
|
+
).length;
|
|
60
|
+
|
|
61
|
+
// Growth calculations
|
|
62
|
+
const weeklyGrowth = lastWeek > 0 ? ((thisWeek - lastWeek) / lastWeek) * 100 : 0;
|
|
63
|
+
const monthlyGrowth = lastMonth > 0 ? ((thisMonth - lastMonth) / lastMonth) * 100 : 0;
|
|
64
|
+
|
|
65
|
+
// Top categories
|
|
66
|
+
const categoryCount = new Map<string, number>();
|
|
67
|
+
for (const memory of memories) {
|
|
68
|
+
const category = memory.metadata.category || "uncategorized";
|
|
69
|
+
categoryCount.set(category, (categoryCount.get(category) || 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const topCategories = Array.from(categoryCount.entries())
|
|
73
|
+
.map(([category, count]) => ({ category, count }))
|
|
74
|
+
.sort((a, b) => b.count - a.count)
|
|
75
|
+
.slice(0, 10);
|
|
76
|
+
|
|
77
|
+
// Recently added (last 5)
|
|
78
|
+
const recentlyAdded = memories
|
|
79
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
80
|
+
.slice(0, 5)
|
|
81
|
+
.map((m) => ({
|
|
82
|
+
id: m.id,
|
|
83
|
+
content: m.content.length > 100 ? m.content.slice(0, 100) + "..." : m.content,
|
|
84
|
+
created_at: m.created_at,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
total,
|
|
89
|
+
today,
|
|
90
|
+
thisWeek,
|
|
91
|
+
thisMonth,
|
|
92
|
+
weeklyGrowth,
|
|
93
|
+
monthlyGrowth,
|
|
94
|
+
topCategories,
|
|
95
|
+
recentlyAdded,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format stats as human-readable text
|
|
101
|
+
*/
|
|
102
|
+
export function formatStatsAsText(stats: StatsOutput, verbose: boolean = false): string {
|
|
103
|
+
const lines: string[] = [];
|
|
104
|
+
|
|
105
|
+
lines.push("📊 MemoryRelay Statistics");
|
|
106
|
+
lines.push("");
|
|
107
|
+
|
|
108
|
+
// Overview
|
|
109
|
+
lines.push("OVERVIEW");
|
|
110
|
+
lines.push(` Total memories: ${stats.total}`);
|
|
111
|
+
lines.push(` Added today: ${stats.today}`);
|
|
112
|
+
lines.push(` This week: ${stats.thisWeek} (${stats.weeklyGrowth > 0 ? '+' : ''}${stats.weeklyGrowth.toFixed(0)}%)`);
|
|
113
|
+
lines.push(` This month: ${stats.thisMonth} (${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toFixed(0)}%)`);
|
|
114
|
+
lines.push("");
|
|
115
|
+
|
|
116
|
+
// Top categories
|
|
117
|
+
if (stats.topCategories.length > 0) {
|
|
118
|
+
lines.push("TOP CATEGORIES");
|
|
119
|
+
const displayCount = verbose ? 10 : 5;
|
|
120
|
+
for (const cat of stats.topCategories.slice(0, displayCount)) {
|
|
121
|
+
const percentage = ((cat.count / stats.total) * 100).toFixed(1);
|
|
122
|
+
lines.push(` ${cat.category.padEnd(20)} ${cat.count.toString().padStart(4)} (${percentage}%)`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Recently added (verbose only)
|
|
128
|
+
if (verbose && stats.recentlyAdded.length > 0) {
|
|
129
|
+
lines.push("RECENTLY ADDED");
|
|
130
|
+
for (const memory of stats.recentlyAdded) {
|
|
131
|
+
const date = new Date(memory.created_at).toLocaleDateString();
|
|
132
|
+
lines.push(` [${date}] ${memory.content}`);
|
|
133
|
+
}
|
|
134
|
+
lines.push("");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Format stats as JSON
|
|
142
|
+
*/
|
|
143
|
+
export function formatStatsAsJSON(stats: StatsOutput): string {
|
|
144
|
+
return JSON.stringify(stats, null, 2);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Main CLI stats command handler
|
|
149
|
+
*/
|
|
150
|
+
export async function statsCommand(
|
|
151
|
+
getAllMemories: () => Promise<Array<{
|
|
152
|
+
id: string;
|
|
153
|
+
content: string;
|
|
154
|
+
metadata: Record<string, string>;
|
|
155
|
+
created_at: number;
|
|
156
|
+
}>>,
|
|
157
|
+
options: StatsCommandOptions = {}
|
|
158
|
+
): Promise<string> {
|
|
159
|
+
const stats = await gatherStatsForCLI(getAllMemories);
|
|
160
|
+
|
|
161
|
+
if (options.format === "json") {
|
|
162
|
+
return formatStatsAsJSON(stats);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return formatStatsAsText(stats, options.verbose);
|
|
166
|
+
}
|
|
@@ -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
|
+
}
|