@equilateral_ai/mindmeld 3.5.2 → 4.0.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/hooks/session-end.js +25 -0
- package/hooks/session-start.js +363 -83
- package/hooks/session-watcher.js +585 -0
- package/package.json +19 -13
- package/scripts/init-project.js +9 -23
- package/src/client/dbShim.js +16 -0
- package/src/core/AuthManager.js +3 -2
- package/src/handlers/helpers/dbOperations.js +9 -46
- package/src/index.js +2 -217
- package/src/utils/piiMask.js +16 -0
- package/scripts/harvest.js +0 -601
- package/scripts/inject.js +0 -409
- package/scripts/mcp-bridge.js +0 -220
- package/scripts/repo-analyzer.js +0 -870
- package/src/collaboration/CollaborationPrompt.js +0 -460
- package/src/core/AlertEngine.js +0 -813
- package/src/core/AlertNotifier.js +0 -363
- package/src/core/CorrelationAnalyzer.js +0 -931
- package/src/core/CrossReferenceEngine.js +0 -624
- package/src/core/CurationEngine.js +0 -688
- package/src/core/DeprecationScheduler.js +0 -183
- package/src/core/LoadBearingDetector.js +0 -242
- package/src/core/NotificationService.js +0 -1032
- package/src/core/RapportOrchestrator.js +0 -632
- package/src/core/RelevanceDetector.js +0 -694
- package/src/core/StandardLifecycle.js +0 -244
- package/src/core/StandardsIngestion.js +0 -991
- package/src/core/TeamLoadBearingDetector.js +0 -431
- package/src/core/parsers/adrParser.js +0 -479
- package/src/core/parsers/cursorRulesParser.js +0 -564
- package/src/core/parsers/eslintParser.js +0 -439
- package/src/database/dbOperations.js +0 -105
- package/src/handlers/activity/activityGetMe.js +0 -98
- package/src/handlers/activity/activityGetTeam.js +0 -175
- package/src/handlers/admin/adminSetup.js +0 -216
- package/src/handlers/alerts/alertsAcknowledge.js +0 -92
- package/src/handlers/alerts/alertsGet.js +0 -250
- package/src/handlers/analytics/activitySummaryGet.js +0 -234
- package/src/handlers/analytics/coachingGet.js +0 -361
- package/src/handlers/analytics/convergenceGet.js +0 -236
- package/src/handlers/analytics/developerScoreGet.js +0 -137
- package/src/handlers/collaborators/collaboratorAdd.js +0 -200
- package/src/handlers/collaborators/collaboratorInvite.js +0 -219
- package/src/handlers/collaborators/collaboratorList.js +0 -82
- package/src/handlers/collaborators/collaboratorRemove.js +0 -128
- package/src/handlers/collaborators/inviteAccept.js +0 -122
- package/src/handlers/company/companyUsersDelete.js +0 -141
- package/src/handlers/company/companyUsersGet.js +0 -90
- package/src/handlers/company/companyUsersPost.js +0 -267
- package/src/handlers/company/companyUsersPut.js +0 -76
- package/src/handlers/context/contextGet.js +0 -57
- package/src/handlers/context/invariantsGet.js +0 -74
- package/src/handlers/context/loopsGet.js +0 -82
- package/src/handlers/context/notesCreate.js +0 -74
- package/src/handlers/context/purposeGet.js +0 -78
- package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
- package/src/handlers/correlations/correlationsGet.js +0 -93
- package/src/handlers/correlations/correlationsProjectGet.js +0 -153
- package/src/handlers/enterprise/controlTowerGet.js +0 -224
- package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
- package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
- package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
- package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
- package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
- package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
- package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
- package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
- package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
- package/src/handlers/github/githubConnectionStatus.js +0 -49
- package/src/handlers/github/githubDiscoverPatterns.js +0 -621
- package/src/handlers/github/githubOAuthCallback.js +0 -178
- package/src/handlers/github/githubOAuthStart.js +0 -59
- package/src/handlers/github/githubPatternsReview.js +0 -76
- package/src/handlers/github/githubReposList.js +0 -105
- package/src/handlers/health/healthGet.js +0 -55
- package/src/handlers/helpers/auditLogger.js +0 -201
- package/src/handlers/helpers/checkSuperAdmin.js +0 -84
- package/src/handlers/helpers/decisionFrames.js +0 -29
- package/src/handlers/helpers/errorHandler.js +0 -49
- package/src/handlers/helpers/index.js +0 -138
- package/src/handlers/helpers/lambdaWrapper.js +0 -60
- package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
- package/src/handlers/helpers/predictiveCache.js +0 -51
- package/src/handlers/helpers/projectAccess.js +0 -88
- package/src/handlers/helpers/responseUtil.js +0 -55
- package/src/handlers/helpers/subscriptionTiers.js +0 -1168
- package/src/handlers/mcp/mcpHandler.js +0 -569
- package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
- package/src/handlers/notifications/getPreferences.js +0 -84
- package/src/handlers/notifications/sendNotification.js +0 -170
- package/src/handlers/notifications/updatePreferences.js +0 -316
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
- package/src/handlers/patterns/patternUsagePost.js +0 -182
- package/src/handlers/patterns/patternViolationPost.js +0 -185
- package/src/handlers/projects/projectCreate.js +0 -248
- package/src/handlers/projects/projectDelete.js +0 -82
- package/src/handlers/projects/projectGet.js +0 -95
- package/src/handlers/projects/projectUpdate.js +0 -117
- package/src/handlers/reports/aiLeverage.js +0 -210
- package/src/handlers/reports/engineeringInvestment.js +0 -132
- package/src/handlers/reports/riskForecast.js +0 -206
- package/src/handlers/reports/standardsRoi.js +0 -254
- package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
- package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
- package/src/handlers/scheduled/generateAlerts.js +0 -135
- package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
- package/src/handlers/scheduled/refreshActivity.js +0 -21
- package/src/handlers/scheduled/scanCompliance.js +0 -334
- package/src/handlers/sessions/sessionEndPost.js +0 -180
- package/src/handlers/sessions/sessionStandardsPost.js +0 -171
- package/src/handlers/standards/catalogGet.js +0 -185
- package/src/handlers/standards/catalogSync.js +0 -120
- package/src/handlers/standards/discoveriesGet.js +0 -89
- package/src/handlers/standards/projectStandardsGet.js +0 -129
- package/src/handlers/standards/projectStandardsPut.js +0 -151
- package/src/handlers/standards/standardsAuditGet.js +0 -65
- package/src/handlers/standards/standardsParseUpload.js +0 -149
- package/src/handlers/standards/standardsRelevantPost.js +0 -405
- package/src/handlers/standards/standardsTransition.js +0 -161
- package/src/handlers/stripe/addonManagePost.js +0 -240
- package/src/handlers/stripe/billingPortalPost.js +0 -93
- package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
- package/src/handlers/stripe/seatsUpdatePost.js +0 -185
- package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
- package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
- package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
- package/src/handlers/stripe/webhookPost.js +0 -482
- package/src/handlers/user/apiTokenCreate.js +0 -71
- package/src/handlers/user/apiTokenList.js +0 -64
- package/src/handlers/user/userSplashAck.js +0 -91
- package/src/handlers/user/userSplashGet.js +0 -211
- package/src/handlers/users/cognitoPostConfirmation.js +0 -186
- package/src/handlers/users/cognitoPreSignUp.js +0 -114
- package/src/handlers/users/userEntitlementsGet.js +0 -89
- package/src/handlers/users/userGet.js +0 -118
- package/src/handlers/users/userProfilePut.js +0 -77
- package/src/handlers/webhooks/githubWebhook.js +0 -215
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MindMeld - Background Pattern Watcher
|
|
4
|
+
*
|
|
5
|
+
* Long-lived background process that monitors the JSONL session transcript
|
|
6
|
+
* and harvests patterns on resolution detection rather than a dumb timer.
|
|
7
|
+
*
|
|
8
|
+
* With 1M context windows, pre-compact may never fire. This watcher ensures
|
|
9
|
+
* patterns are harvested continuously throughout long sessions.
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle:
|
|
12
|
+
* 1. SessionStart spawns watcher (or watcher detects breadcrumb update)
|
|
13
|
+
* 2. Polls transcript every 10s, buffers new content
|
|
14
|
+
* 3. Detects resolution signals (test runs, completions, task done)
|
|
15
|
+
* 4. Extracts patterns after 60s cooldown post-resolution
|
|
16
|
+
* 5. Survives /clear via breadcrumb transcript switch
|
|
17
|
+
* 6. Stops on: parent death, stop file, or 2h idle
|
|
18
|
+
*
|
|
19
|
+
* Args:
|
|
20
|
+
* argv[2] - project cwd
|
|
21
|
+
* argv[3] - parent PID (terminal process to monitor)
|
|
22
|
+
*
|
|
23
|
+
* @equilateral_ai/mindmeld v3.5.3
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const fs = require('fs').promises;
|
|
28
|
+
const fsSync = require('fs');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
|
|
31
|
+
process.title = 'mindmeld-watcher';
|
|
32
|
+
|
|
33
|
+
// Import harvest pipeline from pre-compact
|
|
34
|
+
let harvestPatterns = null;
|
|
35
|
+
try {
|
|
36
|
+
const preCompact = require('./pre-compact');
|
|
37
|
+
harvestPatterns = preCompact.harvestPatterns;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`[MindMeld:Watcher] Cannot load pre-compact module: ${error.message}`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const PROJECT_CWD = process.argv[2];
|
|
44
|
+
const PARENT_PID = parseInt(process.argv[3], 10);
|
|
45
|
+
|
|
46
|
+
if (!PROJECT_CWD) {
|
|
47
|
+
console.error('[MindMeld:Watcher] No project cwd provided');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Timing constants ──────────────────────────────────────────────────────────
|
|
52
|
+
const POLL_MS = 10 * 1000; // 10s transcript poll
|
|
53
|
+
const COOLDOWN_MS = 60 * 1000; // 60s after resolution before extracting
|
|
54
|
+
const IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000; // 2h no transcript activity → exit
|
|
55
|
+
const PARENT_CHECK_MS = 10 * 1000; // 10s parent liveness check
|
|
56
|
+
const FALLBACK_HARVEST_MS = 5 * 60 * 1000; // 5 min safety net if no resolution detected
|
|
57
|
+
const MIN_BUFFER_CHARS = 500; // skip trivial buffers
|
|
58
|
+
const MAX_DELTA_BYTES = 200 * 1024; // 200KB cap per read
|
|
59
|
+
const STARTUP_RETRY_MS = 10 * 1000;
|
|
60
|
+
const STARTUP_MAX_RETRIES = 12;
|
|
61
|
+
|
|
62
|
+
// ── Resolution signal patterns ────────────────────────────────────────────────
|
|
63
|
+
// Bash tool calls that indicate work completion
|
|
64
|
+
const RESOLUTION_COMMANDS = [
|
|
65
|
+
/npm\s+test/,
|
|
66
|
+
/npm\s+run\s+test/,
|
|
67
|
+
/jest/,
|
|
68
|
+
/pytest/,
|
|
69
|
+
/ruff\s+check/,
|
|
70
|
+
/eslint/,
|
|
71
|
+
/tsc\s+--noEmit/,
|
|
72
|
+
/go\s+test/,
|
|
73
|
+
/cargo\s+test/,
|
|
74
|
+
/make\s+test/,
|
|
75
|
+
/sam\s+build/,
|
|
76
|
+
/sam\s+deploy/,
|
|
77
|
+
/npm\s+run\s+build/,
|
|
78
|
+
/npm\s+run\s+deploy/,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Completion phrases in assistant messages
|
|
82
|
+
const COMPLETION_PHRASES = [
|
|
83
|
+
/all\s+(?:tests?\s+)?pass/i,
|
|
84
|
+
/successfully\s+(?:deployed|built|completed|created|updated)/i,
|
|
85
|
+
/changes?\s+(?:look|are)\s+good/i,
|
|
86
|
+
/(?:done|finished|completed)[.!]\s*$/im,
|
|
87
|
+
/ready\s+(?:to\s+)?(?:commit|deploy|merge|push|review)/i,
|
|
88
|
+
/implementation\s+is\s+(?:complete|done|ready)/i,
|
|
89
|
+
/that\s+(?:should\s+)?fix/i,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// ── State ─────────────────────────────────────────────────────────────────────
|
|
93
|
+
let byteOffset = 0;
|
|
94
|
+
let jsonlPath = null;
|
|
95
|
+
let sessionId = null;
|
|
96
|
+
let buffer = ''; // accumulated unparsed transcript text
|
|
97
|
+
let pollHandle = null;
|
|
98
|
+
let parentCheckHandle = null;
|
|
99
|
+
let cooldownTimer = null;
|
|
100
|
+
let lastHarvestTime = Date.now();
|
|
101
|
+
let lastActivityTime = Date.now();
|
|
102
|
+
let resolutionDetected = false;
|
|
103
|
+
|
|
104
|
+
// ── Breadcrumb ────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Path to the breadcrumb file written by session-start.
|
|
108
|
+
* Contains session_id and transcript_path, updated on /clear.
|
|
109
|
+
*/
|
|
110
|
+
function breadcrumbPath() {
|
|
111
|
+
return path.join(PROJECT_CWD, '.mindmeld', 'current-session.json');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read the breadcrumb to get current session info and transcript path.
|
|
116
|
+
*/
|
|
117
|
+
async function readBreadcrumb() {
|
|
118
|
+
try {
|
|
119
|
+
const content = await fs.readFile(breadcrumbPath(), 'utf-8');
|
|
120
|
+
return JSON.parse(content);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Transcript discovery ──────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function encodeProjectDir(cwd) {
|
|
129
|
+
return cwd.replace(/\//g, '-');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the active JSONL session file.
|
|
134
|
+
* Priority: breadcrumb transcript_path → most recently modified .jsonl
|
|
135
|
+
*/
|
|
136
|
+
async function findSessionFile() {
|
|
137
|
+
// Check breadcrumb first
|
|
138
|
+
const crumb = await readBreadcrumb();
|
|
139
|
+
if (crumb?.transcriptPath) {
|
|
140
|
+
try {
|
|
141
|
+
await fs.access(crumb.transcriptPath);
|
|
142
|
+
sessionId = crumb.sessionId || sessionId;
|
|
143
|
+
return crumb.transcriptPath;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// Breadcrumb points to missing file, fall through
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (crumb?.sessionId) {
|
|
150
|
+
sessionId = crumb.sessionId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fall back to most recently modified JSONL
|
|
154
|
+
const projectDir = path.join(os.homedir(), '.claude', 'projects', encodeProjectDir(PROJECT_CWD));
|
|
155
|
+
let files;
|
|
156
|
+
try {
|
|
157
|
+
files = await fs.readdir(projectDir);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
|
|
163
|
+
if (jsonlFiles.length === 0) return null;
|
|
164
|
+
|
|
165
|
+
let newest = null;
|
|
166
|
+
let newestMtime = 0;
|
|
167
|
+
for (const file of jsonlFiles) {
|
|
168
|
+
const fullPath = path.join(projectDir, file);
|
|
169
|
+
try {
|
|
170
|
+
const stat = await fs.stat(fullPath);
|
|
171
|
+
if (stat.mtimeMs > newestMtime) {
|
|
172
|
+
newestMtime = stat.mtimeMs;
|
|
173
|
+
newest = fullPath;
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Skip
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return newest;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── JSONL parsing ─────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parse JSONL entries from raw text. Returns array of parsed entry objects
|
|
187
|
+
* for resolution detection, plus accumulated transcript text.
|
|
188
|
+
*/
|
|
189
|
+
function parseJsonlEntries(rawText) {
|
|
190
|
+
const lines = rawText.split('\n').filter(l => l.trim());
|
|
191
|
+
const entries = [];
|
|
192
|
+
const textParts = [];
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
try {
|
|
196
|
+
const entry = JSON.parse(line);
|
|
197
|
+
entries.push(entry);
|
|
198
|
+
|
|
199
|
+
const content = entry.message?.content || entry.content;
|
|
200
|
+
if (!content) continue;
|
|
201
|
+
|
|
202
|
+
if (typeof content === 'string') {
|
|
203
|
+
textParts.push(content);
|
|
204
|
+
} else if (Array.isArray(content)) {
|
|
205
|
+
for (const block of content) {
|
|
206
|
+
if (block.type === 'text' && block.text) {
|
|
207
|
+
textParts.push(block.text);
|
|
208
|
+
}
|
|
209
|
+
// Also capture tool_use for resolution detection
|
|
210
|
+
if (block.type === 'tool_use' && block.input) {
|
|
211
|
+
const input = typeof block.input === 'string' ? block.input : JSON.stringify(block.input);
|
|
212
|
+
textParts.push(input);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Skip partial lines
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { entries, transcript: textParts.join('\n\n') };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Delta reading ─────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
async function readDelta() {
|
|
227
|
+
if (!jsonlPath) return null;
|
|
228
|
+
|
|
229
|
+
let stat;
|
|
230
|
+
try {
|
|
231
|
+
stat = await fs.stat(jsonlPath);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (stat.size <= byteOffset) return null;
|
|
237
|
+
lastActivityTime = Date.now();
|
|
238
|
+
|
|
239
|
+
const readSize = Math.min(stat.size - byteOffset, MAX_DELTA_BYTES);
|
|
240
|
+
const buf = Buffer.alloc(readSize);
|
|
241
|
+
|
|
242
|
+
const fh = await fs.open(jsonlPath, 'r');
|
|
243
|
+
try {
|
|
244
|
+
await fh.read(buf, 0, readSize, byteOffset);
|
|
245
|
+
} finally {
|
|
246
|
+
await fh.close();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const raw = buf.toString('utf-8');
|
|
250
|
+
|
|
251
|
+
// Skip partial first line if mid-file
|
|
252
|
+
let usable = raw;
|
|
253
|
+
if (byteOffset > 0) {
|
|
254
|
+
const firstNewline = raw.indexOf('\n');
|
|
255
|
+
if (firstNewline > 0) {
|
|
256
|
+
usable = raw.substring(firstNewline + 1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Advance to end of last complete line
|
|
261
|
+
const lastNewline = raw.lastIndexOf('\n');
|
|
262
|
+
if (lastNewline >= 0) {
|
|
263
|
+
byteOffset += lastNewline + 1;
|
|
264
|
+
} else {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return usable;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Resolution detection ──────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Scan entries for signals that the agent just finished a unit of work.
|
|
275
|
+
*/
|
|
276
|
+
function detectResolution(entries, transcriptText) {
|
|
277
|
+
// Check for resolution command patterns in tool calls
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
const content = entry.message?.content || entry.content;
|
|
280
|
+
if (!Array.isArray(content)) continue;
|
|
281
|
+
|
|
282
|
+
for (const block of content) {
|
|
283
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
284
|
+
const cmd = block.input?.command || '';
|
|
285
|
+
for (const pattern of RESOLUTION_COMMANDS) {
|
|
286
|
+
if (pattern.test(cmd)) {
|
|
287
|
+
return { type: 'command', signal: cmd.substring(0, 80) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// TaskUpdate with status "completed"
|
|
292
|
+
if (block.type === 'tool_use' && block.name === 'TaskUpdate') {
|
|
293
|
+
const status = block.input?.status;
|
|
294
|
+
if (status === 'completed') {
|
|
295
|
+
return { type: 'task_complete', signal: block.input?.id || 'unknown' };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check for completion phrases in recent text
|
|
302
|
+
for (const phrase of COMPLETION_PHRASES) {
|
|
303
|
+
if (phrase.test(transcriptText)) {
|
|
304
|
+
return { type: 'phrase', signal: transcriptText.match(phrase)?.[0] || '' };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Harvest ───────────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
async function extract() {
|
|
314
|
+
if (!buffer || buffer.length < MIN_BUFFER_CHARS) {
|
|
315
|
+
buffer = '';
|
|
316
|
+
resolutionDetected = false;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const harvestText = buffer.length > MAX_DELTA_BYTES
|
|
321
|
+
? buffer.substring(buffer.length - MAX_DELTA_BYTES)
|
|
322
|
+
: buffer;
|
|
323
|
+
|
|
324
|
+
console.error(`[MindMeld:Watcher] Extracting patterns (${harvestText.length} chars)`);
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const result = await harvestPatterns({
|
|
328
|
+
sessionId: sessionId || 'unknown',
|
|
329
|
+
transcript: harvestText,
|
|
330
|
+
source: 'watcher'
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (result && !result.skipped) {
|
|
334
|
+
console.error(`[MindMeld:Watcher] Harvest: ${result.patternsDetected || 0} patterns, ${result.violations || 0} violations, ${result.reinforced || 0} reinforced`);
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(`[MindMeld:Watcher] Harvest error (non-fatal): ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
buffer = '';
|
|
341
|
+
resolutionDetected = false;
|
|
342
|
+
lastHarvestTime = Date.now();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Transcript switch (clear detection) ───────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Check if the breadcrumb has been updated with a new transcript path.
|
|
349
|
+
* This happens when session-start fires again after /clear.
|
|
350
|
+
*/
|
|
351
|
+
async function checkTranscriptSwitch() {
|
|
352
|
+
const crumb = await readBreadcrumb();
|
|
353
|
+
if (!crumb?.transcriptPath) return false;
|
|
354
|
+
|
|
355
|
+
// No current path yet — initial setup, not a switch
|
|
356
|
+
if (!jsonlPath) return false;
|
|
357
|
+
|
|
358
|
+
// Normalize for comparison
|
|
359
|
+
const currentBasename = path.basename(jsonlPath);
|
|
360
|
+
const crumbBasename = path.basename(crumb.transcriptPath);
|
|
361
|
+
|
|
362
|
+
if (currentBasename !== crumbBasename) {
|
|
363
|
+
console.error(`[MindMeld:Watcher] Transcript switch detected: ${currentBasename} → ${crumbBasename}`);
|
|
364
|
+
|
|
365
|
+
// Flush remaining buffer from old transcript
|
|
366
|
+
if (buffer.length >= MIN_BUFFER_CHARS) {
|
|
367
|
+
console.error('[MindMeld:Watcher] Flushing buffer from previous transcript');
|
|
368
|
+
await extract();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Reset state for new transcript
|
|
372
|
+
try {
|
|
373
|
+
await fs.access(crumb.transcriptPath);
|
|
374
|
+
jsonlPath = crumb.transcriptPath;
|
|
375
|
+
sessionId = crumb.sessionId || sessionId;
|
|
376
|
+
byteOffset = 0; // Read from start of new transcript
|
|
377
|
+
buffer = '';
|
|
378
|
+
resolutionDetected = false;
|
|
379
|
+
|
|
380
|
+
// Set offset to current size if we don't want pre-existing content
|
|
381
|
+
const stat = await fs.stat(jsonlPath);
|
|
382
|
+
byteOffset = stat.size;
|
|
383
|
+
|
|
384
|
+
console.error(`[MindMeld:Watcher] Now watching: ${path.basename(jsonlPath)}`);
|
|
385
|
+
return true;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.error(`[MindMeld:Watcher] New transcript not accessible: ${err.message}`);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Parent process liveness ───────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
function isParentAlive() {
|
|
398
|
+
if (!PARENT_PID || isNaN(PARENT_PID)) return true; // Can't check, assume alive
|
|
399
|
+
try {
|
|
400
|
+
process.kill(PARENT_PID, 0); // Signal 0 = existence check
|
|
401
|
+
return true;
|
|
402
|
+
} catch (err) {
|
|
403
|
+
return false; // ESRCH = process gone
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Stop file detection ───────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
function stopFilePath() {
|
|
410
|
+
return path.join(PROJECT_CWD, '.mindmeld', 'watcher.stop');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function checkStopFile() {
|
|
414
|
+
try {
|
|
415
|
+
await fs.access(stopFilePath());
|
|
416
|
+
await fs.unlink(stopFilePath()).catch(() => {});
|
|
417
|
+
return true;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── PID file ──────────────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
async function writePidFile() {
|
|
426
|
+
try {
|
|
427
|
+
const pidPath = path.join(PROJECT_CWD, '.mindmeld', 'watcher.pid');
|
|
428
|
+
await fs.writeFile(pidPath, String(process.pid));
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.error(`[MindMeld:Watcher] Could not write PID file: ${err.message}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Poll tick ─────────────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
async function poll() {
|
|
437
|
+
try {
|
|
438
|
+
// Check for transcript switch (breadcrumb updated by session-start after /clear)
|
|
439
|
+
await checkTranscriptSwitch();
|
|
440
|
+
|
|
441
|
+
// Read new lines from transcript
|
|
442
|
+
const delta = await readDelta();
|
|
443
|
+
if (delta) {
|
|
444
|
+
const { entries, transcript } = parseJsonlEntries(delta);
|
|
445
|
+
if (transcript) {
|
|
446
|
+
buffer += transcript + '\n\n';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check for resolution signals in new entries
|
|
450
|
+
if (!resolutionDetected) {
|
|
451
|
+
const resolution = detectResolution(entries, transcript);
|
|
452
|
+
if (resolution) {
|
|
453
|
+
resolutionDetected = true;
|
|
454
|
+
console.error(`[MindMeld:Watcher] Resolution detected (${resolution.type}): ${resolution.signal}`);
|
|
455
|
+
|
|
456
|
+
// Start cooldown — extract after 60s
|
|
457
|
+
if (cooldownTimer) clearTimeout(cooldownTimer);
|
|
458
|
+
cooldownTimer = setTimeout(() => {
|
|
459
|
+
cooldownTimer = null;
|
|
460
|
+
extract();
|
|
461
|
+
}, COOLDOWN_MS);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Fallback: if no resolution detected but buffer is accumulating, harvest on timer
|
|
467
|
+
if (!resolutionDetected && !cooldownTimer && buffer.length >= MIN_BUFFER_CHARS) {
|
|
468
|
+
const sinceLastHarvest = Date.now() - lastHarvestTime;
|
|
469
|
+
if (sinceLastHarvest >= FALLBACK_HARVEST_MS) {
|
|
470
|
+
console.error('[MindMeld:Watcher] Fallback harvest (no resolution signal)');
|
|
471
|
+
await extract();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch (err) {
|
|
475
|
+
console.error(`[MindMeld:Watcher] Poll error (non-fatal): ${err.message}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Parent liveness + idle + stop file checks.
|
|
481
|
+
*/
|
|
482
|
+
async function livenessCheck() {
|
|
483
|
+
// Parent process died (terminal closed)
|
|
484
|
+
if (!isParentAlive()) {
|
|
485
|
+
console.error('[MindMeld:Watcher] Parent process gone, extracting and exiting');
|
|
486
|
+
await extract();
|
|
487
|
+
cleanup();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Stop file written by session-end
|
|
492
|
+
if (await checkStopFile()) {
|
|
493
|
+
console.error('[MindMeld:Watcher] Stop file detected, extracting and exiting');
|
|
494
|
+
await extract();
|
|
495
|
+
cleanup();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Idle timeout
|
|
500
|
+
if (Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) {
|
|
501
|
+
console.error('[MindMeld:Watcher] Idle for 2h, extracting and exiting');
|
|
502
|
+
await extract();
|
|
503
|
+
cleanup();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
function cleanup() {
|
|
511
|
+
if (pollHandle) {
|
|
512
|
+
clearInterval(pollHandle);
|
|
513
|
+
pollHandle = null;
|
|
514
|
+
}
|
|
515
|
+
if (parentCheckHandle) {
|
|
516
|
+
clearInterval(parentCheckHandle);
|
|
517
|
+
parentCheckHandle = null;
|
|
518
|
+
}
|
|
519
|
+
if (cooldownTimer) {
|
|
520
|
+
clearTimeout(cooldownTimer);
|
|
521
|
+
cooldownTimer = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Remove PID file
|
|
525
|
+
try {
|
|
526
|
+
fsSync.unlinkSync(path.join(PROJECT_CWD, '.mindmeld', 'watcher.pid'));
|
|
527
|
+
} catch (err) {
|
|
528
|
+
// Already gone
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
async function main() {
|
|
537
|
+
console.error(`[MindMeld:Watcher] Starting for ${path.basename(PROJECT_CWD)} (parent PID: ${PARENT_PID || 'unknown'})`);
|
|
538
|
+
|
|
539
|
+
await writePidFile();
|
|
540
|
+
|
|
541
|
+
// Find the JSONL file, retrying if it doesn't exist yet
|
|
542
|
+
for (let attempt = 0; attempt < STARTUP_MAX_RETRIES; attempt++) {
|
|
543
|
+
jsonlPath = await findSessionFile();
|
|
544
|
+
if (jsonlPath) break;
|
|
545
|
+
await new Promise(r => setTimeout(r, STARTUP_RETRY_MS));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!jsonlPath) {
|
|
549
|
+
console.error('[MindMeld:Watcher] Could not find session JSONL file after retries, exiting');
|
|
550
|
+
cleanup();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Read session ID from breadcrumb if not set
|
|
555
|
+
if (!sessionId) {
|
|
556
|
+
const crumb = await readBreadcrumb();
|
|
557
|
+
sessionId = crumb?.sessionId || 'unknown';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.error(`[MindMeld:Watcher] Watching: ${path.basename(jsonlPath)} (session: ${sessionId})`);
|
|
561
|
+
|
|
562
|
+
// Start offset at current file size — only capture new activity
|
|
563
|
+
try {
|
|
564
|
+
const stat = await fs.stat(jsonlPath);
|
|
565
|
+
byteOffset = stat.size;
|
|
566
|
+
} catch (err) {
|
|
567
|
+
byteOffset = 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Start polling
|
|
571
|
+
pollHandle = setInterval(poll, POLL_MS);
|
|
572
|
+
parentCheckHandle = setInterval(livenessCheck, PARENT_CHECK_MS);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
process.on('SIGTERM', async () => {
|
|
576
|
+
console.error('[MindMeld:Watcher] SIGTERM received, extracting and exiting');
|
|
577
|
+
await extract();
|
|
578
|
+
cleanup();
|
|
579
|
+
});
|
|
580
|
+
process.on('SIGINT', cleanup);
|
|
581
|
+
|
|
582
|
+
main().catch(err => {
|
|
583
|
+
console.error(`[MindMeld:Watcher] Fatal: ${err.message}`);
|
|
584
|
+
cleanup();
|
|
585
|
+
});
|
package/package.json
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equilateral_ai/mindmeld",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Intelligent standards injection for AI coding sessions - context-aware, self-documenting, scales to large codebases",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mindmeld": "
|
|
7
|
+
"mindmeld": "scripts/init-project.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"src/",
|
|
10
|
+
"src/index.js",
|
|
11
|
+
"src/core/AuthManager.js",
|
|
12
|
+
"src/core/PatternValidator.js",
|
|
13
|
+
"src/core/LLMPatternDetector.js",
|
|
14
|
+
"src/client/dbShim.js",
|
|
15
|
+
"src/handlers/helpers/dbOperations.js",
|
|
16
|
+
"src/utils/piiMask.js",
|
|
11
17
|
"hooks/",
|
|
12
18
|
"scripts/init-project.js",
|
|
13
19
|
"scripts/auth-login.js",
|
|
14
|
-
"scripts/inject.js",
|
|
15
|
-
"scripts/harvest.js",
|
|
16
|
-
"scripts/repo-analyzer.js",
|
|
17
|
-
"scripts/mcp-bridge.js",
|
|
18
20
|
"README.md"
|
|
19
21
|
],
|
|
20
22
|
"publishConfig": {
|
|
@@ -43,7 +45,9 @@
|
|
|
43
45
|
"test:benchmark": "node scripts/test-claude-hooks.js --benchmark",
|
|
44
46
|
"test:smoke": "jest --selectProjects smoke --testTimeout=15000",
|
|
45
47
|
"test:e2e": "cd e2e && npx playwright test",
|
|
46
|
-
"test:e2e:admin": "cd frontend/admin && npx playwright test"
|
|
48
|
+
"test:e2e:admin": "cd frontend/admin && npx playwright test",
|
|
49
|
+
"prepack": "mkdir -p src/handlers/helpers && cp src/client/dbShim.js src/handlers/helpers/dbOperations.js",
|
|
50
|
+
"postpack": "git checkout -- src/handlers/helpers/dbOperations.js"
|
|
47
51
|
},
|
|
48
52
|
"claudeCode": {
|
|
49
53
|
"hooks": {
|
|
@@ -79,15 +83,17 @@
|
|
|
79
83
|
},
|
|
80
84
|
"homepage": "https://mindmeld.dev",
|
|
81
85
|
"dependencies": {
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
+
"js-yaml": "^4.1.1"
|
|
87
|
+
},
|
|
88
|
+
"optionalDependencies": {
|
|
89
|
+
"@aws-sdk/client-bedrock-runtime": "^3.460.0"
|
|
86
90
|
},
|
|
87
91
|
"devDependencies": {
|
|
88
92
|
"@aws-sdk/client-s3": "^3.460.0",
|
|
89
93
|
"@aws-sdk/client-ses": "^3.985.0",
|
|
90
|
-
"
|
|
94
|
+
"dotenv": "^17.4.2",
|
|
95
|
+
"jest": "^29.7.0",
|
|
96
|
+
"pg": "^8.18.0"
|
|
91
97
|
},
|
|
92
98
|
"engines": {
|
|
93
99
|
"node": ">=18.0.0"
|
package/scripts/init-project.js
CHANGED
|
@@ -702,14 +702,9 @@ if (args.command === 'init') {
|
|
|
702
702
|
process.exit(1);
|
|
703
703
|
});
|
|
704
704
|
} else if (args.command === 'inject') {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
.then(() => process.exit(0))
|
|
709
|
-
.catch(error => {
|
|
710
|
-
console.error('\n❌ Error:', error.message);
|
|
711
|
-
process.exit(1);
|
|
712
|
-
});
|
|
705
|
+
console.error('The inject subcommand has been removed in v4.0.0.');
|
|
706
|
+
console.error('Standards injection is now handled automatically by the session-start hook via the MindMeld API.');
|
|
707
|
+
process.exit(1);
|
|
713
708
|
} else if (args.command === 'harvest') {
|
|
714
709
|
const { harvest, showHarvestHelp } = require('./harvest');
|
|
715
710
|
if (args.help) { showHarvestHelp(); process.exit(0); }
|
|
@@ -741,8 +736,9 @@ if (args.command === 'init') {
|
|
|
741
736
|
process.exit(1);
|
|
742
737
|
});
|
|
743
738
|
} else if (args.command === 'standards') {
|
|
744
|
-
|
|
745
|
-
|
|
739
|
+
console.error('The standards subcommand has been removed in v4.0.0.');
|
|
740
|
+
console.error('Configure standards at https://app.mindmeld.dev');
|
|
741
|
+
process.exit(1);
|
|
746
742
|
} else if (args.command === 'open') {
|
|
747
743
|
openWebApp(args.projectPath || 'dashboard')
|
|
748
744
|
.then(() => process.exit(0))
|
|
@@ -751,19 +747,9 @@ if (args.command === 'init') {
|
|
|
751
747
|
process.exit(1);
|
|
752
748
|
});
|
|
753
749
|
} else if (args.command === 'mcp') {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (!token) {
|
|
758
|
-
console.error('\nMindMeld MCP bridge requires an API token.\n');
|
|
759
|
-
console.error('Set one of:');
|
|
760
|
-
console.error(' 1. MINDMELD_TOKEN environment variable');
|
|
761
|
-
console.error(' 2. --token CLI argument');
|
|
762
|
-
console.error(' 3. Save to ~/.mindmeld/api-token\n');
|
|
763
|
-
console.error('Create a token at: https://app.mindmeld.dev/api-tokens\n');
|
|
764
|
-
process.exit(1);
|
|
765
|
-
}
|
|
766
|
-
startBridge(token);
|
|
750
|
+
console.error('The mcp subcommand has been removed in v4.0.0.');
|
|
751
|
+
console.error('Use the MindMeld MCP server directly via Claude Code settings.');
|
|
752
|
+
process.exit(1);
|
|
767
753
|
} else {
|
|
768
754
|
console.error(`Unknown command: ${args.command}`);
|
|
769
755
|
console.error('Run "mindmeld --help" for usage.');
|