@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.
- package/README.md +249 -23
- package/index.ts +1 -1
- package/package.json +4 -3
- 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,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
|
+
});
|