mcvay-mind 1.1.0 → 1.1.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.js +42 -0
- package/lib/auto-memory-capture.js +270 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* node index.js embed-rebuild [--types preference,lesson] [--batch-size 16]
|
|
19
19
|
* node index.js detect-preferences --from <file>
|
|
20
20
|
* node index.js feedback-preference --label <correct|missed|wrong> --utterance "..." --expected "..."
|
|
21
|
+
* node index.js auto-capture status|enable|disable
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
const store = require('./lib/store');
|
|
@@ -31,6 +32,7 @@ const expandCache = require('./lib/expand-cache');
|
|
|
31
32
|
const metrics = require('./lib/metrics');
|
|
32
33
|
const { extractPreferences } = require('./lib/preference-extractor/Rule');
|
|
33
34
|
const preferenceState = require('./lib/preference-state');
|
|
35
|
+
const autoMemoryCapture = require('./lib/auto-memory-capture');
|
|
34
36
|
const { readFileSync } = require('fs');
|
|
35
37
|
|
|
36
38
|
// ============================================================================
|
|
@@ -94,6 +96,9 @@ async function main() {
|
|
|
94
96
|
case 'preference-state':
|
|
95
97
|
handlePreferenceState();
|
|
96
98
|
break;
|
|
99
|
+
case 'auto-capture':
|
|
100
|
+
handleAutoCapture();
|
|
101
|
+
break;
|
|
97
102
|
case 'help':
|
|
98
103
|
printUsage();
|
|
99
104
|
break;
|
|
@@ -123,6 +128,7 @@ Usage:
|
|
|
123
128
|
node index.js detect-preferences --from <file>
|
|
124
129
|
node index.js feedback-preference --label <correct|missed|wrong> --utterance "..." --expected "..."
|
|
125
130
|
node index.js preference-state --query "<message>"
|
|
131
|
+
node index.js auto-capture status|enable|disable
|
|
126
132
|
|
|
127
133
|
Types: decision, preference, relationship, commitment, lesson, task, project, moc
|
|
128
134
|
|
|
@@ -183,6 +189,7 @@ Examples:
|
|
|
183
189
|
node index.js detect-preferences --from ./conversation.txt
|
|
184
190
|
node index.js feedback-preference --label missed --utterance "avoid emoji" --expected "Don't use emoji."
|
|
185
191
|
node index.js preference-state --query "writing a Discord reply"
|
|
192
|
+
node index.js auto-capture status
|
|
186
193
|
`);
|
|
187
194
|
}
|
|
188
195
|
|
|
@@ -553,6 +560,41 @@ function handleRebuildIndexes() {
|
|
|
553
560
|
console.log('✓ Indexes rebuilt.');
|
|
554
561
|
}
|
|
555
562
|
|
|
563
|
+
function handleAutoCapture() {
|
|
564
|
+
const action = args[1];
|
|
565
|
+
|
|
566
|
+
if (!action || action === 'help') {
|
|
567
|
+
console.log('Usage:');
|
|
568
|
+
console.log(' node index.js auto-capture status');
|
|
569
|
+
console.log(' node index.js auto-capture enable');
|
|
570
|
+
console.log(' node index.js auto-capture disable');
|
|
571
|
+
process.exit(action ? 0 : 1);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (action === 'status') {
|
|
575
|
+
const state = autoMemoryCapture.getAutoCaptureState(cwd);
|
|
576
|
+
console.log(`Auto-capture: ${state.enabled ? 'enabled' : 'disabled'}`);
|
|
577
|
+
console.log(`Last capture at: ${state.last_capture_at || 'never'}`);
|
|
578
|
+
console.log(`Updated at: ${state.updated_at || 'never'}`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (action === 'enable') {
|
|
583
|
+
autoMemoryCapture.setAutoCaptureEnabled(true, cwd);
|
|
584
|
+
console.log('✓ Auto-capture enabled');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (action === 'disable') {
|
|
589
|
+
autoMemoryCapture.setAutoCaptureEnabled(false, cwd);
|
|
590
|
+
console.log('✓ Auto-capture disabled');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.error(`Unknown auto-capture action: ${action}`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
556
598
|
async function handleEmbedRebuild() {
|
|
557
599
|
const options = parseArgs(args.slice(1));
|
|
558
600
|
const types = options.types ? options.types.split(',').map(t => t.trim()).filter(Boolean) : undefined;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
|
|
2
|
+
const { createHash } = require('crypto');
|
|
3
|
+
const { join, dirname } = require('path');
|
|
4
|
+
const store = require('./store');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_STATE = {
|
|
7
|
+
enabled: true,
|
|
8
|
+
updated_at: null,
|
|
9
|
+
last_capture_hash: null,
|
|
10
|
+
last_capture_at: null,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PATTERN_CONFIG = {
|
|
14
|
+
positive: [
|
|
15
|
+
'\\bthanks\\b',
|
|
16
|
+
'\\bthank\\s*you\\b',
|
|
17
|
+
'\\bthx\\b',
|
|
18
|
+
'\\bappreciate\\s+it\\b',
|
|
19
|
+
'\\bgood\\s+job\\b',
|
|
20
|
+
'\\bgreat\\s+job\\b',
|
|
21
|
+
'\\bnice\\s+work\\b',
|
|
22
|
+
'\\bwell\\s+done\\b',
|
|
23
|
+
'认同',
|
|
24
|
+
'同意',
|
|
25
|
+
'赞同',
|
|
26
|
+
'做得好',
|
|
27
|
+
],
|
|
28
|
+
correction: [
|
|
29
|
+
'^no\\b',
|
|
30
|
+
"that's\\s+wrong",
|
|
31
|
+
'\\byou\\s+are\\s+wrong\\b',
|
|
32
|
+
'\\bincorrect\\b',
|
|
33
|
+
'\\bnot\\s+quite\\b',
|
|
34
|
+
'\\bactually\\b',
|
|
35
|
+
'\\bto\\s+clarify\\b',
|
|
36
|
+
'\\bthat\\s+is\\s+not\\s+right\\b',
|
|
37
|
+
'\\byou\\s+missed\\b',
|
|
38
|
+
'\\bnope\\b',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function getStatePath(cwd = process.cwd()) {
|
|
43
|
+
return join(cwd, 'state', 'mcvay-mind-auto-capture.json');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureDirectory(filePath) {
|
|
47
|
+
const directory = dirname(filePath);
|
|
48
|
+
if (directory && !existsSync(directory)) {
|
|
49
|
+
mkdirSync(directory, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJsonSafe(filePath) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeJson(filePath, value) {
|
|
62
|
+
ensureDirectory(filePath);
|
|
63
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getAutoCaptureState(cwd = process.cwd()) {
|
|
67
|
+
const statePath = getStatePath(cwd);
|
|
68
|
+
if (!existsSync(statePath)) {
|
|
69
|
+
return { ...DEFAULT_STATE };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const raw = readJsonSafe(statePath);
|
|
73
|
+
if (!raw || typeof raw !== 'object') {
|
|
74
|
+
return { ...DEFAULT_STATE };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...DEFAULT_STATE,
|
|
79
|
+
...raw,
|
|
80
|
+
enabled: raw.enabled !== false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function setAutoCaptureEnabled(enabled, cwd = process.cwd()) {
|
|
85
|
+
const previous = getAutoCaptureState(cwd);
|
|
86
|
+
const next = {
|
|
87
|
+
...previous,
|
|
88
|
+
enabled: Boolean(enabled),
|
|
89
|
+
updated_at: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
writeJson(getStatePath(cwd), next);
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizePatternConfig(patternConfig = {}) {
|
|
96
|
+
const input = patternConfig && typeof patternConfig === 'object' ? patternConfig : {};
|
|
97
|
+
const positive = Array.isArray(input.positive) ? input.positive : DEFAULT_PATTERN_CONFIG.positive;
|
|
98
|
+
const correction = Array.isArray(input.correction) ? input.correction : DEFAULT_PATTERN_CONFIG.correction;
|
|
99
|
+
return {
|
|
100
|
+
positive: positive.map((v) => `${v}`.trim()).filter(Boolean),
|
|
101
|
+
correction: correction.map((v) => `${v}`.trim()).filter(Boolean),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function compilePatterns(patterns = []) {
|
|
106
|
+
const compiled = [];
|
|
107
|
+
for (const pattern of patterns) {
|
|
108
|
+
try {
|
|
109
|
+
compiled.push(new RegExp(pattern, 'iu'));
|
|
110
|
+
} catch {
|
|
111
|
+
// Ignore invalid regex patterns and continue.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return compiled;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function detectSignal(userMessage, patternConfig = {}) {
|
|
118
|
+
const text = `${userMessage || ''}`.trim();
|
|
119
|
+
if (!text) {
|
|
120
|
+
return { type: null, pattern: null };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const normalized = normalizePatternConfig(patternConfig);
|
|
124
|
+
const correctionPatterns = compilePatterns(normalized.correction);
|
|
125
|
+
for (const regex of correctionPatterns) {
|
|
126
|
+
if (regex.test(text)) {
|
|
127
|
+
return { type: 'correction', pattern: regex.source };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const positivePatterns = compilePatterns(normalized.positive);
|
|
132
|
+
for (const regex of positivePatterns) {
|
|
133
|
+
if (regex.test(text)) {
|
|
134
|
+
return { type: 'positive', pattern: regex.source };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { type: null, pattern: null };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createInteractionHash(userMessage, assistantResponse, signalType) {
|
|
142
|
+
return createHash('sha1')
|
|
143
|
+
.update(`${signalType || ''}\n${userMessage || ''}\n${assistantResponse || ''}`, 'utf8')
|
|
144
|
+
.digest('hex');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildLessonPayload(signal, userMessage, assistantResponse) {
|
|
148
|
+
const assistantSnippet = `${assistantResponse || ''}`.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
149
|
+
const userSnippet = `${userMessage || ''}`.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
150
|
+
|
|
151
|
+
if (signal.type === 'correction') {
|
|
152
|
+
return {
|
|
153
|
+
type: 'lesson',
|
|
154
|
+
title: 'User correction on response quality',
|
|
155
|
+
tags: ['user_correction', 'auto_capture', 'feedback'],
|
|
156
|
+
lesson_type: 'correction',
|
|
157
|
+
severity: 'moderate',
|
|
158
|
+
context: 'Automatic post-response interaction capture.',
|
|
159
|
+
outcome: 'Detected corrective user feedback and logged as actionable lesson.',
|
|
160
|
+
confidence: 95,
|
|
161
|
+
source: 'auto-capture',
|
|
162
|
+
content: [
|
|
163
|
+
'## Content',
|
|
164
|
+
'User indicated the previous response was incorrect or needed correction.',
|
|
165
|
+
'',
|
|
166
|
+
'## Context',
|
|
167
|
+
`User message: "${userSnippet || '(empty)'}"`,
|
|
168
|
+
`Assistant response excerpt: "${assistantSnippet || '(empty)'}"`,
|
|
169
|
+
'',
|
|
170
|
+
'## Takeaway',
|
|
171
|
+
'Re-check assumptions and ground future responses in the user correction.',
|
|
172
|
+
].join('\n'),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (signal.type === 'positive') {
|
|
177
|
+
return {
|
|
178
|
+
type: 'lesson',
|
|
179
|
+
title: 'Positive user feedback on response',
|
|
180
|
+
tags: ['positive_impact', 'auto_capture', 'feedback'],
|
|
181
|
+
lesson_type: 'success',
|
|
182
|
+
severity: 'minor',
|
|
183
|
+
context: 'Automatic post-response interaction capture.',
|
|
184
|
+
outcome: 'Detected positive reinforcement from user and logged successful approach.',
|
|
185
|
+
confidence: 88,
|
|
186
|
+
source: 'auto-capture',
|
|
187
|
+
content: [
|
|
188
|
+
'## Content',
|
|
189
|
+
'User gave positive feedback about the response.',
|
|
190
|
+
'',
|
|
191
|
+
'## Context',
|
|
192
|
+
`User message: "${userSnippet || '(empty)'}"`,
|
|
193
|
+
`Assistant response excerpt: "${assistantSnippet || '(empty)'}"`,
|
|
194
|
+
'',
|
|
195
|
+
'## Takeaway',
|
|
196
|
+
'Keep this response style and quality pattern for similar requests.',
|
|
197
|
+
].join('\n'),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function captureFromInteraction({
|
|
205
|
+
userMessage = '',
|
|
206
|
+
assistantResponse = '',
|
|
207
|
+
patternConfig = {},
|
|
208
|
+
dedupeWindowMs = 10 * 60 * 1000,
|
|
209
|
+
} = {}, cwd = process.cwd()) {
|
|
210
|
+
const state = getAutoCaptureState(cwd);
|
|
211
|
+
if (!state.enabled) {
|
|
212
|
+
return { captured: false, reason: 'disabled' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const signal = detectSignal(userMessage, patternConfig);
|
|
216
|
+
if (!signal.type) {
|
|
217
|
+
return { captured: false, reason: 'no_match' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const interactionHash = createInteractionHash(userMessage, assistantResponse, signal.type);
|
|
221
|
+
const nowMs = Date.now();
|
|
222
|
+
const previousCaptureAt = Number(new Date(state.last_capture_at || 0).getTime()) || 0;
|
|
223
|
+
const withinWindow = nowMs - previousCaptureAt >= 0 && nowMs - previousCaptureAt < dedupeWindowMs;
|
|
224
|
+
if (withinWindow && state.last_capture_hash === interactionHash) {
|
|
225
|
+
return {
|
|
226
|
+
captured: false,
|
|
227
|
+
reason: 'duplicate_recent',
|
|
228
|
+
signalType: signal.type,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const payload = buildLessonPayload(signal, userMessage, assistantResponse);
|
|
233
|
+
if (!payload) {
|
|
234
|
+
return { captured: false, reason: 'no_payload', signalType: signal.type };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const result = store.ingest(payload, cwd);
|
|
238
|
+
if (!result.success) {
|
|
239
|
+
return {
|
|
240
|
+
captured: false,
|
|
241
|
+
reason: 'ingest_failed',
|
|
242
|
+
signalType: signal.type,
|
|
243
|
+
errors: result.errors || [],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const nextState = {
|
|
248
|
+
...state,
|
|
249
|
+
last_capture_hash: interactionHash,
|
|
250
|
+
last_capture_at: new Date(nowMs).toISOString(),
|
|
251
|
+
updated_at: new Date(nowMs).toISOString(),
|
|
252
|
+
};
|
|
253
|
+
writeJson(getStatePath(cwd), nextState);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
captured: true,
|
|
257
|
+
signalType: signal.type,
|
|
258
|
+
matchedPattern: signal.pattern,
|
|
259
|
+
slug: result.slug,
|
|
260
|
+
tags: payload.tags,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
DEFAULT_PATTERN_CONFIG,
|
|
266
|
+
getAutoCaptureState,
|
|
267
|
+
setAutoCaptureEnabled,
|
|
268
|
+
detectSignal,
|
|
269
|
+
captureFromInteraction,
|
|
270
|
+
};
|