@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.
package/index.ts CHANGED
@@ -1,16 +1,37 @@
1
1
  /**
2
2
  * OpenClaw Memory Plugin - MemoryRelay
3
- * Version: 0.11.5 (Full Single-File)
3
+ * Version: 0.12.0 (Phase 1 - Adoption Framework)
4
4
  *
5
5
  * Long-term memory with vector search using MemoryRelay API.
6
6
  * Provides auto-recall and auto-capture via lifecycle hooks.
7
7
  * Includes: memories, entities, agents, sessions, decisions, patterns, projects.
8
+ * New in v0.12.0: Smart auto-capture, daily stats, CLI commands, onboarding
8
9
  *
9
10
  * API: https://api.memoryrelay.net
10
11
  * Docs: https://memoryrelay.ai
11
12
  */
12
13
 
13
14
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
15
+ import {
16
+ calculateStats,
17
+ morningCheck,
18
+ eveningReview,
19
+ shouldRunHeartbeat,
20
+ formatStatsForDisplay,
21
+ type DailyStatsConfig,
22
+ type MemoryStats,
23
+ } from "./src/heartbeat/daily-stats.js";
24
+ import {
25
+ statsCommand,
26
+ type StatsCommandOptions,
27
+ } from "./src/cli/stats-command.js";
28
+ import {
29
+ checkFirstRun,
30
+ generateOnboardingPrompt,
31
+ generateSuccessMessage,
32
+ runSimpleOnboarding,
33
+ type OnboardingResult,
34
+ } from "./src/onboarding/first-run.js";
14
35
 
15
36
  // ============================================================================
16
37
  // Constants
@@ -131,7 +152,7 @@ interface MemoryStats {
131
152
  interface PluginConfig {
132
153
  agentId: string;
133
154
  autoRecall: boolean;
134
- autoCapture: boolean;
155
+ autoCapture: AutoCaptureConfig; // Updated in v0.12.0
135
156
  recallLimit: number;
136
157
  recallThreshold: number;
137
158
  excludeChannels: string[];
@@ -283,7 +304,10 @@ class StatusReporter {
283
304
  ? `✓ Enabled (limit: ${report.config.recallLimit}, threshold: ${report.config.recallThreshold})`
284
305
  : "✗ Disabled";
285
306
  lines.push(` Auto-Recall: ${recallStatus}`);
286
- lines.push(` Auto-Capture: ${report.config.autoCapture ? "✓ Enabled" : "✗ Disabled"}`);
307
+ const captureStatus = report.config.autoCapture.enabled
308
+ ? `✓ Enabled (tier: ${report.config.autoCapture.tier})`
309
+ : "✗ Disabled";
310
+ lines.push(` Auto-Capture: ${captureStatus}`);
287
311
  if (report.config.defaultProject) {
288
312
  lines.push(` Default Project: ${report.config.defaultProject}`);
289
313
  }
@@ -377,17 +401,35 @@ class StatusReporter {
377
401
  }
378
402
  }
379
403
 
404
+ // Auto-capture configuration types (Phase 1 - Issue #12)
405
+ type AutoCaptureTier = "off" | "conservative" | "smart" | "aggressive";
406
+
407
+ interface AutoCaptureConfig {
408
+ enabled: boolean;
409
+ tier: AutoCaptureTier;
410
+ confirmFirst?: number; // Number of captures to confirm (default: 5)
411
+ categories?: {
412
+ credentials?: boolean;
413
+ preferences?: boolean;
414
+ technical?: boolean;
415
+ personal?: boolean;
416
+ };
417
+ blocklist?: string[]; // Regex patterns to never capture
418
+ }
419
+
380
420
  interface MemoryRelayConfig {
381
421
  apiKey?: string;
382
422
  agentId?: string;
383
423
  apiUrl?: string;
384
- autoCapture?: boolean;
424
+ autoCapture?: boolean | AutoCaptureConfig; // Enhanced in v0.12.0
385
425
  autoRecall?: boolean;
386
426
  recallLimit?: number;
387
427
  recallThreshold?: number;
388
428
  excludeChannels?: string[];
389
429
  defaultProject?: string;
390
430
  enabledTools?: string;
431
+ // Daily stats configuration (v0.12.0)
432
+ dailyStats?: DailyStatsConfig;
391
433
  // Debug and logging options (v0.8.0)
392
434
  debug?: boolean;
393
435
  verbose?: boolean;
@@ -471,6 +513,100 @@ async function fetchWithTimeout(
471
513
  }
472
514
  }
473
515
 
516
+ // ============================================================================
517
+ // Auto-Capture Configuration Helpers (Phase 1 - Issue #12)
518
+ // ============================================================================
519
+
520
+ /**
521
+ * Normalize auto-capture config from boolean or object format
522
+ */
523
+ function normalizeAutoCaptureConfig(
524
+ config: boolean | AutoCaptureConfig | undefined
525
+ ): AutoCaptureConfig {
526
+ // Default configuration (smart auto-capture enabled by default in v0.12.0)
527
+ const defaultConfig: AutoCaptureConfig = {
528
+ enabled: true,
529
+ tier: "smart",
530
+ confirmFirst: 5,
531
+ categories: {
532
+ credentials: true,
533
+ preferences: true,
534
+ technical: true,
535
+ personal: false, // Privacy: personal info requires confirmation
536
+ },
537
+ blocklist: [
538
+ // Privacy patterns - never auto-capture
539
+ /password\s*[:=]\s*[^\s]+/i,
540
+ /credit\s*card/i,
541
+ /ssn\s*[:=]/i,
542
+ /social\s*security/i,
543
+ ].map((r) => r.source),
544
+ };
545
+
546
+ // Handle legacy boolean config
547
+ if (typeof config === "boolean") {
548
+ return {
549
+ ...defaultConfig,
550
+ enabled: config,
551
+ };
552
+ }
553
+
554
+ // Handle undefined (use smart default in v0.12.0+)
555
+ if (config === undefined) {
556
+ return defaultConfig;
557
+ }
558
+
559
+ // Merge provided config with defaults
560
+ return {
561
+ enabled: config.enabled ?? defaultConfig.enabled,
562
+ tier: config.tier ?? defaultConfig.tier,
563
+ confirmFirst: config.confirmFirst ?? defaultConfig.confirmFirst,
564
+ categories: {
565
+ ...defaultConfig.categories,
566
+ ...config.categories,
567
+ },
568
+ blocklist: config.blocklist ?? defaultConfig.blocklist,
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Check if content matches any blocklist patterns
574
+ */
575
+ function isBlocklisted(content: string, blocklist: string[]): boolean {
576
+ return blocklist.some((pattern) => {
577
+ try {
578
+ return new RegExp(pattern, "i").test(content);
579
+ } catch {
580
+ return false; // Invalid regex, skip
581
+ }
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Mask sensitive data in content (API keys, tokens, etc.)
587
+ */
588
+ function maskSensitiveData(content: string): string {
589
+ // Mask API keys (show only last 4 chars)
590
+ content = content.replace(
591
+ /\b([a-z]{2,}_)?([a-z]{4,}_)?[a-f0-9]{32,}\b/gi,
592
+ (match) => {
593
+ if (match.length <= 8) return match;
594
+ return `${match.slice(0, 4)}...${match.slice(-4)}`;
595
+ }
596
+ );
597
+
598
+ // Mask email addresses (show only domain)
599
+ content = content.replace(
600
+ /\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b/gi,
601
+ (match) => {
602
+ const domain = match.split("@")[1];
603
+ return `***@${domain}`;
604
+ }
605
+ );
606
+
607
+ return content;
608
+ }
609
+
474
610
  // ============================================================================
475
611
  // MemoryRelay API Client (Full Suite)
476
612
  // ============================================================================
@@ -1208,11 +1344,13 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
1208
1344
  total_memories: memoryCount,
1209
1345
  };
1210
1346
 
1211
- // Get config
1347
+ // Get config - normalize autoCapture to new format
1348
+ const autoCaptureConfig = normalizeAutoCaptureConfig(cfg?.autoCapture);
1349
+
1212
1350
  const pluginConfig = {
1213
1351
  agentId: agentId,
1214
1352
  autoRecall: cfg?.autoRecall ?? true,
1215
- autoCapture: cfg?.autoCapture ?? false,
1353
+ autoCapture: autoCaptureConfig,
1216
1354
  recallLimit: cfg?.recallLimit ?? 5,
1217
1355
  recallThreshold: cfg?.recallThreshold ?? 0.3,
1218
1356
  excludeChannels: cfg?.excludeChannels ?? [],
@@ -3641,7 +3779,9 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
3641
3779
  });
3642
3780
 
3643
3781
  // Auto-capture: analyze and store important information after agent ends
3644
- if (cfg?.autoCapture) {
3782
+ const autoCaptureConfig = normalizeAutoCaptureConfig(cfg?.autoCapture);
3783
+
3784
+ if (autoCaptureConfig.enabled) {
3645
3785
  api.on("agent_end", async (event) => {
3646
3786
  if (!event.success || !event.messages || event.messages.length === 0) {
3647
3787
  return;
@@ -3673,7 +3813,13 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
3673
3813
  }
3674
3814
  }
3675
3815
 
3676
- const toCapture = texts.filter((text) => text && shouldCapture(text));
3816
+ const toCapture = texts.filter((text) => {
3817
+ if (!text || !shouldCapture(text)) return false;
3818
+ // Check blocklist
3819
+ if (isBlocklisted(text, autoCaptureConfig.blocklist || [])) return false;
3820
+ return true;
3821
+ });
3822
+
3677
3823
  if (toCapture.length === 0) return;
3678
3824
 
3679
3825
  let stored = 0;
@@ -3696,9 +3842,43 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
3696
3842
  }
3697
3843
 
3698
3844
  api.logger.info?.(
3699
- `memory-memoryrelay: plugin v0.11.4 loaded (39 tools, autoRecall: ${cfg?.autoRecall}, autoCapture: ${cfg?.autoCapture}, debug: ${debugEnabled})`,
3845
+ `memory-memoryrelay: plugin v0.12.0 loaded (39 tools, autoRecall: ${cfg?.autoRecall}, autoCapture: ${autoCaptureConfig.enabled ? autoCaptureConfig.tier : 'off'}, debug: ${debugEnabled})`,
3700
3846
  );
3701
3847
 
3848
+ // ========================================================================
3849
+ // First-Run Onboarding (Phase 1 - Issue #9)
3850
+ // ========================================================================
3851
+
3852
+ // Check if this is the first run and auto-onboard if needed
3853
+ try {
3854
+ const onboardingCheck = await checkFirstRun(async () => {
3855
+ const memories = await client.list(1);
3856
+ return memories.length;
3857
+ });
3858
+
3859
+ if (onboardingCheck.shouldOnboard) {
3860
+ // Auto-onboard with simple setup
3861
+ await runSimpleOnboarding(
3862
+ async (content, metadata) => {
3863
+ const memory = await client.store(content, metadata || {});
3864
+ return { id: memory.id };
3865
+ },
3866
+ "Welcome to MemoryRelay! This is your first memory. Use memory_store to add more.",
3867
+ autoCaptureConfig.enabled
3868
+ );
3869
+
3870
+ const successMsg = generateSuccessMessage(
3871
+ "Welcome to MemoryRelay! This is your first memory.",
3872
+ autoCaptureConfig.enabled
3873
+ );
3874
+
3875
+ api.logger.info?.(`\n${successMsg}`);
3876
+ }
3877
+ } catch (err) {
3878
+ // Don't fail plugin load if onboarding fails
3879
+ api.logger.warn?.(`memory-memoryrelay: onboarding check failed: ${String(err)}`);
3880
+ }
3881
+
3702
3882
  // ========================================================================
3703
3883
  // CLI Helper Tools (v0.8.0)
3704
3884
  // ========================================================================
@@ -3860,6 +4040,92 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
3860
4040
  });
3861
4041
  }
3862
4042
 
4043
+ // memoryrelay:heartbeat - Daily stats check (Phase 1 - Issue #10)
4044
+ api.registerGatewayMethod?.("memoryrelay.heartbeat", async ({ respond, args }) => {
4045
+ try {
4046
+ const dailyStatsConfig: DailyStatsConfig = {
4047
+ enabled: cfg?.dailyStats?.enabled ?? true,
4048
+ morningTime: cfg?.dailyStats?.morningTime || "09:00",
4049
+ eveningTime: cfg?.dailyStats?.eveningTime || "20:00",
4050
+ };
4051
+
4052
+ // Check if it's time for a heartbeat
4053
+ const heartbeatType = shouldRunHeartbeat(dailyStatsConfig);
4054
+
4055
+ if (!heartbeatType) {
4056
+ respond(true, {
4057
+ type: "none",
4058
+ message: "Not scheduled for heartbeat check right now",
4059
+ });
4060
+ return;
4061
+ }
4062
+
4063
+ // Calculate stats
4064
+ const memories = await client.list(1000); // Get recent memories
4065
+ const stats = await calculateStats(
4066
+ async () => memories,
4067
+ () => 0 // Recall count not tracked yet (Phase 3)
4068
+ );
4069
+
4070
+ // Run appropriate check
4071
+ let result;
4072
+ if (heartbeatType === "morning") {
4073
+ result = await morningCheck(stats);
4074
+ } else {
4075
+ result = await eveningReview(stats);
4076
+ }
4077
+
4078
+ respond(true, {
4079
+ type: heartbeatType,
4080
+ shouldNotify: result.shouldNotify,
4081
+ message: result.message,
4082
+ stats: result.stats,
4083
+ });
4084
+ } catch (err) {
4085
+ respond(false, { error: String(err) });
4086
+ }
4087
+ });
4088
+
4089
+ // memoryrelay:onboarding - Show onboarding prompt (Phase 1 - Issue #9)
4090
+ api.registerGatewayMethod?.("memoryrelay.onboarding", async ({ respond }) => {
4091
+ try {
4092
+ const onboardingCheck = await checkFirstRun(async () => {
4093
+ const memories = await client.list(1);
4094
+ return memories.length;
4095
+ });
4096
+
4097
+ const prompt = generateOnboardingPrompt();
4098
+
4099
+ respond(true, {
4100
+ isFirstRun: onboardingCheck.isFirstRun,
4101
+ alreadyOnboarded: onboardingCheck.state?.completed || false,
4102
+ prompt,
4103
+ });
4104
+ } catch (err) {
4105
+ respond(false, { error: String(err) });
4106
+ }
4107
+ });
4108
+
4109
+ // memoryrelay:stats - CLI stats command (Phase 1 - Issue #11)
4110
+ api.registerGatewayMethod?.("memoryrelay.stats", async ({ respond, args }) => {
4111
+ try {
4112
+ const options: StatsCommandOptions = {
4113
+ format: (args?.format as "text" | "json") || "text",
4114
+ verbose: Boolean(args?.verbose),
4115
+ };
4116
+
4117
+ const memories = await client.list(1000);
4118
+ const output = await statsCommand(async () => memories, options);
4119
+
4120
+ respond(true, {
4121
+ output,
4122
+ format: options.format,
4123
+ });
4124
+ } catch (err) {
4125
+ respond(false, { error: String(err) });
4126
+ }
4127
+ });
4128
+
3863
4129
  // memoryrelay:test - Test individual tool
3864
4130
  api.registerGatewayMethod?.("memoryrelay.test", async ({ respond, args }) => {
3865
4131
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoryrelay/plugin-memoryrelay-ai",
3
- "version": "0.11.5",
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",
@@ -33,8 +33,10 @@
33
33
  "openclaw": ">=2026.2.0"
34
34
  },
35
35
  "devDependencies": {
36
- "vitest": "^1.2.0",
37
- "@vitest/coverage-v8": "^1.2.0"
36
+ "@types/node": "^25.3.5",
37
+ "@vitest/coverage-v8": "^1.2.0",
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^1.2.0"
38
40
  },
39
41
  "openclaw": {
40
42
  "extensions": [
@@ -45,7 +47,8 @@
45
47
  "index.ts",
46
48
  "openclaw.plugin.json",
47
49
  "README.md",
48
- "LICENSE"
50
+ "LICENSE",
51
+ "src/"
49
52
  ],
50
53
  "engines": {
51
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
+ }