@equilateral_ai/mindmeld 3.5.0 → 3.5.2
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/pre-compact.js +269 -21
- package/hooks/session-start.js +139 -34
- package/package.json +2 -1
- package/scripts/auth-login.js +45 -8
- package/src/core/StandardsIngestion.js +3 -1
- package/src/handlers/collaborators/collaboratorList.js +4 -10
- package/src/handlers/correlations/correlationsProjectGet.js +4 -13
- package/src/handlers/github/githubDiscoverPatterns.js +4 -8
- package/src/handlers/github/githubPatternsReview.js +4 -8
- package/src/handlers/helpers/decisionFrames.js +29 -0
- package/src/handlers/helpers/index.js +14 -0
- package/src/handlers/helpers/mindmeldMcpCore.js +566 -57
- package/src/handlers/helpers/predictiveCache.js +51 -0
- package/src/handlers/helpers/projectAccess.js +88 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +113 -14
- package/src/handlers/standards/discoveriesGet.js +4 -8
- package/src/handlers/standards/projectStandardsGet.js +5 -11
- package/src/handlers/standards/projectStandardsPut.js +34 -14
- package/src/handlers/standards/standardsParseUpload.js +4 -8
- package/src/handlers/standards/standardsRelevantPost.js +126 -29
package/hooks/pre-compact.js
CHANGED
|
@@ -18,6 +18,56 @@ const path = require('path');
|
|
|
18
18
|
const fs = require('fs').promises;
|
|
19
19
|
const crypto = require('crypto');
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Scrub sensitive data from text before sending to MindMeld API.
|
|
23
|
+
* Replaces AWS keys, API tokens, passwords, connection strings,
|
|
24
|
+
* private keys, and generic secrets with [REDACTED].
|
|
25
|
+
* @param {string} text - Text that may contain secrets
|
|
26
|
+
* @returns {string} Text with secrets replaced by [REDACTED]
|
|
27
|
+
*/
|
|
28
|
+
function scrubSecrets(text) {
|
|
29
|
+
if (typeof text !== 'string') return text;
|
|
30
|
+
|
|
31
|
+
const patterns = [
|
|
32
|
+
// AWS Access Keys
|
|
33
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
34
|
+
// AWS Secret Keys
|
|
35
|
+
/(?:aws_secret_access_key|secret_key|secretAccessKey)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/gi,
|
|
36
|
+
// Generic API tokens
|
|
37
|
+
/(?:api[_-]?key|api[_-]?token|auth[_-]?token|bearer)\s*[=:]\s*['"]?[A-Za-z0-9_\-\.]{20,}['"]?/gi,
|
|
38
|
+
// Passwords
|
|
39
|
+
/(?:password|passwd|pwd)\s*[=:]\s*['"]?[^\s'"]{4,}['"]?/gi,
|
|
40
|
+
// Connection strings
|
|
41
|
+
/(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+/gi,
|
|
42
|
+
// Private keys
|
|
43
|
+
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
|
|
44
|
+
// Generic secrets
|
|
45
|
+
/(?:secret|token|credential)\s*[=:]\s*['"]?[A-Za-z0-9_\-\.\/+=]{16,}['"]?/gi,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
let scrubbed = text;
|
|
49
|
+
for (const pattern of patterns) {
|
|
50
|
+
scrubbed = scrubbed.replace(pattern, '[REDACTED]');
|
|
51
|
+
}
|
|
52
|
+
return scrubbed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Deep-scrub secrets from an object by traversing all string values.
|
|
57
|
+
* @param {*} obj - Object, array, or primitive to scrub
|
|
58
|
+
* @returns {*} Scrubbed copy (original is not mutated)
|
|
59
|
+
*/
|
|
60
|
+
function scrubSecretsDeep(obj) {
|
|
61
|
+
if (typeof obj === 'string') return scrubSecrets(obj);
|
|
62
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') return obj;
|
|
63
|
+
if (Array.isArray(obj)) return obj.map(scrubSecretsDeep);
|
|
64
|
+
const result = {};
|
|
65
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
66
|
+
result[key] = scrubSecretsDeep(value);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
21
71
|
// LLM Pattern Detection (optional - requires ANTHROPIC_API_KEY)
|
|
22
72
|
let LLMPatternDetector = null;
|
|
23
73
|
try {
|
|
@@ -200,7 +250,7 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
200
250
|
? sessionTranscript
|
|
201
251
|
: sessionTranscript.transcript || JSON.stringify(sessionTranscript);
|
|
202
252
|
|
|
203
|
-
llmAnalysis = await detector.analyzeSessionTranscript(transcriptText, {
|
|
253
|
+
llmAnalysis = await detector.analyzeSessionTranscript(scrubSecrets(transcriptText), {
|
|
204
254
|
projectName: path.basename(process.cwd()),
|
|
205
255
|
filesChanged: sessionTranscript.filesChanged || []
|
|
206
256
|
});
|
|
@@ -238,23 +288,23 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
238
288
|
// 2. Validate against standards
|
|
239
289
|
const validationResults = await validatePatterns(mindmeld, patterns);
|
|
240
290
|
|
|
241
|
-
// 3. Record violations
|
|
291
|
+
// 3. Record violations (scrub secrets before sending to API)
|
|
242
292
|
for (const result of validationResults.violations) {
|
|
243
|
-
await mindmeld.recordViolation({
|
|
293
|
+
await mindmeld.recordViolation(scrubSecretsDeep({
|
|
244
294
|
pattern: result.pattern,
|
|
245
295
|
violations: result.violations,
|
|
246
296
|
sessionId: sessionId,
|
|
247
297
|
userId: userId
|
|
248
|
-
});
|
|
298
|
+
}));
|
|
249
299
|
}
|
|
250
300
|
|
|
251
|
-
// 4. Reinforce valid patterns
|
|
301
|
+
// 4. Reinforce valid patterns (scrub secrets before sending to API)
|
|
252
302
|
for (const result of validationResults.valid) {
|
|
253
|
-
await mindmeld.reinforcePattern({
|
|
303
|
+
await mindmeld.reinforcePattern(scrubSecretsDeep({
|
|
254
304
|
pattern: result.pattern,
|
|
255
305
|
sessionId: sessionId,
|
|
256
306
|
userId: userId
|
|
257
|
-
});
|
|
307
|
+
}));
|
|
258
308
|
}
|
|
259
309
|
|
|
260
310
|
// 5. Check for promotion candidates
|
|
@@ -274,7 +324,26 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
274
324
|
console.error('[MindMeld] Plan harvesting failed (non-fatal):', error.message);
|
|
275
325
|
}
|
|
276
326
|
|
|
277
|
-
// 7.
|
|
327
|
+
// 7. Detect and send corrections from conversation text
|
|
328
|
+
let correctionsDetected = 0;
|
|
329
|
+
try {
|
|
330
|
+
const transcriptText = typeof sessionTranscript === 'string'
|
|
331
|
+
? sessionTranscript
|
|
332
|
+
: sessionTranscript.transcript || JSON.stringify(sessionTranscript);
|
|
333
|
+
|
|
334
|
+
const corrections = detectCorrections(transcriptText);
|
|
335
|
+
correctionsDetected = corrections.length;
|
|
336
|
+
|
|
337
|
+
if (corrections.length > 0) {
|
|
338
|
+
console.error(`[MindMeld] Detected ${corrections.length} correction(s) in session`);
|
|
339
|
+
await sendCorrections(corrections, authToken, apiConfig);
|
|
340
|
+
console.error(`[MindMeld] Sent ${corrections.length} correction(s) to API`);
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error('[MindMeld] Correction harvesting failed (non-fatal):', error.message);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 8. Log results
|
|
278
347
|
const elapsed = Date.now() - startTime;
|
|
279
348
|
const summary = {
|
|
280
349
|
patternsDetected: patterns.length,
|
|
@@ -282,6 +351,7 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
282
351
|
reinforced: validationResults.valid.length,
|
|
283
352
|
promotionCandidates: candidates.length,
|
|
284
353
|
plansHarvested: harvestedPlans.length,
|
|
354
|
+
correctionsDetected: correctionsDetected,
|
|
285
355
|
plans: harvestedPlans,
|
|
286
356
|
readmeStale: null,
|
|
287
357
|
readmeUpdateRecommended: false,
|
|
@@ -436,10 +506,10 @@ function parsePlanFile(filename, content, stat) {
|
|
|
436
506
|
sizeBytes: stat.size,
|
|
437
507
|
lineCount: lines.length,
|
|
438
508
|
sections: Object.keys(sections),
|
|
439
|
-
context: sections['context'] || null,
|
|
509
|
+
context: scrubSecrets(sections['context'] || null),
|
|
440
510
|
filesReferenced: fileRefs.slice(0, 20),
|
|
441
511
|
projectHints: projectHints,
|
|
442
|
-
content: content
|
|
512
|
+
content: scrubSecrets(content)
|
|
443
513
|
};
|
|
444
514
|
}
|
|
445
515
|
|
|
@@ -502,6 +572,109 @@ async function checkPromotionCandidates(mindmeld, validPatterns) {
|
|
|
502
572
|
return candidates;
|
|
503
573
|
}
|
|
504
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Detect correction language patterns in conversation text.
|
|
577
|
+
* Scans for phrases indicating the user corrected the AI's approach.
|
|
578
|
+
* @param {string} conversationText - Raw conversation text to scan
|
|
579
|
+
* @returns {Array<{correction_text: string, context_before: string, context_after: string, pattern_matched: string}>}
|
|
580
|
+
*/
|
|
581
|
+
function detectCorrections(conversationText) {
|
|
582
|
+
if (typeof conversationText !== 'string' || conversationText.length === 0) return [];
|
|
583
|
+
|
|
584
|
+
const correctionPatterns = [
|
|
585
|
+
/no,? don'?t/gi,
|
|
586
|
+
/that'?s wrong/gi,
|
|
587
|
+
/instead,? do/gi,
|
|
588
|
+
/not like that/gi,
|
|
589
|
+
/revert that/gi,
|
|
590
|
+
/undo that/gi,
|
|
591
|
+
/shouldn'?t have/gi,
|
|
592
|
+
/wrong approach/gi,
|
|
593
|
+
/bad pattern/gi,
|
|
594
|
+
/don'?t use/gi,
|
|
595
|
+
/never do that/gi,
|
|
596
|
+
/stop doing/gi,
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
const corrections = [];
|
|
600
|
+
|
|
601
|
+
for (const pattern of correctionPatterns) {
|
|
602
|
+
let match;
|
|
603
|
+
while ((match = pattern.exec(conversationText)) !== null) {
|
|
604
|
+
const matchStart = match.index;
|
|
605
|
+
const matchEnd = matchStart + match[0].length;
|
|
606
|
+
|
|
607
|
+
const contextStart = Math.max(0, matchStart - 100);
|
|
608
|
+
const contextEnd = Math.min(conversationText.length, matchEnd + 100);
|
|
609
|
+
|
|
610
|
+
corrections.push({
|
|
611
|
+
correction_text: match[0],
|
|
612
|
+
context_before: conversationText.slice(contextStart, matchStart),
|
|
613
|
+
context_after: conversationText.slice(matchEnd, contextEnd),
|
|
614
|
+
pattern_matched: pattern.source,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return corrections;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Send detected corrections to the MindMeld API.
|
|
624
|
+
* @param {Array} corrections - Array of correction objects from detectCorrections
|
|
625
|
+
* @param {string} authToken - Auth token for API calls
|
|
626
|
+
* @param {{apiUrl: string}} apiConfig - API configuration
|
|
627
|
+
* @returns {Promise<void>}
|
|
628
|
+
*/
|
|
629
|
+
async function sendCorrections(corrections, authToken, apiConfig) {
|
|
630
|
+
if (!corrections || corrections.length === 0) return;
|
|
631
|
+
|
|
632
|
+
const url = `${apiConfig.apiUrl}/corrections`;
|
|
633
|
+
const body = JSON.stringify({
|
|
634
|
+
corrections: scrubSecretsDeep(corrections),
|
|
635
|
+
source: 'hook-harvest',
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const https = require('https');
|
|
639
|
+
const http = require('http');
|
|
640
|
+
const parsedUrl = new URL(url);
|
|
641
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
642
|
+
|
|
643
|
+
const headers = {
|
|
644
|
+
'Content-Type': 'application/json',
|
|
645
|
+
'Content-Length': Buffer.byteLength(body),
|
|
646
|
+
};
|
|
647
|
+
if (authToken) {
|
|
648
|
+
headers['Authorization'] = `Bearer ${authToken}`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return new Promise((resolve, reject) => {
|
|
652
|
+
const req = transport.request(
|
|
653
|
+
{
|
|
654
|
+
hostname: parsedUrl.hostname,
|
|
655
|
+
port: parsedUrl.port,
|
|
656
|
+
path: parsedUrl.pathname,
|
|
657
|
+
method: 'POST',
|
|
658
|
+
headers,
|
|
659
|
+
},
|
|
660
|
+
(res) => {
|
|
661
|
+
let data = '';
|
|
662
|
+
res.on('data', (chunk) => (data += chunk));
|
|
663
|
+
res.on('end', () => {
|
|
664
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
665
|
+
resolve(data);
|
|
666
|
+
} else {
|
|
667
|
+
reject(new Error(`Corrections API returned ${res.statusCode}: ${data}`));
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
req.on('error', reject);
|
|
673
|
+
req.write(body);
|
|
674
|
+
req.end();
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
505
678
|
/**
|
|
506
679
|
* Generate session ID using crypto for consistency
|
|
507
680
|
*/
|
|
@@ -650,20 +823,95 @@ async function generatePostCompactContext(summary, llmAnalysis) {
|
|
|
650
823
|
return sections.join('\n');
|
|
651
824
|
}
|
|
652
825
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
826
|
+
/**
|
|
827
|
+
* Read stdin with timeout.
|
|
828
|
+
* Claude Code hooks receive JSON input via stdin, not command-line arguments.
|
|
829
|
+
* @returns {Promise<string>} stdin content or empty string
|
|
830
|
+
*/
|
|
831
|
+
function readStdin() {
|
|
832
|
+
return new Promise((resolve) => {
|
|
833
|
+
let data = '';
|
|
834
|
+
const timeout = setTimeout(() => resolve(data), 2000);
|
|
835
|
+
|
|
836
|
+
if (process.stdin.isTTY) {
|
|
837
|
+
clearTimeout(timeout);
|
|
838
|
+
resolve('');
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
process.stdin.setEncoding('utf-8');
|
|
843
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
844
|
+
process.stdin.on('end', () => {
|
|
845
|
+
clearTimeout(timeout);
|
|
846
|
+
resolve(data);
|
|
847
|
+
});
|
|
848
|
+
process.stdin.on('error', () => {
|
|
849
|
+
clearTimeout(timeout);
|
|
850
|
+
resolve('');
|
|
851
|
+
});
|
|
852
|
+
process.stdin.resume();
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Read the session transcript from the best available source.
|
|
858
|
+
* Priority:
|
|
859
|
+
* 1. stdin JSON with transcript_path (Claude Code hooks protocol)
|
|
860
|
+
* 2. process.argv[2] (legacy / manual testing, limited by OS ARG_MAX)
|
|
861
|
+
* @returns {Promise<Object>} Parsed session transcript
|
|
862
|
+
*/
|
|
863
|
+
async function readTranscriptInput() {
|
|
864
|
+
// 1. Try stdin — Claude Code hooks pass JSON with transcript_path
|
|
865
|
+
try {
|
|
866
|
+
const stdin = await readStdin();
|
|
867
|
+
if (stdin) {
|
|
868
|
+
const hookInput = JSON.parse(stdin);
|
|
869
|
+
|
|
870
|
+
// If we got a transcript_path, read the file (no ARG_MAX limit)
|
|
871
|
+
if (hookInput.transcript_path) {
|
|
872
|
+
console.error(`[MindMeld] Reading transcript from file: ${hookInput.transcript_path}`);
|
|
873
|
+
const content = await fs.readFile(hookInput.transcript_path, 'utf-8');
|
|
874
|
+
const transcript = parseSessionTranscript(content);
|
|
875
|
+
// Merge hook metadata (session_id, cwd) into transcript
|
|
876
|
+
if (hookInput.session_id && !transcript.sessionId) {
|
|
877
|
+
transcript.sessionId = hookInput.session_id;
|
|
878
|
+
}
|
|
879
|
+
return transcript;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// If stdin itself contains the transcript (inline JSON)
|
|
883
|
+
if (hookInput.transcript || hookInput.messages || hookInput.sessionId) {
|
|
884
|
+
return parseSessionTranscript(JSON.stringify(hookInput));
|
|
885
|
+
}
|
|
657
886
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
887
|
+
// Stdin had JSON but no transcript — try parsing it as-is
|
|
888
|
+
return parseSessionTranscript(stdin);
|
|
889
|
+
}
|
|
890
|
+
} catch (e) {
|
|
891
|
+
console.error(`[MindMeld] stdin parse failed (falling back to argv): ${e.message}`);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// 2. Fallback: process.argv[2] (legacy, limited to ~1MB on macOS)
|
|
895
|
+
const argInput = process.argv[2];
|
|
896
|
+
if (argInput) {
|
|
897
|
+
console.error('[MindMeld] Reading transcript from argv (legacy mode)');
|
|
898
|
+
return parseSessionTranscript(argInput);
|
|
661
899
|
}
|
|
662
900
|
|
|
663
|
-
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Execute if called directly
|
|
905
|
+
if (require.main === module) {
|
|
906
|
+
readTranscriptInput()
|
|
907
|
+
.then(async (sessionTranscript) => {
|
|
908
|
+
if (!sessionTranscript) {
|
|
909
|
+
console.error('[MindMeld] No transcript input received (stdin or argv)');
|
|
910
|
+
process.exit(0);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const result = await harvestPatterns(sessionTranscript);
|
|
664
914
|
|
|
665
|
-
harvestPatterns(sessionTranscript)
|
|
666
|
-
.then(async (result) => {
|
|
667
915
|
// Generate and output context summary for post-compaction injection
|
|
668
916
|
const postCompactContext = await generatePostCompactContext(
|
|
669
917
|
result,
|
|
@@ -680,4 +928,4 @@ if (require.main === module) {
|
|
|
680
928
|
});
|
|
681
929
|
}
|
|
682
930
|
|
|
683
|
-
module.exports = { harvestPatterns, harvestPlans, parseSessionTranscript, generatePostCompactContext };
|
|
931
|
+
module.exports = { harvestPatterns, harvestPlans, parseSessionTranscript, generatePostCompactContext, scrubSecrets, scrubSecretsDeep, detectCorrections, sendCorrections, readStdin, readTranscriptInput };
|
package/hooks/session-start.js
CHANGED
|
@@ -427,7 +427,7 @@ async function loadAuthToken() {
|
|
|
427
427
|
* @returns {Promise<Object>} Cognito constructor options or empty object (uses prod defaults)
|
|
428
428
|
*/
|
|
429
429
|
async function loadCognitoConfig() {
|
|
430
|
-
// 1. Check .mindmeld/config.json first (
|
|
430
|
+
// 1. Check .mindmeld/config.json first (explicit project config, always wins)
|
|
431
431
|
try {
|
|
432
432
|
const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
433
433
|
const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
|
|
@@ -442,21 +442,28 @@ async function loadCognitoConfig() {
|
|
|
442
442
|
// No .mindmeld/config.json or no auth section
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
// 2. Fallback to .myworld.json
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
445
|
+
// 2. Fallback to .myworld.json — but only for consumer projects.
|
|
446
|
+
// If this IS the MindMeld source repo, .myworld.json has dev Cognito
|
|
447
|
+
// config that would authenticate against the wrong user pool.
|
|
448
|
+
const isSourceRepo = await isMindMeldSourceRepo();
|
|
449
|
+
if (!isSourceRepo) {
|
|
450
|
+
try {
|
|
451
|
+
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
452
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
453
|
+
const config = JSON.parse(content);
|
|
454
|
+
const auth = config.deployments?.backend?.auth;
|
|
455
|
+
if (auth?.domain && auth?.client_id) {
|
|
456
|
+
return {
|
|
457
|
+
cognitoDomain: `${auth.domain}.auth.us-east-2.amazoncognito.com`,
|
|
458
|
+
cognitoClientId: auth.client_id
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
} catch (error) {
|
|
462
|
+
// No .myworld.json
|
|
456
463
|
}
|
|
457
|
-
} catch (error) {
|
|
458
|
-
// No .myworld.json — use production defaults
|
|
459
464
|
}
|
|
465
|
+
|
|
466
|
+
// 3. Production defaults (AuthManager has these built-in)
|
|
460
467
|
return {};
|
|
461
468
|
}
|
|
462
469
|
|
|
@@ -483,15 +490,40 @@ function spawnBackgroundLogin() {
|
|
|
483
490
|
}
|
|
484
491
|
}
|
|
485
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Detect if we're running inside the MindMeld source repository (developer context)
|
|
495
|
+
* vs. a consumer project that uses MindMeld for standards injection.
|
|
496
|
+
*
|
|
497
|
+
* When developing MindMeld itself, .myworld.json contains deployment config pointing
|
|
498
|
+
* at dev/staging environments. The hook should still use production for its own
|
|
499
|
+
* standards and auth — dev URLs are for the product, not for the hook's API calls.
|
|
500
|
+
*
|
|
501
|
+
* @returns {Promise<boolean>} True if this is the MindMeld source repo
|
|
502
|
+
*/
|
|
503
|
+
async function isMindMeldSourceRepo() {
|
|
504
|
+
try {
|
|
505
|
+
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
506
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
507
|
+
const config = JSON.parse(content);
|
|
508
|
+
const productName = (config.project?.product || '').toLowerCase();
|
|
509
|
+
return productName === 'mindmeld';
|
|
510
|
+
} catch (error) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
486
515
|
/**
|
|
487
516
|
* Load API configuration
|
|
488
|
-
* Priority: .mindmeld/config.json → .myworld.json → env var → production default
|
|
489
|
-
*
|
|
490
|
-
*
|
|
517
|
+
* Priority: .mindmeld/config.json → .myworld.json (consumer only) → env var → production default
|
|
518
|
+
*
|
|
519
|
+
* When running inside the MindMeld source repo, .myworld.json is skipped because it
|
|
520
|
+
* contains dev/staging deployment config for the product — not config for the hook's
|
|
521
|
+
* own API calls, which should always target production.
|
|
522
|
+
*
|
|
491
523
|
* @returns {Promise<{apiUrl: string}>}
|
|
492
524
|
*/
|
|
493
525
|
async function loadApiConfig() {
|
|
494
|
-
// 1. Check .mindmeld/config.json first (
|
|
526
|
+
// 1. Check .mindmeld/config.json first (explicit project config, always wins)
|
|
495
527
|
try {
|
|
496
528
|
const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
497
529
|
const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
|
|
@@ -503,20 +535,28 @@ async function loadApiConfig() {
|
|
|
503
535
|
// No .mindmeld/config.json or no apiUrl
|
|
504
536
|
}
|
|
505
537
|
|
|
506
|
-
// 2. Fallback to .myworld.json
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
538
|
+
// 2. Fallback to .myworld.json — but only for consumer projects.
|
|
539
|
+
// If this IS the MindMeld source repo, .myworld.json has dev deployment
|
|
540
|
+
// URLs that would send hook API calls to the wrong environment.
|
|
541
|
+
const isSourceRepo = await isMindMeldSourceRepo();
|
|
542
|
+
if (!isSourceRepo) {
|
|
543
|
+
try {
|
|
544
|
+
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
545
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
546
|
+
const config = JSON.parse(content);
|
|
547
|
+
const backend = config.deployments?.backend;
|
|
548
|
+
if (backend?.api?.base_url) {
|
|
549
|
+
return { apiUrl: backend.api.base_url };
|
|
550
|
+
}
|
|
551
|
+
} catch (error) {
|
|
552
|
+
// No .myworld.json
|
|
553
|
+
}
|
|
519
554
|
}
|
|
555
|
+
|
|
556
|
+
// 3. Environment variable or production default
|
|
557
|
+
return {
|
|
558
|
+
apiUrl: process.env.MINDMELD_API_URL || 'https://api.mindmeld.dev'
|
|
559
|
+
};
|
|
520
560
|
}
|
|
521
561
|
|
|
522
562
|
/**
|
|
@@ -565,7 +605,9 @@ async function fetchRelevantStandardsFromAPI(apiUrl, authToken, characteristics,
|
|
|
565
605
|
try {
|
|
566
606
|
const parsed = JSON.parse(data);
|
|
567
607
|
if (res.statusCode >= 400) {
|
|
568
|
-
|
|
608
|
+
const err = new Error(parsed.message || `HTTP ${res.statusCode}`);
|
|
609
|
+
err.statusCode = res.statusCode;
|
|
610
|
+
reject(err);
|
|
569
611
|
} else {
|
|
570
612
|
resolve(parsed.data?.standards || parsed.standards || []);
|
|
571
613
|
}
|
|
@@ -767,6 +809,14 @@ async function injectContext() {
|
|
|
767
809
|
// Subscription enforcement — do NOT fall through to file-based injection
|
|
768
810
|
console.error('[MindMeld] Active subscription required. Subscribe at app.mindmeld.dev');
|
|
769
811
|
return '';
|
|
812
|
+
} else if (standardsResult.status === 'rejected' &&
|
|
813
|
+
(standardsResult.reason.statusCode === 401 || standardsResult.reason.message === 'Unauthorized')) {
|
|
814
|
+
// Auth token expired or invalid — trigger re-auth and use file-based fallback
|
|
815
|
+
console.error('[MindMeld] Auth token expired. Triggering re-authentication...');
|
|
816
|
+
spawnBackgroundLogin();
|
|
817
|
+
const categories = mindmeld.relevanceDetector.mapCharacteristicsToCategories(characteristics);
|
|
818
|
+
relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories, characteristics);
|
|
819
|
+
console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback (scored)`);
|
|
770
820
|
} else {
|
|
771
821
|
if (standardsResult.status === 'rejected') {
|
|
772
822
|
console.error(`[MindMeld] API fallback: ${standardsResult.reason.message}`);
|
|
@@ -893,6 +943,20 @@ async function cacheSubagentContext(standards, teamPatterns, projectName) {
|
|
|
893
943
|
console.error(`[MindMeld] Cached subagent context (${sections.length} lines)`);
|
|
894
944
|
}
|
|
895
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Get maturity tier prefix for a standard
|
|
948
|
+
* @param {string|undefined} maturityTier - The maturity_tier from the standard
|
|
949
|
+
* @returns {string} Prefix string based on tier
|
|
950
|
+
*/
|
|
951
|
+
function getMaturityPrefix(maturityTier) {
|
|
952
|
+
switch (maturityTier) {
|
|
953
|
+
case 'reinforced': return '**[MUST FOLLOW]** ';
|
|
954
|
+
case 'solidified': return '**[SHOULD FOLLOW]** ';
|
|
955
|
+
case 'provisional':
|
|
956
|
+
default: return '**[CONSIDER]** ';
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
896
960
|
/**
|
|
897
961
|
* Format context injection for Claude Code
|
|
898
962
|
* @param {object} data - Context data to inject
|
|
@@ -968,8 +1032,15 @@ function formatContextInjection(data) {
|
|
|
968
1032
|
const standards = [];
|
|
969
1033
|
const workflows = [];
|
|
970
1034
|
|
|
1035
|
+
const businessInvariants = [];
|
|
971
1036
|
if (relevantStandards && relevantStandards.length > 0) {
|
|
972
1037
|
for (const item of relevantStandards) {
|
|
1038
|
+
// Separate business invariants from code standards
|
|
1039
|
+
if (item.content_type === 'business_invariant') {
|
|
1040
|
+
businessInvariants.push(item);
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
973
1044
|
// Detect workflows: check type flag, rule prefix, keywords, or structured examples
|
|
974
1045
|
const isWorkflow = item.type === 'workflow' ||
|
|
975
1046
|
(item.rule && item.rule.startsWith('WORKFLOW:')) ||
|
|
@@ -990,7 +1061,9 @@ function formatContextInjection(data) {
|
|
|
990
1061
|
sections.push('');
|
|
991
1062
|
|
|
992
1063
|
for (const standard of standards) {
|
|
993
|
-
|
|
1064
|
+
const tierPrefix = getMaturityPrefix(standard.maturity_tier);
|
|
1065
|
+
const lbPrefix = standard.load_bearing ? '[CRITICAL] ' : '';
|
|
1066
|
+
sections.push(`### ${tierPrefix}${lbPrefix}${standard.element}`);
|
|
994
1067
|
sections.push(`**Category**: ${standard.category}`);
|
|
995
1068
|
// Add fingerprint to rule text
|
|
996
1069
|
sections.push(`**Rule**: ${standard.rule} ${fingerprintStr}`);
|
|
@@ -1018,6 +1091,11 @@ function formatContextInjection(data) {
|
|
|
1018
1091
|
}
|
|
1019
1092
|
}
|
|
1020
1093
|
|
|
1094
|
+
if (standard.consequence_tier === 'IRREVERSIBLE' || standard.consequence_tier === 'EXTERNAL_SIDE_EFFECT') {
|
|
1095
|
+
sections.push('');
|
|
1096
|
+
sections.push('⚠️ CONSEQUENCE: This standard involves irreversible/external actions. Verify before proceeding.');
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1021
1099
|
sections.push('');
|
|
1022
1100
|
}
|
|
1023
1101
|
}
|
|
@@ -1033,7 +1111,8 @@ function formatContextInjection(data) {
|
|
|
1033
1111
|
? workflow.examples[0]
|
|
1034
1112
|
: null;
|
|
1035
1113
|
|
|
1036
|
-
|
|
1114
|
+
const tierPrefix = getMaturityPrefix(workflow.maturity_tier);
|
|
1115
|
+
sections.push(`### ${tierPrefix}${workflow.element} ${fingerprintStr}`);
|
|
1037
1116
|
sections.push(`**Category**: ${workflow.category}`);
|
|
1038
1117
|
|
|
1039
1118
|
if (workflowData) {
|
|
@@ -1085,6 +1164,32 @@ function formatContextInjection(data) {
|
|
|
1085
1164
|
}
|
|
1086
1165
|
}
|
|
1087
1166
|
|
|
1167
|
+
// Business invariants (rendered with rationale and consequences)
|
|
1168
|
+
if (businessInvariants.length > 0) {
|
|
1169
|
+
sections.push('## Business Invariants');
|
|
1170
|
+
sections.push('');
|
|
1171
|
+
|
|
1172
|
+
for (const invariant of businessInvariants) {
|
|
1173
|
+
const tierPrefix = getMaturityPrefix(invariant.maturity_tier);
|
|
1174
|
+
sections.push(`### ${tierPrefix}${invariant.element}`);
|
|
1175
|
+
sections.push(`**Domain**: ${invariant.category}`);
|
|
1176
|
+
sections.push(`**Invariant**: ${invariant.rule} ${fingerprintStr}`);
|
|
1177
|
+
if (invariant.rationale) {
|
|
1178
|
+
sections.push(`**Rationale**: ${invariant.rationale}`);
|
|
1179
|
+
}
|
|
1180
|
+
if (invariant.consequences) {
|
|
1181
|
+
sections.push(`**If violated**: ${invariant.consequences}`);
|
|
1182
|
+
}
|
|
1183
|
+
if (invariant.exceptions && Array.isArray(invariant.exceptions) && invariant.exceptions.length > 0) {
|
|
1184
|
+
sections.push('**Exceptions**:');
|
|
1185
|
+
for (const ex of invariant.exceptions) {
|
|
1186
|
+
sections.push(`- ${ex}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
sections.push('');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1088
1193
|
// Team patterns (high correlation only)
|
|
1089
1194
|
if (teamPatterns && teamPatterns.length > 0) {
|
|
1090
1195
|
sections.push('## Team Patterns');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equilateral_ai/mindmeld",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
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": {
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@aws-sdk/client-bedrock-runtime": "^3.460.0",
|
|
83
83
|
"@aws-sdk/client-ssm": "^3.985.0",
|
|
84
|
+
"js-yaml": "^4.1.1",
|
|
84
85
|
"pg": "^8.18.0"
|
|
85
86
|
},
|
|
86
87
|
"devDependencies": {
|