mcvay-mind 1.0.0

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/SKILL.md ADDED
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: mcvay-mind
3
+ description: Typed memory system with YAML schemas, full-text search, wiki-link extraction, and proactive context surfacing for OpenClaw agents
4
+ ---
5
+
6
+ # McVay Mind - Typed Memory System
7
+
8
+ A typed memory system for OpenClaw agents with YAML schemas, full-text search, wiki-link extraction, and proactive context surfacing.
9
+
10
+ ## Quick Start
11
+
12
+ ```bash
13
+ # Add a memory
14
+ node ~/.openclaw/skills/mcvay-mind/index.js add --type preference --title "User prefers concise" --content "User wants responses under 3 sentences"
15
+
16
+ # Query memories
17
+ node ~/.openclaw/skills/mcvay-mind/index.js query --type preference --days 7
18
+
19
+ # Search all memories
20
+ node ~/.openclaw/skills/mcvay-mind/index.js search "user preference"
21
+
22
+ # Rebuild indexes
23
+ node ~/.openclaw/skills/mcvay-mind/index.js rebuild-indexes
24
+
25
+ # Validate schema
26
+ node ~/.openclaw/skills/mcvay-mind/index.js validate
27
+ ```
28
+
29
+ ## Memory Types
30
+
31
+ | Type | Directory | Purpose |
32
+ |------|-----------|---------|
33
+ | `decision` | `memory/typed/decisions/` | Choices made, rationale |
34
+ | `preference` | `memory/typed/preferences/` | Likes, dislikes, wants |
35
+ | `relationship` | `memory/typed/relationships/` | People, roles, connections |
36
+ | `commitment` | `memory/typed/commitments/` | Promises, follow-ups, todos |
37
+ | `lesson` | `memory/typed/lessons/` | Learnings, mistakes, insights |
38
+ | `task` | `memory/typed/tasks/` | Work items, action items |
39
+ | `project` | `memory/typed/projects/` | Projects, initiatives |
40
+
41
+ ## Schema Templates
42
+
43
+ Located in `~/.openclaw/skills/mcvay-mind/schema/`:
44
+
45
+ - `base.yaml` — Base schema all types extend
46
+ - `decision.yaml`, `preference.yaml`, `relationship.yaml`, `commitment.yaml`, `lesson.yaml`, `task.yaml`
47
+
48
+ ## Core Modules (in this skill)
49
+
50
+ ```javascript
51
+ const store = require('~/.openclaw/skills/mcvay-mind/lib/store');
52
+ const search = require('~/.openclaw/skills/mcvay-mind/lib/search');
53
+ const entityLinker = require('~/.openclaw/skills/mcvay-mind/lib/entity-linker');
54
+ ```
55
+
56
+ ## Unified Commands
57
+
58
+ All memory operations are now in this skill:
59
+
60
+ ```bash
61
+ # Add memory
62
+ node ~/.openclaw/skills/mcvay-mind/index.js add --type preference --title "Title" --content "Content"
63
+
64
+ # Query by type
65
+ node ~/.openclaw/skills/mcvay-mind/index.js query --type preference --days 7
66
+
67
+ # Search (unified)
68
+ node ~/.openclaw/skills/mcvay-mind/index.js search "query terms"
69
+
70
+ # Active recall (context surfacing)
71
+ node ~/.openclaw/skills/mcvay-mind/index.js recall "topic"
72
+
73
+ # Entity linking
74
+ node ~/.openclaw/skills/mcvay-mind/index.js link
75
+ ```
76
+
77
+ // Add memory
78
+ await store.addMemory({
79
+ type: 'preference',
80
+ title: 'No football emojis',
81
+ content: 'User prefers no football emojis in responses',
82
+ tags: ['preference', 'communication'],
83
+ confidence: 90
84
+ });
85
+
86
+ // Query by type
87
+ const prefs = await store.queryMemories({ type: 'preference', days: 7 });
88
+
89
+ // Full-text search
90
+ const results = await search.searchMemories({
91
+ query: 'user preference concise',
92
+ types: ['preference', 'lesson'],
93
+ limit: 5
94
+ });
95
+
96
+ // Active recall - context surfacing
97
+ const memories = await activeRecall('conversation topic');
98
+
99
+ // Proactive search - search before responding
100
+ const context = await searchMemories({ query: userQuestion });
101
+ ```
102
+
103
+ ## Proactive Search (MANDATORY)
104
+
105
+ Before ANY response, agents MUST search for relevant context:
106
+
107
+ ```bash
108
+ # CLI
109
+ npx tsx skills/proactive-memory/search.ts --query "user question"
110
+ ```
111
+
112
+ ```typescript
113
+ // Programmatic
114
+ import { searchMemories } from './skills/proactive-memory/search';
115
+ const context = await searchMemories({ query: userMessage });
116
+ ```
117
+
118
+ ### When to Search
119
+
120
+ 1. **Responding to questions** - Find relevant context first
121
+ 2. **Troubleshooting errors** - Look for similar past issues
122
+ 3. **Facing problems** - Search for solutions
123
+ 4. **Making decisions** - Check past decisions
124
+ 5. **User corrections** - Find related lessons
125
+
126
+ ## Entity Linking
127
+
128
+ The system automatically extracts wiki-links `[[type/slug]]` from memories and builds a knowledge graph.
129
+
130
+ ```bash
131
+ # Extract links and rebuild graph
132
+ node ~/.openclaw/skills/mcvay-mind/index.js link
133
+ ```
134
+
135
+ ## Memory Format
136
+
137
+ ```yaml
138
+ ---
139
+ title: "Memory Title"
140
+ type: preference
141
+ created: 2026-02-16T12:00:00.000Z
142
+ updated: 2026-02-16T12:00:00.000Z
143
+ tags: [tag1, tag2]
144
+ links: [decision/choice-1, preference/user-pref]
145
+ confidence: 90
146
+ source: agent
147
+ ---
148
+
149
+ # Memory Title
150
+ ## Content
151
+
152
+ Your memory content here.
153
+ ```
154
+
155
+ ## Configuration
156
+
157
+ Set custom workspace:
158
+ ```bash
159
+ export MCVAY_MIND_CWD=/path/to/workspace
160
+ ```
161
+
162
+ ## Integrations
163
+
164
+ ### AGENTS.md Integration
165
+
166
+ Add to startup:
167
+ ```markdown
168
+ ## Before ANY Response (MANDATORY)
169
+
170
+ Run proactive memory search:
171
+ npx tsx skills/proactive-memory/search.ts --query "<user question>"
172
+
173
+ Include relevant memories in your response.
174
+ ```
175
+
176
+ ### Active Recall Triggers
177
+
178
+ Keywords that should trigger recall:
179
+ - "remember", "recall", "what do you know about"
180
+ - Project names
181
+ - People names
182
+
183
+ ## Files
184
+
185
+ ```
186
+ ~/.openclaw/skills/mcvay-mind/
187
+ ā”œā”€ā”€ index.js # CLI entry point
188
+ ā”œā”€ā”€ lib/
189
+ │ ā”œā”€ā”€ store.js # Memory CRUD operations
190
+ │ ā”œā”€ā”€ search.js # Full-text search
191
+ │ └── entity-linker.js # Wiki-link extraction & knowledge graph
192
+ └── schema/ # YAML schemas
193
+ ā”œā”€ā”€ base.yaml
194
+ ā”œā”€ā”€ decision.yaml
195
+ ā”œā”€ā”€ preference.yaml
196
+ ā”œā”€ā”€ relationship.yaml
197
+ ā”œā”€ā”€ commitment.yaml
198
+ ā”œā”€ā”€ lesson.yaml
199
+ └── task.yaml
200
+ ```
package/index.js ADDED
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * McVay Mind Memory System
5
+ *
6
+ * A typed memory system for OpenClaw agents
7
+ *
8
+ * Usage:
9
+ * node index.js add --type <type> --title "<title>" --content "<content>" [options]
10
+ * node index.js query --type <type> [--search "query"] [--days 7]
11
+ * node index.js search "<query>" [--types preference,lesson]
12
+ * node index.js recall "topic"
13
+ * node index.js link
14
+ * node index.js validate
15
+ * node index.js rebuild-indexes
16
+ */
17
+
18
+ const path = require('path');
19
+ const store = require('./lib/store');
20
+ const search = require('./lib/search');
21
+ const entityLinker = require('./lib/entity-linker');
22
+ const activeRecall = require('./lib/active-recall');
23
+
24
+ // ============================================================================
25
+ // CLI Handler
26
+ // ============================================================================
27
+
28
+ const args = process.argv.slice(2);
29
+ const cwd = process.env.MCVAY_MIND_CWD || process.cwd();
30
+
31
+ function main() {
32
+ if (args.length === 0) {
33
+ printUsage();
34
+ return;
35
+ }
36
+
37
+ const command = args[0];
38
+
39
+ switch (command) {
40
+ case 'add':
41
+ handleAdd();
42
+ break;
43
+ case 'query':
44
+ case 'search':
45
+ handleQuery();
46
+ break;
47
+ case 'recall':
48
+ case 'active-recall':
49
+ handleActiveRecall();
50
+ break;
51
+ case 'link':
52
+ case 'entity-link':
53
+ handleEntityLink();
54
+ break;
55
+ case 'backlinks':
56
+ handleBacklinks();
57
+ break;
58
+ case 'validate':
59
+ handleValidate();
60
+ break;
61
+ case 'rebuild-indexes':
62
+ handleRebuildIndexes();
63
+ break;
64
+ case 'help':
65
+ printUsage();
66
+ break;
67
+ default:
68
+ // Try to treat as a search query
69
+ handleQuery([command, ...args.slice(1)]);
70
+ }
71
+ }
72
+
73
+ function printUsage() {
74
+ console.log(`
75
+ McVay Mind Memory System
76
+ =======================
77
+
78
+ Usage:
79
+ node index.js add --type <type> --title "<title>" --content "<content>" [options]
80
+ node index.js query --type <type> [--days 7]
81
+ node index.js search "<query>" [--types type1,type2]
82
+ node index.js recall "topic"
83
+ node index.js link
84
+ node index.js validate
85
+ node index.js rebuild-indexes
86
+
87
+ Types: decision, preference, relationship, commitment, lesson, task, project
88
+
89
+ Options:
90
+ --type <type> Memory type
91
+ --title "<title>" Memory title
92
+ --content "<text>" Memory content
93
+ --tags <tag1,tag2> Comma-separated tags
94
+ --confidence <n> Confidence level (0-100)
95
+ --days <n> Limit to last N days
96
+ --limit <n> Max results to return
97
+
98
+ Examples:
99
+ node index.js add --type preference --title "Example: concise responses" --content "This user prefers short responses" --tags preference,communication
100
+ node index.js search "user preference" --types preference,lesson
101
+ node index.js recall "codex coding"
102
+ `);
103
+ }
104
+
105
+ // ============================================================================
106
+ // Argument Parsing
107
+ // ============================================================================
108
+
109
+ function parseArgs(argList = args.slice(1)) {
110
+ const options = {};
111
+ let i = 0;
112
+ while (i < argList.length) {
113
+ const arg = argList[i];
114
+ if (arg.startsWith('--')) {
115
+ const key = arg.slice(2);
116
+ const value = argList[i + 1];
117
+ if (value && !value.startsWith('--')) {
118
+ options[key] = value;
119
+ i += 2;
120
+ } else {
121
+ options[key] = 'true';
122
+ i++;
123
+ }
124
+ } else if (!arg.startsWith('-')) {
125
+ // Positional arg
126
+ if (!options._) options._ = [];
127
+ options._.push(arg);
128
+ i++;
129
+ } else {
130
+ i++;
131
+ }
132
+ }
133
+ return options;
134
+ }
135
+
136
+ // ============================================================================
137
+ // Command Handlers
138
+ // ============================================================================
139
+
140
+ function handleAdd() {
141
+ const options = parseArgs(args.slice(1));
142
+ const { type, title, content, tags, confidence, source, ...extra } = options;
143
+
144
+ if (!type || !title || !content) {
145
+ console.error('Error: --type, --title, and --content are required');
146
+ process.exit(1);
147
+ }
148
+
149
+ const tagList = tags ? tags.split(',').map(t => t.trim()) : [];
150
+ const conf = confidence ? parseInt(confidence, 10) : 80;
151
+
152
+ const extraFields = {};
153
+ for (const [key, value] of Object.entries(extra)) {
154
+ if (key !== '_') {
155
+ extraFields[key] = value;
156
+ }
157
+ }
158
+
159
+ const result = store.ingest({
160
+ type,
161
+ title,
162
+ content,
163
+ tags: tagList,
164
+ confidence: conf,
165
+ source: source || 'agent',
166
+ ...extraFields
167
+ }, cwd);
168
+
169
+ if (result.success) {
170
+ console.log(`āœ“ Memory ingested: ${result.slug}`);
171
+ } else {
172
+ console.error('āœ— Error ingesting memory:');
173
+ for (const err of result.errors || []) {
174
+ console.error(` - ${err.field}: ${err.message}`);
175
+ }
176
+ process.exit(1);
177
+ }
178
+ }
179
+
180
+ async function handleQuery(argList = null) {
181
+ const options = parseArgs(argList || args.slice(1));
182
+ const { type, search: searchTerm, types, days, limit, 'min-confidence': minConf, _ } = options;
183
+
184
+ const queryText = searchTerm || (_ && _.length > 0 ? _.join(' ') : null);
185
+
186
+ if (queryText) {
187
+ const typeList = types ? types.split(',') : (type ? [type] : undefined);
188
+ const results = await search.searchMemories({
189
+ query: queryText,
190
+ types: typeList,
191
+ days: days ? parseInt(days, 10) : undefined,
192
+ limit: limit ? parseInt(limit, 10) : 10,
193
+ minConfidence: minConf ? parseInt(minConf, 10) : undefined
194
+ }, cwd);
195
+
196
+ console.log(search.formatResults(results));
197
+ } else if (type || types) {
198
+ const typeList = (types || type).split(',');
199
+ const results = store.queryMemories({
200
+ type: typeList[0],
201
+ days: days ? parseInt(days, 10) : undefined,
202
+ limit: limit ? parseInt(limit, 10) : 50,
203
+ minConfidence: minConf ? parseInt(minConf, 10) : undefined
204
+ }, cwd);
205
+
206
+ if (results.length === 0) {
207
+ console.log('No memories found.');
208
+ return;
209
+ }
210
+
211
+ console.log(`Found ${results.length} memory(ies):\n`);
212
+
213
+ const icon = {
214
+ decision: 'šŸ“', preference: 'āš™ļø', lesson: 'šŸ“š',
215
+ commitment: 'āœ…', relationship: 'šŸ‘¤', task: 'šŸŽÆ', project: 'šŸ“'
216
+ };
217
+
218
+ for (const r of results) {
219
+ const fm = r.frontmatter;
220
+ const date = fm.created ? new Date(fm.created).toISOString().split('T')[0] : 'unknown';
221
+ console.log(`${icon[r.type] || 'šŸ“Œ'} [${r.type}] ${fm.title} (confidence: ${fm.confidence || 'N/A'})`);
222
+ console.log(` ${date} | ${r.slug}`);
223
+ if (fm.tags && fm.tags.length > 0) {
224
+ console.log(` Tags: ${fm.tags.join(', ')}`);
225
+ }
226
+ console.log('');
227
+ }
228
+ } else {
229
+ console.error('Error: --search or --type is required');
230
+ process.exit(1);
231
+ }
232
+ }
233
+
234
+ function handleActiveRecall() {
235
+ const options = parseArgs(args.slice(1));
236
+ const { query, _ } = options;
237
+
238
+ const queryText = query || (_ && _.length > 0 ? _.join(' ') : null);
239
+
240
+ if (!queryText) {
241
+ console.log('Usage: node index.js recall "topic"');
242
+ console.log(' or: node index.js recall --query "topic"');
243
+ process.exit(1);
244
+ }
245
+
246
+ activeRecall.activeRecall(queryText).then(data => {
247
+ console.log(activeRecall.formatResults(data));
248
+ }).catch(err => {
249
+ console.error('Error:', err.message);
250
+ process.exit(1);
251
+ });
252
+ }
253
+
254
+ function handleEntityLink() {
255
+ entityLinker.runEntityLinker(cwd);
256
+ }
257
+
258
+ function handleBacklinks() {
259
+ const options = parseArgs(args.slice(1));
260
+ const slug = options._ && options._[0] ? options._[0] : options.backlinks;
261
+
262
+ if (!slug) {
263
+ console.error('Usage: node index.js backlinks <slug>');
264
+ process.exit(1);
265
+ }
266
+
267
+ const backlinks = store.getBacklinks(slug, cwd);
268
+ const forwardLinks = store.getForwardLinks(slug, cwd);
269
+
270
+ console.log(`Links for: ${slug}\n`);
271
+ console.log(`Backlinks (${backlinks.length}):`);
272
+ for (const l of backlinks) {
273
+ console.log(` ← ${l}`);
274
+ }
275
+ console.log(`\nForward Links (${forwardLinks.length}):`);
276
+ for (const l of forwardLinks) {
277
+ console.log(` → ${l}`);
278
+ }
279
+ }
280
+
281
+ function handleValidate() {
282
+ const errors = store.validateAll(cwd);
283
+
284
+ if (errors.length === 0) {
285
+ console.log('āœ“ All memories are valid.');
286
+ } else {
287
+ console.error(`āœ— Found ${errors.length} validation error(s):`);
288
+ for (const err of errors) {
289
+ console.error(` - ${err.file}: ${err.error}`);
290
+ }
291
+ process.exit(1);
292
+ }
293
+ }
294
+
295
+ function handleRebuildIndexes() {
296
+ console.log('Rebuilding indexes...');
297
+ store.rebuildIndexes(cwd);
298
+ console.log('āœ“ Indexes rebuilt.');
299
+ }
300
+
301
+ // ============================================================================
302
+ // Export for programmatic use
303
+ // ============================================================================
304
+
305
+ module.exports = {
306
+ store,
307
+ search: {
308
+ searchMemories: search.searchMemories,
309
+ searchForUserQuestion: search.searchForUserQuestion,
310
+ searchForError: search.searchForError,
311
+ searchRecentPreferences: search.searchRecentPreferences,
312
+ searchRecentLessons: search.searchRecentLessons,
313
+ searchRecentCommitments: search.searchRecentCommitments,
314
+ formatResults: search.formatResults,
315
+ },
316
+ entityLinker: {
317
+ runEntityLinker: entityLinker.runEntityLinker,
318
+ },
319
+ activeRecall: {
320
+ recall: activeRecall.activeRecall,
321
+ extractKeywords: activeRecall.extractKeywords,
322
+ },
323
+ onUserCorrection: (correction, original, context) => store.ingest({
324
+ type: 'lesson',
325
+ title: `User correction: ${correction.substring(0, 50)}`,
326
+ content: `Original: ${original}\nCorrection: ${correction}\n\nContext: ${context || 'N/A'}`,
327
+ tags: ['correction', 'lesson'],
328
+ source: 'agent',
329
+ lesson_type: 'correction'
330
+ }, cwd),
331
+ onError: (error, context) => store.ingest({
332
+ type: 'lesson',
333
+ title: `Error: ${error.substring(0, 50)}`,
334
+ content: error,
335
+ tags: ['error', 'tool-failure'],
336
+ source: 'agent',
337
+ lesson_type: 'error'
338
+ }, cwd),
339
+ VALID_TYPES: store.VALID_TYPES,
340
+ TYPE_DIRS: store.TYPE_DIRS,
341
+ };
342
+
343
+ // Run if called directly
344
+ main();
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Active Recall - Context Surfacing
3
+ * Proactively surfaces relevant memories during conversations
4
+ */
5
+
6
+ const store = require('./store');
7
+
8
+ // Stop words for keyword extraction
9
+ const STOP_WORDS = new Set([
10
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'do', 'does', 'did',
11
+ 'what', 'how', 'why', 'when', 'where', 'who', 'can', 'could',
12
+ 'would', 'should', 'to', 'of', 'in', 'on', 'at', 'for', 'with', 'about'
13
+ ]);
14
+
15
+ /**
16
+ * Extract keywords from query
17
+ */
18
+ function extractKeywords(query) {
19
+ return query.toLowerCase()
20
+ .replace(/[^\w\s]/g, ' ')
21
+ .split(/\s+/)
22
+ .filter(w => w.length > 2 && !STOP_WORDS.has(w));
23
+ }
24
+
25
+ /**
26
+ * Score memory relevance
27
+ */
28
+ function scoreMemory(memory, keywords) {
29
+ let score = parseInt(memory.frontmatter?.confidence || '50', 10);
30
+
31
+ // Boost for keyword matches in title
32
+ const title = (memory.frontmatter?.title || '').toLowerCase();
33
+ for (const kw of keywords) {
34
+ if (title.includes(kw)) score += 20;
35
+ }
36
+
37
+ // Boost for recency (newer = better)
38
+ if (memory.frontmatter?.created) {
39
+ const daysOld = (Date.now() - new Date(memory.frontmatter.created).getTime()) / (1000 * 60 * 60 * 24);
40
+ if (daysOld < 1) score += 10;
41
+ else if (daysOld < 7) score += 5;
42
+ }
43
+
44
+ return Math.min(score, 100);
45
+ }
46
+
47
+ /**
48
+ * Query memory for related memories
49
+ */
50
+ function queryMemoryGraph(keywords) {
51
+ const results = [];
52
+
53
+ for (const kw of keywords) {
54
+ const matches = store.queryMemories({ search: kw, limit: 50 });
55
+ results.push(...matches);
56
+ }
57
+
58
+ // Deduplicate by slug
59
+ const seen = new Set();
60
+ const unique = [];
61
+ for (const r of results) {
62
+ if (!seen.has(r.slug)) {
63
+ seen.add(r.slug);
64
+ unique.push(r);
65
+ }
66
+ }
67
+
68
+ return unique;
69
+ }
70
+
71
+ /**
72
+ * Main active recall function
73
+ */
74
+ async function activeRecall(query) {
75
+ if (!query) {
76
+ return { error: 'No query provided' };
77
+ }
78
+
79
+ const keywords = extractKeywords(query);
80
+
81
+ // Query memory
82
+ const graphResults = queryMemoryGraph(keywords);
83
+
84
+ // Score and sort
85
+ const scored = graphResults.map(m => ({
86
+ ...m,
87
+ score: scoreMemory(m, keywords),
88
+ forwardLinks: store.getForwardLinks(m.slug).length,
89
+ backlinks: store.getBacklinks(m.slug).length
90
+ })).filter(m => m.score > 30);
91
+
92
+ scored.sort((a, b) => b.score - a.score);
93
+
94
+ // Format results
95
+ const icons = {
96
+ decision: 'šŸ“',
97
+ preference: 'āš™ļø',
98
+ lesson: 'šŸ“š',
99
+ commitment: 'āœ…',
100
+ relationship: 'šŸ‘¤',
101
+ task: 'šŸŽÆ',
102
+ project: 'šŸ“'
103
+ };
104
+
105
+ const results = scored.slice(0, 10).map(m => ({
106
+ icon: icons[m.type] || 'šŸ“Œ',
107
+ type: m.type,
108
+ title: m.frontmatter?.title,
109
+ confidence: m.frontmatter?.confidence || '50',
110
+ related: m.forwardLinks + m.backlinks,
111
+ summary: m.content?.substring(0, 200) || '',
112
+ source: `memory/typed/${m.type}s/${m.slug}.md`
113
+ }));
114
+
115
+ return {
116
+ query,
117
+ keywords,
118
+ count: results.length,
119
+ results
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Format for CLI output
125
+ */
126
+ function formatResults(data) {
127
+ if (data.error) {
128
+ return `Error: ${data.error}`;
129
+ }
130
+
131
+ if (data.count === 0) {
132
+ return `No relevant memories found for "${data.query}"`;
133
+ }
134
+
135
+ let output = `\nšŸŽÆ Active Recall: "${data.query}"\n`;
136
+ output += `Found ${data.count} relevant memories:\n\n`;
137
+
138
+ for (const r of data.results) {
139
+ output += `${r.icon} [${r.type}] ${r.title}\n`;
140
+ output += ` Confidence: ${r.confidence}% | Related: ${r.related} connections\n`;
141
+ output += ` ${r.summary.substring(0, 100)}...\n`;
142
+ output += ` Source: ${r.source}\n\n`;
143
+ }
144
+
145
+ return output;
146
+ }
147
+
148
+ // CLI handler - only run if called directly
149
+ if (require.main === module) {
150
+ const args = process.argv.slice(2);
151
+ const queryArg = args.findIndex(a => a === '--query' || a === '-q');
152
+ const query = queryArg >= 0 ? args[queryArg + 1] : args.join(' ');
153
+
154
+ if (!query) {
155
+ console.log('Usage: node lib/active-recall.js --query "topic"');
156
+ console.log(' or: node lib/active-recall.js "topic phrase"');
157
+ process.exit(1);
158
+ }
159
+
160
+ activeRecall(query).then(data => {
161
+ console.log(formatResults(data));
162
+ }).catch(err => {
163
+ console.error('Error:', err.message);
164
+ process.exit(1);
165
+ });
166
+ }
167
+
168
+ module.exports = { activeRecall, extractKeywords, scoreMemory, formatResults };