@equilateral_ai/mindmeld 3.3.1 → 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/README.md +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-end.js +112 -3
- package/hooks/session-start.js +635 -41
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/mindmeldMcpCore.js +594 -0
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +15 -11
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
package/README.md
CHANGED
|
@@ -209,11 +209,6 @@ npm run test:hooks
|
|
|
209
209
|
npm run test:session-start
|
|
210
210
|
npm run test:pre-compact
|
|
211
211
|
|
|
212
|
-
# Test standards parsing (no database)
|
|
213
|
-
node scripts/demo-standards-parsing.js
|
|
214
|
-
|
|
215
|
-
# Test standards ingestion (requires database)
|
|
216
|
-
node scripts/test-standards-ingestion.js
|
|
217
212
|
```
|
|
218
213
|
|
|
219
214
|
### Prerequisites
|
|
@@ -246,11 +241,7 @@ Rapport integrates with `.equilateral-standards/` to provide context-aware stand
|
|
|
246
241
|
|
|
247
242
|
**Test it**:
|
|
248
243
|
```bash
|
|
249
|
-
|
|
250
|
-
node scripts/demo-standards-parsing.js
|
|
251
|
-
|
|
252
|
-
# Full ingestion test (requires database)
|
|
253
|
-
node scripts/test-standards-ingestion.js
|
|
244
|
+
node scripts/ingest-standards.js
|
|
254
245
|
```
|
|
255
246
|
|
|
256
247
|
## Value Proposition
|
package/hooks/pre-compact.js
CHANGED
|
@@ -66,10 +66,27 @@ async function loadAuthToken() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Load Cognito config
|
|
69
|
+
* Load Cognito config
|
|
70
|
+
* Priority: .mindmeld/config.json → .myworld.json → production defaults
|
|
70
71
|
* @returns {Promise<Object>} Cognito constructor options or empty object
|
|
71
72
|
*/
|
|
72
73
|
async function loadCognitoConfig() {
|
|
74
|
+
// 1. Check .mindmeld/config.json first (always points to prod)
|
|
75
|
+
try {
|
|
76
|
+
const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
77
|
+
const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
|
|
78
|
+
const config = JSON.parse(content);
|
|
79
|
+
if (config.auth?.cognitoDomain && config.auth?.cognitoClientId) {
|
|
80
|
+
return {
|
|
81
|
+
cognitoDomain: config.auth.cognitoDomain,
|
|
82
|
+
cognitoClientId: config.auth.cognitoClientId
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// No .mindmeld/config.json or no auth section
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Fallback to .myworld.json (may point to dev)
|
|
73
90
|
try {
|
|
74
91
|
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
75
92
|
const content = await fs.readFile(configPath, 'utf-8');
|
|
@@ -88,10 +105,24 @@ async function loadCognitoConfig() {
|
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
/**
|
|
91
|
-
* Load API configuration
|
|
108
|
+
* Load API configuration
|
|
109
|
+
* Priority: .mindmeld/config.json → .myworld.json → env var → production default
|
|
92
110
|
* @returns {Promise<{apiUrl: string}>}
|
|
93
111
|
*/
|
|
94
112
|
async function loadApiConfig() {
|
|
113
|
+
// 1. Check .mindmeld/config.json first (always points to prod)
|
|
114
|
+
try {
|
|
115
|
+
const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
116
|
+
const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
|
|
117
|
+
const config = JSON.parse(content);
|
|
118
|
+
if (config.apiUrl) {
|
|
119
|
+
return { apiUrl: config.apiUrl };
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// No .mindmeld/config.json or no apiUrl
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Fallback to .myworld.json (may point to dev)
|
|
95
126
|
try {
|
|
96
127
|
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
97
128
|
const content = await fs.readFile(configPath, 'utf-8');
|
|
@@ -134,9 +165,22 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
134
165
|
apiUrl: apiConfig.apiUrl
|
|
135
166
|
});
|
|
136
167
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
168
|
+
// Load project context so reinforcePattern/recordViolation have project_id
|
|
169
|
+
await mindmeld.detectProject();
|
|
170
|
+
|
|
171
|
+
// Load session context persisted by session-start hook
|
|
172
|
+
let sessionContext = null;
|
|
173
|
+
try {
|
|
174
|
+
const sessionContextPath = path.join(process.cwd(), '.mindmeld', 'current-session.json');
|
|
175
|
+
const content = await fs.readFile(sessionContextPath, 'utf-8');
|
|
176
|
+
sessionContext = JSON.parse(content);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
// Expected: file may not exist if session-start didn't run
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extract session metadata — prefer persisted session context
|
|
182
|
+
const sessionId = sessionContext?.sessionId || sessionTranscript.sessionId || generateSessionId();
|
|
183
|
+
const userId = sessionContext?.userEmail || sessionTranscript.userId || process.env.USER || 'unknown';
|
|
140
184
|
|
|
141
185
|
// 1. Detect patterns from session (LLM-powered or regex fallback)
|
|
142
186
|
let patterns = [];
|
|
@@ -216,27 +260,18 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
216
260
|
// 5. Check for promotion candidates
|
|
217
261
|
const candidates = await checkPromotionCandidates(mindmeld, validationResults.valid);
|
|
218
262
|
|
|
219
|
-
// 6.
|
|
220
|
-
let
|
|
263
|
+
// 6. Harvest plans from ~/.claude/plans/
|
|
264
|
+
let harvestedPlans = [];
|
|
221
265
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.error(`[MindMeld] Changes since last update: ${readmeStatus.changes}`);
|
|
228
|
-
console.error(`[MindMeld] Agent changes: ${readmeStatus.analysis.agentChanges}`);
|
|
229
|
-
console.error(`[MindMeld] Standards changes: ${readmeStatus.analysis.standardsChanges}`);
|
|
230
|
-
console.error(`[MindMeld] Significance: ${readmeStatus.analysis.significance}`);
|
|
231
|
-
|
|
232
|
-
if (readmeStatus.shouldTrigger) {
|
|
233
|
-
console.error(`[MindMeld] 🔧 Critical changes detected - README update recommended`);
|
|
234
|
-
console.error(`[MindMeld] Run: node -e "const L=require('./src/agents/specialists/LibrarianAgent');new L().generateProjectReadme({projectRoot:process.cwd()})"`);
|
|
266
|
+
harvestedPlans = await harvestPlans(sessionTranscript);
|
|
267
|
+
if (harvestedPlans.length > 0) {
|
|
268
|
+
console.error(`[MindMeld] Harvested ${harvestedPlans.length} plan(s) from session`);
|
|
269
|
+
for (const plan of harvestedPlans) {
|
|
270
|
+
console.error(` - ${plan.title} (${plan.filename})`);
|
|
235
271
|
}
|
|
236
272
|
}
|
|
237
273
|
} catch (error) {
|
|
238
|
-
|
|
239
|
-
console.error(`[MindMeld] README check skipped:`, error.message);
|
|
274
|
+
console.error('[MindMeld] Plan harvesting failed (non-fatal):', error.message);
|
|
240
275
|
}
|
|
241
276
|
|
|
242
277
|
// 7. Log results
|
|
@@ -246,8 +281,10 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
246
281
|
violations: validationResults.violations.length,
|
|
247
282
|
reinforced: validationResults.valid.length,
|
|
248
283
|
promotionCandidates: candidates.length,
|
|
249
|
-
|
|
250
|
-
|
|
284
|
+
plansHarvested: harvestedPlans.length,
|
|
285
|
+
plans: harvestedPlans,
|
|
286
|
+
readmeStale: null,
|
|
287
|
+
readmeUpdateRecommended: false,
|
|
251
288
|
llmUsed: useLLM && llmAnalysis?.success,
|
|
252
289
|
llmModel: llmAnalysis?.model || null,
|
|
253
290
|
llmSummary: llmAnalysis?.summary || null,
|
|
@@ -279,6 +316,133 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
279
316
|
}
|
|
280
317
|
}
|
|
281
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Harvest plans from ~/.claude/plans/
|
|
321
|
+
* Scans for plan files modified during this session (within last 4 hours)
|
|
322
|
+
* Extracts title, project references, and key decisions
|
|
323
|
+
*
|
|
324
|
+
* @param {Object} sessionTranscript - Session data (may contain session start time)
|
|
325
|
+
* @returns {Promise<Array>} Array of harvested plan objects
|
|
326
|
+
*/
|
|
327
|
+
async function harvestPlans(sessionTranscript) {
|
|
328
|
+
const os = require('os');
|
|
329
|
+
const plansDir = path.join(os.homedir(), '.claude', 'plans');
|
|
330
|
+
|
|
331
|
+
let files;
|
|
332
|
+
try {
|
|
333
|
+
files = await fs.readdir(plansDir);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (error.code === 'ENOENT') return [];
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
340
|
+
if (mdFiles.length === 0) return [];
|
|
341
|
+
|
|
342
|
+
// Plans modified in the last 4 hours are considered session-relevant
|
|
343
|
+
const cutoff = Date.now() - (4 * 60 * 60 * 1000);
|
|
344
|
+
const harvested = [];
|
|
345
|
+
|
|
346
|
+
for (const filename of mdFiles) {
|
|
347
|
+
const filePath = path.join(plansDir, filename);
|
|
348
|
+
const stat = await fs.stat(filePath);
|
|
349
|
+
|
|
350
|
+
if (stat.mtimeMs < cutoff) continue;
|
|
351
|
+
|
|
352
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
353
|
+
const plan = parsePlanFile(filename, content, stat);
|
|
354
|
+
if (plan) {
|
|
355
|
+
harvested.push(plan);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Sort by most recently modified first
|
|
360
|
+
harvested.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
361
|
+
|
|
362
|
+
return harvested;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Parse a plan file into structured data
|
|
367
|
+
* @param {string} filename - Plan filename
|
|
368
|
+
* @param {string} content - Raw markdown content
|
|
369
|
+
* @param {Object} stat - File stat object
|
|
370
|
+
* @returns {Object|null} Parsed plan or null if empty
|
|
371
|
+
*/
|
|
372
|
+
function parsePlanFile(filename, content, stat) {
|
|
373
|
+
if (!content || content.trim().length === 0) return null;
|
|
374
|
+
|
|
375
|
+
const lines = content.split('\n');
|
|
376
|
+
|
|
377
|
+
// Extract title from first heading
|
|
378
|
+
let title = filename.replace('.md', '');
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
const headingMatch = line.match(/^#\s+(?:Plan:\s*)?(.+)/);
|
|
381
|
+
if (headingMatch) {
|
|
382
|
+
title = headingMatch[1].trim();
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Extract sections
|
|
388
|
+
const sections = {};
|
|
389
|
+
let currentSection = null;
|
|
390
|
+
let currentContent = [];
|
|
391
|
+
|
|
392
|
+
for (const line of lines) {
|
|
393
|
+
const sectionMatch = line.match(/^##\s+(.+)/);
|
|
394
|
+
if (sectionMatch) {
|
|
395
|
+
if (currentSection) {
|
|
396
|
+
sections[currentSection] = currentContent.join('\n').trim();
|
|
397
|
+
}
|
|
398
|
+
currentSection = sectionMatch[1].trim().toLowerCase();
|
|
399
|
+
currentContent = [];
|
|
400
|
+
} else if (currentSection) {
|
|
401
|
+
currentContent.push(line);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (currentSection) {
|
|
405
|
+
sections[currentSection] = currentContent.join('\n').trim();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Extract file paths mentioned (src/..., hooks/..., etc.)
|
|
409
|
+
const fileRefs = [];
|
|
410
|
+
const filePattern = /(?:^|\s|`)((?:src|hooks|scripts|frontend|lib|test|tests)\/[\w/./-]+\.\w+)/g;
|
|
411
|
+
let match;
|
|
412
|
+
while ((match = filePattern.exec(content)) !== null) {
|
|
413
|
+
if (!fileRefs.includes(match[1])) {
|
|
414
|
+
fileRefs.push(match[1]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Detect which project this plan relates to
|
|
419
|
+
const projectHints = [];
|
|
420
|
+
const projectPatterns = [
|
|
421
|
+
/rapport/i, /mindmeld/i, /equilateral/i, /jarvis/i,
|
|
422
|
+
/honeydo/i, /timebridge/i, /powerspec/i
|
|
423
|
+
];
|
|
424
|
+
for (const pp of projectPatterns) {
|
|
425
|
+
if (pp.test(content)) {
|
|
426
|
+
projectHints.push(pp.source.replace(/\/i$/, ''));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
filename: filename,
|
|
432
|
+
planId: filename.replace('.md', ''),
|
|
433
|
+
title: title,
|
|
434
|
+
modifiedAt: stat.mtimeMs,
|
|
435
|
+
modifiedIso: stat.mtime.toISOString(),
|
|
436
|
+
sizeBytes: stat.size,
|
|
437
|
+
lineCount: lines.length,
|
|
438
|
+
sections: Object.keys(sections),
|
|
439
|
+
context: sections['context'] || null,
|
|
440
|
+
filesReferenced: fileRefs.slice(0, 20),
|
|
441
|
+
projectHints: projectHints,
|
|
442
|
+
content: content
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
282
446
|
/**
|
|
283
447
|
* Check if MindMeld is configured for this project
|
|
284
448
|
*/
|
|
@@ -405,6 +569,30 @@ async function generatePostCompactContext(summary, llmAnalysis) {
|
|
|
405
569
|
sections.push('');
|
|
406
570
|
}
|
|
407
571
|
|
|
572
|
+
// Include active plans from this session
|
|
573
|
+
if (summary.plans && summary.plans.length > 0) {
|
|
574
|
+
sections.push('## Active Plans');
|
|
575
|
+
for (const plan of summary.plans) {
|
|
576
|
+
sections.push(`### ${plan.title}`);
|
|
577
|
+
sections.push(`- **File**: \`~/.claude/plans/${plan.filename}\``);
|
|
578
|
+
sections.push(`- **Modified**: ${plan.modifiedIso}`);
|
|
579
|
+
if (plan.projectHints.length > 0) {
|
|
580
|
+
sections.push(`- **Projects**: ${plan.projectHints.join(', ')}`);
|
|
581
|
+
}
|
|
582
|
+
if (plan.filesReferenced.length > 0) {
|
|
583
|
+
sections.push(`- **Files**: ${plan.filesReferenced.slice(0, 10).join(', ')}`);
|
|
584
|
+
}
|
|
585
|
+
if (plan.context) {
|
|
586
|
+
// Include first 500 chars of context section
|
|
587
|
+
const contextPreview = plan.context.length > 500
|
|
588
|
+
? plan.context.substring(0, 500) + '...'
|
|
589
|
+
: plan.context;
|
|
590
|
+
sections.push(`- **Context**: ${contextPreview}`);
|
|
591
|
+
}
|
|
592
|
+
sections.push('');
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
408
596
|
// Load relevant standards from .equilateral-standards if available
|
|
409
597
|
const standardsPath = path.join(process.cwd(), '.equilateral-standards');
|
|
410
598
|
try {
|
|
@@ -492,4 +680,4 @@ if (require.main === module) {
|
|
|
492
680
|
});
|
|
493
681
|
}
|
|
494
682
|
|
|
495
|
-
module.exports = { harvestPatterns, parseSessionTranscript, generatePostCompactContext };
|
|
683
|
+
module.exports = { harvestPatterns, harvestPlans, parseSessionTranscript, generatePostCompactContext };
|
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
|
|