@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-Run Onboarding Wizard (Phase 1 - Issue #9)
|
|
3
|
+
*
|
|
4
|
+
* Detects first run and guides user through initial setup
|
|
5
|
+
* Stores first memory, configures auto-capture preferences
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "fs/promises";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
|
|
12
|
+
export interface OnboardingState {
|
|
13
|
+
completed: boolean;
|
|
14
|
+
completedAt?: number;
|
|
15
|
+
firstMemoryId?: string;
|
|
16
|
+
autoCaptureOptIn?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OnboardingResult {
|
|
20
|
+
isFirstRun: boolean;
|
|
21
|
+
shouldOnboard: boolean;
|
|
22
|
+
state?: OnboardingState;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get onboarding state file path
|
|
27
|
+
*/
|
|
28
|
+
function getStateFilePath(): string {
|
|
29
|
+
const homeDir = os.homedir();
|
|
30
|
+
const openclawDir = path.join(homeDir, ".openclaw");
|
|
31
|
+
return path.join(openclawDir, "memoryrelay-onboarding.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if this is the first run
|
|
36
|
+
*/
|
|
37
|
+
export async function checkFirstRun(
|
|
38
|
+
getTotalMemories: () => Promise<number>
|
|
39
|
+
): Promise<OnboardingResult> {
|
|
40
|
+
const stateFile = getStateFilePath();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Check if state file exists
|
|
44
|
+
const stateContent = await fs.readFile(stateFile, "utf-8");
|
|
45
|
+
const state: OnboardingState = JSON.parse(stateContent);
|
|
46
|
+
|
|
47
|
+
// Already onboarded
|
|
48
|
+
if (state.completed) {
|
|
49
|
+
return {
|
|
50
|
+
isFirstRun: false,
|
|
51
|
+
shouldOnboard: false,
|
|
52
|
+
state,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// State file doesn't exist - might be first run
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if there are any memories
|
|
60
|
+
const totalMemories = await getTotalMemories();
|
|
61
|
+
|
|
62
|
+
// First run if: no state file AND no memories
|
|
63
|
+
const isFirstRun = totalMemories === 0;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
isFirstRun,
|
|
67
|
+
shouldOnboard: isFirstRun,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Mark onboarding as complete
|
|
73
|
+
*/
|
|
74
|
+
export async function markOnboardingComplete(
|
|
75
|
+
firstMemoryId: string,
|
|
76
|
+
autoCaptureOptIn: boolean
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const stateFile = getStateFilePath();
|
|
79
|
+
const state: OnboardingState = {
|
|
80
|
+
completed: true,
|
|
81
|
+
completedAt: Date.now(),
|
|
82
|
+
firstMemoryId,
|
|
83
|
+
autoCaptureOptIn,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Ensure directory exists
|
|
87
|
+
const dir = path.dirname(stateFile);
|
|
88
|
+
await fs.mkdir(dir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
// Write state file
|
|
91
|
+
await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate onboarding prompt text
|
|
96
|
+
*/
|
|
97
|
+
export function generateOnboardingPrompt(): string {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
|
|
100
|
+
lines.push("👋 Welcome to MemoryRelay!");
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push("This is your first time using MemoryRelay. Let's get you set up!");
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push("MemoryRelay helps you remember important information across conversations.");
|
|
105
|
+
lines.push("It uses semantic search to recall relevant context when you need it.");
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push("🎯 QUICK START");
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push("1. Store your first memory (try: 'I prefer concise responses')");
|
|
112
|
+
lines.push("2. Later, I'll automatically recall it when relevant");
|
|
113
|
+
lines.push("3. Your memories grow smarter over time");
|
|
114
|
+
lines.push("");
|
|
115
|
+
lines.push("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
116
|
+
lines.push("");
|
|
117
|
+
lines.push("⚙️ SMART AUTO-CAPTURE");
|
|
118
|
+
lines.push("");
|
|
119
|
+
lines.push("MemoryRelay can automatically detect and store important information:");
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push(" ✅ Credentials (API keys, connection strings)");
|
|
122
|
+
lines.push(" ✅ Preferences (communication style, tool choices)");
|
|
123
|
+
lines.push(" ✅ Technical facts (commands, configs, troubleshooting)");
|
|
124
|
+
lines.push(" ❌ Personal info (requires your confirmation first)");
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push("Your first 5 auto-captures will ask for confirmation to ensure");
|
|
127
|
+
lines.push("you're comfortable with what's being stored. After that, it runs");
|
|
128
|
+
lines.push("silently in the background.");
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push("Would you like to enable smart auto-capture? (Recommended)");
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push("Type 'yes' to enable, or 'no' to manually store memories");
|
|
133
|
+
lines.push("");
|
|
134
|
+
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generate success message after onboarding
|
|
140
|
+
*/
|
|
141
|
+
export function generateSuccessMessage(
|
|
142
|
+
firstMemoryContent: string,
|
|
143
|
+
autoCaptureEnabled: boolean
|
|
144
|
+
): string {
|
|
145
|
+
const lines: string[] = [];
|
|
146
|
+
|
|
147
|
+
lines.push("✅ Onboarding Complete!");
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push(`Your first memory has been stored:`);
|
|
150
|
+
lines.push(`"${firstMemoryContent}"`);
|
|
151
|
+
lines.push("");
|
|
152
|
+
|
|
153
|
+
if (autoCaptureEnabled) {
|
|
154
|
+
lines.push("🎯 Smart auto-capture is enabled");
|
|
155
|
+
lines.push(" I'll detect and store important information automatically");
|
|
156
|
+
lines.push(" (Your first 5 captures will ask for confirmation)");
|
|
157
|
+
} else {
|
|
158
|
+
lines.push("📝 Manual mode selected");
|
|
159
|
+
lines.push(" Use memory_store tool to save memories manually");
|
|
160
|
+
lines.push(" You can enable auto-capture anytime in config");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
lines.push("");
|
|
164
|
+
lines.push("💡 Quick tips:");
|
|
165
|
+
lines.push(" • Store preferences, facts, commands, and insights");
|
|
166
|
+
lines.push(" • I'll automatically recall relevant memories in future conversations");
|
|
167
|
+
lines.push(" • Check stats anytime with: openclaw gateway-call memoryrelay.stats");
|
|
168
|
+
lines.push(" • Morning/evening summaries show your memory growth");
|
|
169
|
+
lines.push("");
|
|
170
|
+
lines.push("Ready to remember everything! 🚀");
|
|
171
|
+
lines.push("");
|
|
172
|
+
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Interactive onboarding flow (for CLI or chat)
|
|
178
|
+
*/
|
|
179
|
+
export async function runOnboardingFlow(
|
|
180
|
+
storeMemory: (content: string, metadata?: Record<string, string>) => Promise<{ id: string }>,
|
|
181
|
+
getUserResponse: (prompt: string) => Promise<string>
|
|
182
|
+
): Promise<{
|
|
183
|
+
completed: boolean;
|
|
184
|
+
firstMemoryId?: string;
|
|
185
|
+
autoCaptureEnabled?: boolean;
|
|
186
|
+
}> {
|
|
187
|
+
// Show welcome
|
|
188
|
+
const welcomePrompt = generateOnboardingPrompt();
|
|
189
|
+
const autoCaptureResponse = await getUserResponse(welcomePrompt);
|
|
190
|
+
|
|
191
|
+
// Parse auto-capture preference
|
|
192
|
+
const autoCaptureEnabled = autoCaptureResponse.toLowerCase().includes("yes");
|
|
193
|
+
|
|
194
|
+
// Prompt for first memory
|
|
195
|
+
const firstMemoryPrompt = [
|
|
196
|
+
"",
|
|
197
|
+
"Great! Let's store your first memory.",
|
|
198
|
+
"",
|
|
199
|
+
"What would you like me to remember?",
|
|
200
|
+
"(Examples: 'I prefer Python', 'My project uses PostgreSQL', 'I'm building a chatbot')",
|
|
201
|
+
"",
|
|
202
|
+
].join("\n");
|
|
203
|
+
|
|
204
|
+
const firstMemoryContent = await getUserResponse(firstMemoryPrompt);
|
|
205
|
+
|
|
206
|
+
// Store first memory
|
|
207
|
+
const result = await storeMemory(firstMemoryContent, {
|
|
208
|
+
category: "onboarding",
|
|
209
|
+
source: "first-run-wizard",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Mark complete
|
|
213
|
+
await markOnboardingComplete(result.id, autoCaptureEnabled);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
completed: true,
|
|
217
|
+
firstMemoryId: result.id,
|
|
218
|
+
autoCaptureEnabled,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Simple non-interactive onboarding (for automatic setup)
|
|
224
|
+
*/
|
|
225
|
+
export async function runSimpleOnboarding(
|
|
226
|
+
storeMemory: (content: string, metadata?: Record<string, string>) => Promise<{ id: string }>,
|
|
227
|
+
defaultMemory: string = "MemoryRelay onboarding complete",
|
|
228
|
+
autoCaptureEnabled: boolean = true
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const result = await storeMemory(defaultMemory, {
|
|
231
|
+
category: "system",
|
|
232
|
+
source: "auto-onboarding",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await markOnboardingComplete(result.id, autoCaptureEnabled);
|
|
236
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusReporter Tests (Simplified)
|
|
3
|
+
*
|
|
4
|
+
* Tests matching actual implementation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeEach } from "vitest";
|
|
8
|
+
import { StatusReporter, type ConnectionStatus, type PluginConfig, type MemoryStats } from "./status-reporter";
|
|
9
|
+
import { DebugLogger } from "./debug-logger";
|
|
10
|
+
|
|
11
|
+
describe("StatusReporter", () => {
|
|
12
|
+
let reporter: StatusReporter;
|
|
13
|
+
let debugLogger: DebugLogger;
|
|
14
|
+
let mockConnection: ConnectionStatus;
|
|
15
|
+
let mockConfig: PluginConfig;
|
|
16
|
+
let mockStats: MemoryStats;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
debugLogger = new DebugLogger({
|
|
20
|
+
enabled: true,
|
|
21
|
+
verbose: false,
|
|
22
|
+
maxEntries: 100,
|
|
23
|
+
});
|
|
24
|
+
reporter = new StatusReporter(debugLogger);
|
|
25
|
+
|
|
26
|
+
mockConnection = {
|
|
27
|
+
status: "connected",
|
|
28
|
+
endpoint: "https://api.memoryrelay.net",
|
|
29
|
+
lastCheck: new Date().toISOString(),
|
|
30
|
+
responseTime: 45,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
mockConfig = {
|
|
34
|
+
agentId: "test-agent",
|
|
35
|
+
autoRecall: true,
|
|
36
|
+
autoCapture: false,
|
|
37
|
+
recallLimit: 5,
|
|
38
|
+
recallThreshold: 0.3,
|
|
39
|
+
excludeChannels: [],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
mockStats = {
|
|
43
|
+
total_memories: 100,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("records and clears tool failures", () => {
|
|
48
|
+
reporter.recordFailure("memory_store", "500 Error");
|
|
49
|
+
let issues = reporter.getIssues();
|
|
50
|
+
expect(issues).toHaveLength(1);
|
|
51
|
+
expect(issues[0].tool).toBe("memory_store");
|
|
52
|
+
|
|
53
|
+
reporter.recordSuccess("memory_store");
|
|
54
|
+
issues = reporter.getIssues();
|
|
55
|
+
expect(issues).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("buildReport creates status report", () => {
|
|
59
|
+
const toolGroups = {
|
|
60
|
+
"Core Memory": ["memory_store", "memory_recall"],
|
|
61
|
+
"Projects": ["project_list"],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const report = reporter.buildReport(
|
|
65
|
+
mockConnection,
|
|
66
|
+
mockConfig,
|
|
67
|
+
mockStats,
|
|
68
|
+
toolGroups,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(report.connection).toEqual(mockConnection);
|
|
72
|
+
expect(report.config).toEqual(mockConfig);
|
|
73
|
+
expect(report.stats).toEqual(mockStats);
|
|
74
|
+
expect(report.tools["Core Memory"]).toBeDefined();
|
|
75
|
+
expect(report.tools["Projects"]).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("buildReport includes tool status from debug logs", () => {
|
|
79
|
+
debugLogger.log({
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
tool: "memory_store",
|
|
82
|
+
method: "POST",
|
|
83
|
+
path: "/v1/memories",
|
|
84
|
+
duration: 142,
|
|
85
|
+
status: "success",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const toolGroups = {
|
|
89
|
+
"Core Memory": ["memory_store"],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const report = reporter.buildReport(
|
|
93
|
+
mockConnection,
|
|
94
|
+
mockConfig,
|
|
95
|
+
mockStats,
|
|
96
|
+
toolGroups,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const memoryTools = report.tools["Core Memory"];
|
|
100
|
+
expect(memoryTools.available).toBe(1);
|
|
101
|
+
expect(memoryTools.failed).toBe(0);
|
|
102
|
+
expect(memoryTools.tools[0].status).toBe("working");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("buildReport shows failed tools", () => {
|
|
106
|
+
debugLogger.log({
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
tool: "memory_store",
|
|
109
|
+
method: "POST",
|
|
110
|
+
path: "/v1/memories",
|
|
111
|
+
duration: 156,
|
|
112
|
+
status: "error",
|
|
113
|
+
error: "500 Internal Server Error",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const toolGroups = {
|
|
117
|
+
"Core Memory": ["memory_store"],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const report = reporter.buildReport(
|
|
121
|
+
mockConnection,
|
|
122
|
+
mockConfig,
|
|
123
|
+
mockStats,
|
|
124
|
+
toolGroups,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const memoryTools = report.tools["Core Memory"];
|
|
128
|
+
expect(memoryTools.available).toBe(0);
|
|
129
|
+
expect(memoryTools.failed).toBe(1);
|
|
130
|
+
expect(memoryTools.tools[0].status).toBe("error");
|
|
131
|
+
expect(memoryTools.tools[0].error).toBe("500 Internal Server Error");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("buildReport includes recent calls", () => {
|
|
135
|
+
debugLogger.log({
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
tool: "memory_store",
|
|
138
|
+
method: "POST",
|
|
139
|
+
path: "/v1/memories",
|
|
140
|
+
duration: 142,
|
|
141
|
+
status: "success",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const toolGroups = {
|
|
145
|
+
"Core Memory": ["memory_store"],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const report = reporter.buildReport(
|
|
149
|
+
mockConnection,
|
|
150
|
+
mockConfig,
|
|
151
|
+
mockStats,
|
|
152
|
+
toolGroups,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(report.recentCalls).toHaveLength(1);
|
|
156
|
+
expect(report.recentCalls[0].tool).toBe("memory_store");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("formatReport creates human-readable output", () => {
|
|
160
|
+
const toolGroups = {
|
|
161
|
+
"Core Memory": ["memory_store", "memory_recall"],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const report = reporter.buildReport(
|
|
165
|
+
mockConnection,
|
|
166
|
+
mockConfig,
|
|
167
|
+
mockStats,
|
|
168
|
+
toolGroups,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const formatted = StatusReporter.formatReport(report);
|
|
172
|
+
expect(formatted).toContain("MemoryRelay Plugin Status");
|
|
173
|
+
expect(formatted).toContain("connected");
|
|
174
|
+
expect(formatted).toContain("Core Memory");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("formatCompact creates brief output", () => {
|
|
178
|
+
const toolGroups = {
|
|
179
|
+
"Core Memory": ["memory_store"],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const report = reporter.buildReport(
|
|
183
|
+
mockConnection,
|
|
184
|
+
mockConfig,
|
|
185
|
+
mockStats,
|
|
186
|
+
toolGroups,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const compact = StatusReporter.formatCompact(report);
|
|
190
|
+
expect(compact).toContain("connected");
|
|
191
|
+
expect(compact.length).toBeLessThan(200); // Should be brief
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("handles disconnected status", () => {
|
|
195
|
+
mockConnection.status = "disconnected";
|
|
196
|
+
|
|
197
|
+
const toolGroups = {
|
|
198
|
+
"Core Memory": ["memory_store"],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const report = reporter.buildReport(
|
|
202
|
+
mockConnection,
|
|
203
|
+
mockConfig,
|
|
204
|
+
mockStats,
|
|
205
|
+
toolGroups,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(report.connection.status).toBe("disconnected");
|
|
209
|
+
const formatted = StatusReporter.formatReport(report);
|
|
210
|
+
expect(formatted).toContain("disconnected");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("includes issues in report", () => {
|
|
214
|
+
reporter.recordFailure("memory_batch_store", "500 Error");
|
|
215
|
+
|
|
216
|
+
const toolGroups = {
|
|
217
|
+
"Core Memory": ["memory_batch_store"],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const report = reporter.buildReport(
|
|
221
|
+
mockConnection,
|
|
222
|
+
mockConfig,
|
|
223
|
+
mockStats,
|
|
224
|
+
toolGroups,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(report.issues).toHaveLength(1);
|
|
228
|
+
expect(report.issues[0].tool).toBe("memory_batch_store");
|
|
229
|
+
});
|
|
230
|
+
});
|