@equilateral_ai/mindmeld 3.4.0 → 3.5.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/hooks/session-end.js
CHANGED
|
@@ -5,16 +5,29 @@
|
|
|
5
5
|
* Records session completion and outcomes when a Claude Code session ends.
|
|
6
6
|
* Calls POST /api/sessions/end with session metadata.
|
|
7
7
|
*
|
|
8
|
+
* When reason === "clear" (user cleared context to continue), also harvests
|
|
9
|
+
* patterns from the transcript before context is lost. This covers the gap
|
|
10
|
+
* where PreCompact doesn't fire on /clear — only on compaction.
|
|
11
|
+
*
|
|
8
12
|
* Input (stdin JSON from Claude Code):
|
|
9
13
|
* { session_id, transcript_path, cwd, reason, hook_event_name }
|
|
10
14
|
*
|
|
11
|
-
* @equilateral_ai/mindmeld v3.
|
|
15
|
+
* @equilateral_ai/mindmeld v3.5.0
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
const path = require('path');
|
|
15
19
|
const fs = require('fs').promises;
|
|
16
20
|
const { execSync } = require('child_process');
|
|
17
21
|
|
|
22
|
+
// Import pattern harvesting from pre-compact hook
|
|
23
|
+
let harvestPatterns = null;
|
|
24
|
+
try {
|
|
25
|
+
const preCompact = require('./pre-compact');
|
|
26
|
+
harvestPatterns = preCompact.harvestPatterns;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// pre-compact module not available — pattern harvesting on clear will be skipped
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
/**
|
|
19
32
|
* Load auth token for API calls
|
|
20
33
|
* Priority: env var → project credentials.json → global ~/.mindmeld/auth.json
|
|
@@ -166,9 +179,94 @@ function readStdin() {
|
|
|
166
179
|
});
|
|
167
180
|
}
|
|
168
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Harvest patterns from transcript on clear events.
|
|
184
|
+
* Reads the JSONL transcript, extracts conversation text,
|
|
185
|
+
* and delegates to pre-compact's harvestPatterns.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} transcriptPath - Path to the JSONL transcript file
|
|
188
|
+
* @param {string} sessionId - Current session ID
|
|
189
|
+
* @returns {Promise<Object|null>} Harvest results or null
|
|
190
|
+
*/
|
|
191
|
+
async function harvestPatternsOnClear(transcriptPath, sessionId) {
|
|
192
|
+
try {
|
|
193
|
+
console.error('[MindMeld] Clear detected — harvesting patterns before context is lost');
|
|
194
|
+
|
|
195
|
+
// Read transcript JSONL (cap at 200KB to keep processing fast)
|
|
196
|
+
const stat = await fs.stat(transcriptPath);
|
|
197
|
+
let transcriptContent;
|
|
198
|
+
|
|
199
|
+
if (stat.size > 200 * 1024) {
|
|
200
|
+
// Read last 200KB — most recent context is most valuable
|
|
201
|
+
const fd = await fs.open(transcriptPath, 'r');
|
|
202
|
+
const buffer = Buffer.alloc(200 * 1024);
|
|
203
|
+
await fd.read(buffer, 0, buffer.length, stat.size - buffer.length);
|
|
204
|
+
await fd.close();
|
|
205
|
+
transcriptContent = buffer.toString('utf-8');
|
|
206
|
+
// Skip partial first line
|
|
207
|
+
const firstNewline = transcriptContent.indexOf('\n');
|
|
208
|
+
if (firstNewline > 0) {
|
|
209
|
+
transcriptContent = transcriptContent.substring(firstNewline + 1);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
transcriptContent = await fs.readFile(transcriptPath, 'utf-8');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Parse JSONL — extract assistant message text for pattern detection
|
|
216
|
+
const lines = transcriptContent.split('\n').filter(l => l.trim());
|
|
217
|
+
const textParts = [];
|
|
218
|
+
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
try {
|
|
221
|
+
const entry = JSON.parse(line);
|
|
222
|
+
// Claude Code transcript entries have varied formats
|
|
223
|
+
const content = entry.message?.content || entry.content;
|
|
224
|
+
if (!content) continue;
|
|
225
|
+
|
|
226
|
+
if (typeof content === 'string') {
|
|
227
|
+
textParts.push(content);
|
|
228
|
+
} else if (Array.isArray(content)) {
|
|
229
|
+
// Content blocks — extract text blocks
|
|
230
|
+
for (const block of content) {
|
|
231
|
+
if (block.type === 'text' && block.text) {
|
|
232
|
+
textParts.push(block.text);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
// Skip unparseable lines
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const transcriptText = textParts.join('\n\n');
|
|
242
|
+
|
|
243
|
+
if (transcriptText.length < 100) {
|
|
244
|
+
console.error('[MindMeld] Transcript too short for pattern detection, skipping');
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Delegate to pre-compact's full harvesting pipeline
|
|
249
|
+
const result = await harvestPatterns({
|
|
250
|
+
sessionId: sessionId,
|
|
251
|
+
userId: process.env.USER || 'unknown',
|
|
252
|
+
transcript: transcriptText
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
console.error(`[MindMeld] Clear-event harvest: ${result.patternsDetected || 0} patterns, ` +
|
|
256
|
+
`${result.violations || 0} violations, ${result.reinforced || 0} reinforced` +
|
|
257
|
+
(result.plansHarvested ? `, ${result.plansHarvested} plans` : ''));
|
|
258
|
+
|
|
259
|
+
return result;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error(`[MindMeld] Clear-event harvest failed (non-fatal): ${error.message}`);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
169
266
|
/**
|
|
170
267
|
* Main hook execution
|
|
171
268
|
* Records session end via API call (fire-and-forget)
|
|
269
|
+
* On clear events, also harvests patterns from the transcript
|
|
172
270
|
*/
|
|
173
271
|
async function recordSessionEnd() {
|
|
174
272
|
const startTime = Date.now();
|
|
@@ -217,7 +315,7 @@ async function recordSessionEnd() {
|
|
|
217
315
|
git_branch: gitBranch,
|
|
218
316
|
session_data: {
|
|
219
317
|
end_reason: reason,
|
|
220
|
-
hook_version: '3.
|
|
318
|
+
hook_version: '3.5.0'
|
|
221
319
|
}
|
|
222
320
|
};
|
|
223
321
|
|
|
@@ -238,7 +336,7 @@ async function recordSessionEnd() {
|
|
|
238
336
|
timeout: 3000
|
|
239
337
|
};
|
|
240
338
|
|
|
241
|
-
|
|
339
|
+
const metadataPromise = new Promise((resolve) => {
|
|
242
340
|
const req = http.request(options, (res) => {
|
|
243
341
|
let body = '';
|
|
244
342
|
res.on('data', (chunk) => { body += chunk; });
|
|
@@ -268,10 +366,21 @@ async function recordSessionEnd() {
|
|
|
268
366
|
req.end();
|
|
269
367
|
});
|
|
270
368
|
|
|
369
|
+
// Pattern harvesting on clear — PreCompact doesn't fire on /clear,
|
|
370
|
+
// so we harvest here before context is lost
|
|
371
|
+
let harvestResult = null;
|
|
372
|
+
const harvestPromise = (reason === 'clear' && transcriptPath && harvestPatterns)
|
|
373
|
+
? harvestPatternsOnClear(transcriptPath, sessionId)
|
|
374
|
+
: Promise.resolve(null);
|
|
375
|
+
|
|
376
|
+
// Run metadata recording and pattern harvesting in parallel
|
|
377
|
+
[, harvestResult] = await Promise.all([metadataPromise, harvestPromise]);
|
|
378
|
+
|
|
271
379
|
return {
|
|
272
380
|
sessionId,
|
|
273
381
|
reason,
|
|
274
382
|
duration,
|
|
383
|
+
harvest: harvestResult,
|
|
275
384
|
elapsed: Date.now() - startTime
|
|
276
385
|
};
|
|
277
386
|
|
package/package.json
CHANGED