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 +200 -0
- package/index.js +344 -0
- package/lib/active-recall.js +168 -0
- package/lib/entity-linker.js +238 -0
- package/lib/proactive-search.js +285 -0
- package/lib/search.js +387 -0
- package/lib/store.js +726 -0
- package/package.json +19 -0
- package/schema/base.yaml +71 -0
- package/schema/commitment.yaml +57 -0
- package/schema/decision.yaml +81 -0
- package/schema/lesson.yaml +50 -0
- package/schema/preference.yaml +37 -0
- package/schema/relationship.yaml +51 -0
- package/schema/task.yaml +53 -0
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 };
|