@memoryrelay/plugin-memoryrelay-ai 0.11.5 → 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.
@@ -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
+ });