@equilateral_ai/mindmeld 3.3.0 → 3.4.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.
Files changed (69) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-start.js +636 -42
  4. package/hooks/subagent-start.js +150 -0
  5. package/hooks/subagent-stop.js +184 -0
  6. package/package.json +8 -7
  7. package/scripts/init-project.js +74 -33
  8. package/scripts/mcp-bridge.js +220 -0
  9. package/src/core/CorrelationAnalyzer.js +157 -0
  10. package/src/core/LLMPatternDetector.js +198 -0
  11. package/src/core/RelevanceDetector.js +123 -36
  12. package/src/core/StandardsIngestion.js +119 -18
  13. package/src/handlers/activity/activityGetMe.js +1 -1
  14. package/src/handlers/activity/activityGetTeam.js +100 -55
  15. package/src/handlers/admin/adminSetup.js +216 -0
  16. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  17. package/src/handlers/alerts/alertsGet.js +11 -11
  18. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  19. package/src/handlers/analytics/coachingGet.js +11 -11
  20. package/src/handlers/analytics/convergenceGet.js +236 -0
  21. package/src/handlers/analytics/developerScoreGet.js +41 -111
  22. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  23. package/src/handlers/company/companyUsersDelete.js +141 -0
  24. package/src/handlers/company/companyUsersGet.js +90 -0
  25. package/src/handlers/company/companyUsersPost.js +267 -0
  26. package/src/handlers/company/companyUsersPut.js +76 -0
  27. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  28. package/src/handlers/correlations/correlationsGet.js +8 -8
  29. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  30. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  31. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  32. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  33. package/src/handlers/github/githubConnectionStatus.js +1 -1
  34. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  35. package/src/handlers/github/githubPatternsReview.js +7 -36
  36. package/src/handlers/health/healthGet.js +55 -0
  37. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  38. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  39. package/src/handlers/mcp/mcpHandler.js +569 -0
  40. package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
  41. package/src/handlers/notifications/sendNotification.js +18 -18
  42. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  43. package/src/handlers/projects/projectCreate.js +124 -10
  44. package/src/handlers/projects/projectDelete.js +4 -4
  45. package/src/handlers/projects/projectGet.js +8 -8
  46. package/src/handlers/projects/projectUpdate.js +4 -4
  47. package/src/handlers/reports/aiLeverage.js +34 -30
  48. package/src/handlers/reports/engineeringInvestment.js +16 -16
  49. package/src/handlers/reports/riskForecast.js +41 -21
  50. package/src/handlers/reports/standardsRoi.js +101 -9
  51. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  52. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  53. package/src/handlers/standards/discoveriesGet.js +93 -0
  54. package/src/handlers/standards/projectStandardsGet.js +2 -2
  55. package/src/handlers/standards/projectStandardsPut.js +2 -2
  56. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  57. package/src/handlers/standards/standardsTransition.js +112 -15
  58. package/src/handlers/stripe/billingPortalPost.js +1 -1
  59. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  60. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  61. package/src/handlers/stripe/webhookPost.js +42 -14
  62. package/src/handlers/user/apiTokenCreate.js +71 -0
  63. package/src/handlers/user/apiTokenList.js +64 -0
  64. package/src/handlers/user/userSplashGet.js +90 -73
  65. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  66. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  67. package/src/handlers/users/userGet.js +12 -8
  68. package/src/handlers/webhooks/githubWebhook.js +117 -125
  69. package/src/index.js +46 -51
@@ -23,7 +23,7 @@ function generateFingerprint(userId, companyId, tier) {
23
23
  user_id: userId || 'anonymous',
24
24
  company_id: companyId || 'unknown',
25
25
  timestamp: new Date().toISOString(),
26
- subscription_tier: tier || 'free'
26
+ subscription_tier: tier || 'enterprise'
27
27
  };
28
28
 
29
29
  const base64Fingerprint = Buffer.from(JSON.stringify(fingerprint)).toString('base64');
@@ -81,6 +81,274 @@ function filterStandardsByPreferences(standards, preferences) {
81
81
  });
82
82
  }
83
83
 
84
+ /**
85
+ * Detect git remote URL from current directory
86
+ * @returns {Promise<string|null>} Git remote URL or null
87
+ */
88
+ async function detectGitRemote() {
89
+ const { execSync } = require('child_process');
90
+ try {
91
+ const remote = execSync('git remote get-url origin', {
92
+ cwd: process.cwd(),
93
+ encoding: 'utf-8',
94
+ timeout: 2000,
95
+ stdio: ['pipe', 'pipe', 'pipe']
96
+ }).trim();
97
+ return remote || null;
98
+ } catch (error) {
99
+ // Not a git repo or no remote
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get project name from git remote URL or directory name
106
+ * @param {string|null} gitRemote - Git remote URL
107
+ * @returns {string} Project name
108
+ */
109
+ function getProjectNameFromRemote(gitRemote) {
110
+ if (gitRemote) {
111
+ // Extract repo name from URL (handles both HTTPS and SSH)
112
+ // https://github.com/user/repo.git -> repo
113
+ // git@github.com:user/repo.git -> repo
114
+ const match = gitRemote.match(/\/([^\/]+?)(\.git)?$/);
115
+ if (match) {
116
+ return match[1];
117
+ }
118
+ }
119
+ // Fallback to directory name
120
+ return path.basename(process.cwd());
121
+ }
122
+
123
+ /**
124
+ * Auto-create project if it doesn't exist
125
+ * Called when .mindmeld/config.json is missing but user is authenticated
126
+ * @param {string} authToken - Auth token for API calls
127
+ * @param {string} apiUrl - API base URL
128
+ * @returns {Promise<Object|null>} Created/existing project config or null
129
+ */
130
+ async function autoCreateProject(authToken, apiUrl) {
131
+ if (!authToken) {
132
+ return null;
133
+ }
134
+
135
+ const https = require('https');
136
+ const url = require('url');
137
+
138
+ // 1. Detect git remote
139
+ const gitRemote = await detectGitRemote();
140
+ const projectName = getProjectNameFromRemote(gitRemote);
141
+
142
+ // 2. Get user's entitlements to find company
143
+ let companyId = null;
144
+ let clientId = null;
145
+ let userEmail = null;
146
+ let subscriptionTier = 'enterprise';
147
+
148
+ try {
149
+ const entitlementsResponse = await makeHttpRequest(
150
+ `${apiUrl}/api/users/entitlements`,
151
+ 'GET',
152
+ null,
153
+ authToken,
154
+ 3000
155
+ );
156
+
157
+ if (entitlementsResponse && entitlementsResponse.Records && entitlementsResponse.Records.length > 0) {
158
+ // Use first client's first company (user's primary organization)
159
+ const primaryClient = entitlementsResponse.Records[0];
160
+ clientId = primaryClient.client_id;
161
+ subscriptionTier = primaryClient.subscription_tier || 'enterprise';
162
+
163
+ if (primaryClient.companies && primaryClient.companies.length > 0) {
164
+ // Prefer a company where user is admin
165
+ const adminCompany = primaryClient.companies.find(c => c.admin);
166
+ const targetCompany = adminCompany || primaryClient.companies[0];
167
+ companyId = targetCompany.company_id;
168
+ }
169
+ }
170
+
171
+ // Also get user email from /api/users/me
172
+ const userResponse = await makeHttpRequest(
173
+ `${apiUrl}/api/users/me`,
174
+ 'GET',
175
+ null,
176
+ authToken,
177
+ 2000
178
+ );
179
+ if (userResponse && userResponse.data) {
180
+ userEmail = userResponse.data.email_address || userResponse.data.Email_Address;
181
+ }
182
+ } catch (e) {
183
+ console.error('[MindMeld] Could not fetch user info:', e.message);
184
+ }
185
+
186
+ if (!companyId) {
187
+ // User not set up with a company yet
188
+ console.error('[MindMeld] User has no company. Complete onboarding at app.mindmeld.dev');
189
+ return null;
190
+ }
191
+
192
+ // 3. Check if project already exists
193
+ try {
194
+ const projectsResponse = await makeHttpRequest(
195
+ `${apiUrl}/api/projects?Company_ID=${encodeURIComponent(companyId)}`,
196
+ 'GET',
197
+ null,
198
+ authToken,
199
+ 3000
200
+ );
201
+
202
+ if (projectsResponse && projectsResponse.Records) {
203
+ // Look for existing project by name or repo URL
204
+ const existing = projectsResponse.Records.find(p =>
205
+ p.project_name === projectName ||
206
+ (gitRemote && p.repo_url === gitRemote)
207
+ );
208
+
209
+ if (existing) {
210
+ // Project exists - save config locally and return
211
+ const config = {
212
+ projectId: existing.project_id,
213
+ projectName: existing.project_name,
214
+ companyId: existing.company_id,
215
+ userEmail: userEmail,
216
+ subscriptionTier: subscriptionTier,
217
+ repoUrl: existing.repo_url || gitRemote,
218
+ autoCreated: false
219
+ };
220
+ await saveLocalConfig(config);
221
+ console.error(`[MindMeld] Linked to existing project: ${existing.project_name}`);
222
+ return config;
223
+ }
224
+ }
225
+ } catch (e) {
226
+ console.error('[MindMeld] Could not check existing projects:', e.message);
227
+ }
228
+
229
+ // 4. Create new project
230
+ try {
231
+ const createPayload = {
232
+ Company_ID: companyId,
233
+ project_name: projectName,
234
+ description: `Auto-created from ${gitRemote || process.cwd()}`,
235
+ private: true,
236
+ repo_url: gitRemote
237
+ };
238
+
239
+ const createResponse = await makeHttpRequest(
240
+ `${apiUrl}/api/projects`,
241
+ 'POST',
242
+ createPayload,
243
+ authToken,
244
+ 5000
245
+ );
246
+
247
+ if (createResponse && createResponse.Records && createResponse.Records.length > 0) {
248
+ const created = createResponse.Records[0];
249
+ const config = {
250
+ projectId: created.project_id,
251
+ projectName: created.project_name,
252
+ companyId: created.company_id,
253
+ userEmail: userEmail,
254
+ subscriptionTier: subscriptionTier,
255
+ repoUrl: gitRemote,
256
+ autoCreated: true,
257
+ createdAt: new Date().toISOString()
258
+ };
259
+ await saveLocalConfig(config);
260
+ console.error(`[MindMeld] Auto-created project: ${created.project_name}`);
261
+ return config;
262
+ } else if (createResponse && createResponse.error) {
263
+ console.error('[MindMeld] Project creation failed:', createResponse.error);
264
+ }
265
+ } catch (e) {
266
+ console.error('[MindMeld] Could not create project:', e.message);
267
+ }
268
+
269
+ return null;
270
+ }
271
+
272
+ /**
273
+ * Save project config locally to .mindmeld/config.json
274
+ * @param {Object} config - Project configuration
275
+ */
276
+ async function saveLocalConfig(config) {
277
+ const configDir = path.join(process.cwd(), '.mindmeld');
278
+ const configPath = path.join(configDir, 'config.json');
279
+
280
+ try {
281
+ await fs.mkdir(configDir, { recursive: true });
282
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
283
+ } catch (error) {
284
+ console.error('[MindMeld] Could not save config:', error.message);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Make HTTP request helper
290
+ * @param {string} urlStr - Full URL
291
+ * @param {string} method - HTTP method
292
+ * @param {Object|null} body - Request body
293
+ * @param {string|null} authToken - Bearer token
294
+ * @param {number} timeout - Timeout in ms
295
+ * @returns {Promise<Object>} Response object
296
+ */
297
+ function makeHttpRequest(urlStr, method, body, authToken, timeout = 5000) {
298
+ const https = require('https');
299
+ const url = require('url');
300
+
301
+ return new Promise((resolve) => {
302
+ const parsedUrl = url.parse(urlStr);
303
+ const payload = body ? JSON.stringify(body) : null;
304
+
305
+ const options = {
306
+ hostname: parsedUrl.hostname,
307
+ port: parsedUrl.port || 443,
308
+ path: parsedUrl.path,
309
+ method: method,
310
+ headers: {
311
+ 'Content-Type': 'application/json',
312
+ 'Accept': 'application/json'
313
+ },
314
+ timeout: timeout
315
+ };
316
+
317
+ if (authToken) {
318
+ options.headers['Authorization'] = `Bearer ${authToken}`;
319
+ }
320
+ if (payload) {
321
+ options.headers['Content-Length'] = Buffer.byteLength(payload);
322
+ }
323
+
324
+ const req = https.request(options, (res) => {
325
+ let data = '';
326
+ res.on('data', chunk => { data += chunk; });
327
+ res.on('end', () => {
328
+ try {
329
+ const parsed = JSON.parse(data);
330
+ if (res.statusCode >= 400) {
331
+ resolve({ error: parsed.message || `HTTP ${res.statusCode}`, status: res.statusCode });
332
+ } else {
333
+ resolve(parsed);
334
+ }
335
+ } catch (e) {
336
+ resolve({ error: 'Invalid JSON response' });
337
+ }
338
+ });
339
+ });
340
+
341
+ req.on('error', (err) => resolve({ error: err.message }));
342
+ req.on('timeout', () => {
343
+ req.destroy();
344
+ resolve({ error: 'Request timeout' });
345
+ });
346
+
347
+ if (payload) req.write(payload);
348
+ req.end();
349
+ });
350
+ }
351
+
84
352
  /**
85
353
  * Load user and company info from MindMeld config
86
354
  * @returns {Promise<{userId: string, companyId: string, tier: string}>}
@@ -91,10 +359,11 @@ async function loadFingerprintConfig() {
91
359
  const configContent = await fs.readFile(configPath, 'utf-8');
92
360
  const config = JSON.parse(configContent);
93
361
 
362
+ // Support both snake_case and camelCase keys for backwards compatibility
94
363
  return {
95
- userId: config.user_id || process.env.MINDMELD_USER_ID || 'anonymous',
96
- companyId: config.company_id || process.env.MINDMELD_COMPANY_ID || 'unknown',
97
- tier: config.subscription_tier || process.env.MINDMELD_TIER || 'free'
364
+ userId: config.user_id || config.userId || config.userEmail || process.env.MINDMELD_USER_ID || 'anonymous',
365
+ companyId: config.company_id || config.companyId || process.env.MINDMELD_COMPANY_ID || 'unknown',
366
+ tier: config.subscription_tier || config.subscriptionTier || process.env.MINDMELD_TIER || 'enterprise'
98
367
  };
99
368
  } catch (error) {
100
369
  // Expected: config doesn't exist or invalid JSON
@@ -105,7 +374,7 @@ async function loadFingerprintConfig() {
105
374
  return {
106
375
  userId: process.env.MINDMELD_USER_ID || 'anonymous',
107
376
  companyId: process.env.MINDMELD_COMPANY_ID || 'unknown',
108
- tier: process.env.MINDMELD_TIER || 'free'
377
+ tier: process.env.MINDMELD_TIER || 'enterprise'
109
378
  };
110
379
  }
111
380
  }
@@ -153,10 +422,27 @@ async function loadAuthToken() {
153
422
  }
154
423
 
155
424
  /**
156
- * Load Cognito config from .myworld.json (dev vs prod detection)
425
+ * Load Cognito config
426
+ * Priority: .mindmeld/config.json → .myworld.json → production defaults
157
427
  * @returns {Promise<Object>} Cognito constructor options or empty object (uses prod defaults)
158
428
  */
159
429
  async function loadCognitoConfig() {
430
+ // 1. Check .mindmeld/config.json first (always points to prod)
431
+ try {
432
+ const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
433
+ const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
434
+ const config = JSON.parse(content);
435
+ if (config.auth?.cognitoDomain && config.auth?.cognitoClientId) {
436
+ return {
437
+ cognitoDomain: config.auth.cognitoDomain,
438
+ cognitoClientId: config.auth.cognitoClientId
439
+ };
440
+ }
441
+ } catch (error) {
442
+ // No .mindmeld/config.json or no auth section
443
+ }
444
+
445
+ // 2. Fallback to .myworld.json (may point to dev — for backwards compatibility)
160
446
  try {
161
447
  const configPath = path.join(process.cwd(), '.myworld.json');
162
448
  const content = await fs.readFile(configPath, 'utf-8');
@@ -198,20 +484,33 @@ function spawnBackgroundLogin() {
198
484
  }
199
485
 
200
486
  /**
201
- * Load API and Cognito configuration from .myworld.json
202
- * @returns {Promise<{apiUrl: string, cognitoDomain?: string, cognitoClientId?: string}>}
487
+ * Load API configuration
488
+ * Priority: .mindmeld/config.json .myworld.json env var → production default
489
+ * The hook should always talk to the production MindMeld API for standards/patterns,
490
+ * regardless of which environment the developer is building against.
491
+ * @returns {Promise<{apiUrl: string}>}
203
492
  */
204
493
  async function loadApiConfig() {
494
+ // 1. Check .mindmeld/config.json first (always points to prod)
495
+ try {
496
+ const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
497
+ const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
498
+ const config = JSON.parse(content);
499
+ if (config.apiUrl) {
500
+ return { apiUrl: config.apiUrl };
501
+ }
502
+ } catch (error) {
503
+ // No .mindmeld/config.json or no apiUrl
504
+ }
505
+
506
+ // 2. Fallback to .myworld.json (may point to dev)
205
507
  try {
206
508
  const configPath = path.join(process.cwd(), '.myworld.json');
207
509
  const content = await fs.readFile(configPath, 'utf-8');
208
510
  const config = JSON.parse(content);
209
511
  const backend = config.deployments?.backend;
210
- const auth = backend?.auth;
211
512
  return {
212
- apiUrl: backend?.api?.base_url || 'https://api.mindmeld.dev',
213
- cognitoDomain: auth?.domain ? `${auth.domain}.auth.us-east-2.amazoncognito.com` : undefined,
214
- cognitoClientId: auth?.client_id || undefined
513
+ apiUrl: backend?.api?.base_url || 'https://api.mindmeld.dev'
215
514
  };
216
515
  } catch (error) {
217
516
  return {
@@ -268,7 +567,7 @@ async function fetchRelevantStandardsFromAPI(apiUrl, authToken, characteristics,
268
567
  if (res.statusCode >= 400) {
269
568
  reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
270
569
  } else {
271
- resolve(parsed.standards || []);
570
+ resolve(parsed.data?.standards || parsed.standards || []);
272
571
  }
273
572
  } catch (e) {
274
573
  reject(new Error('Invalid API response'));
@@ -287,6 +586,79 @@ async function fetchRelevantStandardsFromAPI(apiUrl, authToken, characteristics,
287
586
  });
288
587
  }
289
588
 
589
+ /**
590
+ * Fetch weekly splash data from API
591
+ * Returns splash summary with stats, or null if already acknowledged
592
+ */
593
+ async function fetchSplashData(apiUrl, authToken) {
594
+ if (!authToken) return null;
595
+
596
+ const https = require('https');
597
+ const url = require('url');
598
+ const parsedUrl = url.parse(`${apiUrl}/api/user/splash`);
599
+
600
+ const options = {
601
+ hostname: parsedUrl.hostname,
602
+ port: parsedUrl.port || 443,
603
+ path: parsedUrl.path,
604
+ method: 'GET',
605
+ headers: {
606
+ 'Authorization': `Bearer ${authToken}`
607
+ },
608
+ timeout: 3000
609
+ };
610
+
611
+ return new Promise((resolve) => {
612
+ const req = https.request(options, (res) => {
613
+ let data = '';
614
+ res.on('data', chunk => { data += chunk; });
615
+ res.on('end', () => {
616
+ try {
617
+ const parsed = JSON.parse(data);
618
+ resolve(parsed.data || parsed);
619
+ } catch (e) {
620
+ resolve(null);
621
+ }
622
+ });
623
+ });
624
+
625
+ req.on('error', () => resolve(null));
626
+ req.on('timeout', () => { req.destroy(); resolve(null); });
627
+ req.end();
628
+ });
629
+ }
630
+
631
+ /**
632
+ * Acknowledge splash (fire-and-forget) to prevent showing again this week
633
+ */
634
+ function acknowledgeSplash(apiUrl, authToken, weekStart) {
635
+ if (!authToken || !weekStart) return;
636
+
637
+ const https = require('https');
638
+ const url = require('url');
639
+ const payload = JSON.stringify({ week_start: weekStart });
640
+ const parsedUrl = url.parse(`${apiUrl}/api/user/splash/acknowledge`);
641
+
642
+ const options = {
643
+ hostname: parsedUrl.hostname,
644
+ port: parsedUrl.port || 443,
645
+ path: parsedUrl.path,
646
+ method: 'POST',
647
+ headers: {
648
+ 'Content-Type': 'application/json',
649
+ 'Authorization': `Bearer ${authToken}`,
650
+ 'Content-Length': Buffer.byteLength(payload)
651
+ },
652
+ timeout: 3000
653
+ };
654
+
655
+ const req = https.request(options, () => {}); // fire-and-forget
656
+ req.on('error', () => {}); // silently ignore
657
+ req.on('timeout', () => req.destroy());
658
+ req.write(payload);
659
+ req.end();
660
+ }
661
+
290
662
  /**
291
663
  * Main hook execution
292
664
  */
@@ -294,19 +666,39 @@ async function injectContext() {
294
666
  const startTime = Date.now();
295
667
 
296
668
  try {
297
- // Fast bail if MindMeld not configured
298
- const hasMindmeld = await checkMindmeldConfiguration();
669
+ // Check if MindMeld is configured for this project
670
+ let hasMindmeld = await checkMindmeldConfiguration();
671
+
672
+ // If not configured, try auto-create for authenticated users
299
673
  if (!hasMindmeld) {
300
- // Check if user is a subscriber but this project isn't initialized
301
- try {
302
- const os = require('os');
303
- const authPath = path.join(os.homedir(), '.mindmeld', 'auth.json');
304
- await fs.access(authPath);
305
- console.error('[MindMeld] Project not initialized. Run "mindmeld init" in this directory.');
306
- } catch (e) {
307
- // No auth file not a subscriber, stay silent
674
+ // Load auth token and API config first
675
+ const [authToken, apiConfig] = await Promise.all([
676
+ loadAuthToken(),
677
+ loadApiConfig()
678
+ ]);
679
+
680
+ if (authToken) {
681
+ // User is authenticated - try to auto-create project
682
+ console.error('[MindMeld] No project config found. Auto-detecting...');
683
+ const autoCreated = await autoCreateProject(authToken, apiConfig.apiUrl);
684
+ if (autoCreated) {
685
+ hasMindmeld = true; // Config now exists
686
+ }
687
+ } else {
688
+ // Not authenticated - check if they have auth file but token expired
689
+ try {
690
+ const os = require('os');
691
+ const authPath = path.join(os.homedir(), '.mindmeld', 'auth.json');
692
+ await fs.access(authPath);
693
+ console.error('[MindMeld] Auth token expired. Run "mindmeld login" to refresh.');
694
+ } catch (e) {
695
+ // No auth file — not a subscriber, stay silent
696
+ }
697
+ }
698
+
699
+ if (!hasMindmeld) {
700
+ return '';
308
701
  }
309
- return '';
310
702
  }
311
703
 
312
704
  // 1. Parallel local reads (no network, no DB)
@@ -334,27 +726,54 @@ async function injectContext() {
334
726
 
335
727
  const sessionId = mindmeld.generateSessionId();
336
728
 
729
+ // 3b. Persist session context for pre-compact hook
730
+ // Pre-compact creates a new MindmeldClient and needs project context
731
+ try {
732
+ const sessionContext = {
733
+ sessionId: sessionId,
734
+ projectId: context.projectId,
735
+ projectName: context.projectName,
736
+ companyId: context.companyId,
737
+ userEmail: context.userEmail,
738
+ startedAt: new Date().toISOString()
739
+ };
740
+ const mindmeldDir = path.join(process.cwd(), '.mindmeld');
741
+ await fs.mkdir(mindmeldDir, { recursive: true });
742
+ await fs.writeFile(
743
+ path.join(mindmeldDir, 'current-session.json'),
744
+ JSON.stringify(sessionContext, null, 2)
745
+ );
746
+ } catch (err) {
747
+ console.error('[MindMeld] Could not persist session context (non-fatal):', err.message);
748
+ }
749
+
337
750
  // 4. Detect project characteristics locally (file system only, no DB)
338
751
  const characteristics = await mindmeld.relevanceDetector.detectProjectCharacteristics();
339
752
 
340
- // 5. Parallel API calls: standards + team context
341
- const [standardsResult, projectContextResult] = await Promise.allSettled([
753
+ // 5. Parallel API calls: standards + team context + splash
754
+ const [standardsResult, projectContextResult, splashResult] = await Promise.allSettled([
342
755
  fetchRelevantStandardsFromAPI(apiConfig.apiUrl, authToken, characteristics, context.projectId, preferences),
343
- mindmeld.loadProjectContext(context.projectId)
756
+ mindmeld.loadProjectContext(context.projectId),
757
+ fetchSplashData(apiConfig.apiUrl, authToken)
344
758
  ]);
345
759
 
346
- // 6. Resolve standards: API → file-based fallback
760
+ // 6. Resolve standards: API → file-based fallback (only for network errors, not subscription blocks)
347
761
  let relevantStandards = [];
348
762
  if (standardsResult.status === 'fulfilled' && standardsResult.value.length > 0) {
349
763
  relevantStandards = standardsResult.value;
350
764
  console.error(`[MindMeld] ${relevantStandards.length} standards from API`);
765
+ } else if (standardsResult.status === 'rejected' &&
766
+ standardsResult.reason.message.includes('subscription required')) {
767
+ // Subscription enforcement — do NOT fall through to file-based injection
768
+ console.error('[MindMeld] Active subscription required. Subscribe at app.mindmeld.dev');
769
+ return '';
351
770
  } else {
352
771
  if (standardsResult.status === 'rejected') {
353
772
  console.error(`[MindMeld] API fallback: ${standardsResult.reason.message}`);
354
773
  }
355
774
  const categories = mindmeld.relevanceDetector.mapCharacteristicsToCategories(characteristics);
356
- relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories);
357
- console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback`);
775
+ relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories, characteristics);
776
+ console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback (scored)`);
358
777
  }
359
778
 
360
779
  // 7. Filter by project preferences
@@ -367,7 +786,7 @@ async function injectContext() {
367
786
  relevantStandards = relevantStandards.slice(0, 10);
368
787
 
369
788
  // 8. Record standards shown (fire-and-forget, non-blocking)
370
- mindmeld.recordStandardsShown(sessionId, relevantStandards);
789
+ mindmeld.recordStandardsShown(sessionId, relevantStandards, context.projectId, context.userEmail);
371
790
 
372
791
  // 9. Resolve team context from parallel API call
373
792
  const projectContext = projectContextResult.status === 'fulfilled' ? projectContextResult.value : null;
@@ -378,7 +797,15 @@ async function injectContext() {
378
797
  ? projectContext.recentLearning
379
798
  : [];
380
799
 
381
- // 10. Build context injection with fingerprint
800
+ // 10. Resolve splash data
801
+ const splashData = splashResult.status === 'fulfilled' ? splashResult.value : null;
802
+
803
+ // Auto-acknowledge splash (fire-and-forget) so it doesn't show again this week
804
+ if (splashData && splashData.show_splash && splashData.summary) {
805
+ acknowledgeSplash(apiConfig.apiUrl, authToken, splashData.summary.week_start);
806
+ }
807
+
808
+ // 11. Build context injection with fingerprint
382
809
  const injection = formatContextInjection({
383
810
  project: context.projectName,
384
811
  sessionId: sessionId,
@@ -386,9 +813,17 @@ async function injectContext() {
386
813
  relevantStandards: relevantStandards,
387
814
  teamPatterns: teamPatterns,
388
815
  recentLearning: recentLearning,
389
- fingerprint: fingerprintConfig
816
+ fingerprint: fingerprintConfig,
817
+ splash: splashData
390
818
  });
391
819
 
820
+ // 12. Cache condensed context for subagent injection
821
+ try {
822
+ await cacheSubagentContext(relevantStandards, teamPatterns, context.projectName);
823
+ } catch (cacheError) {
824
+ console.error('[MindMeld] Subagent context cache failed (non-fatal):', cacheError.message);
825
+ }
826
+
392
827
  const elapsed = Date.now() - startTime;
393
828
  console.error(`[MindMeld] Context injected in ${elapsed}ms`);
394
829
 
@@ -418,6 +853,46 @@ async function checkMindmeldConfiguration() {
418
853
  }
419
854
  }
420
855
 
856
+ /**
857
+ * Cache condensed context for subagent injection
858
+ * Subagents don't get session-start hooks, so we cache the essentials
859
+ * to a file that SubagentStart can read instantly (< 50ms)
860
+ *
861
+ * @param {Array} standards - Relevant standards from API
862
+ * @param {Array} teamPatterns - Team patterns
863
+ * @param {string} projectName - Current project name
864
+ */
865
+ async function cacheSubagentContext(standards, teamPatterns, projectName) {
866
+ const mindmeldDir = path.join(process.cwd(), '.mindmeld');
867
+ const cachePath = path.join(mindmeldDir, 'subagent-context.md');
868
+
869
+ const sections = [];
870
+ sections.push(`# MindMeld Standards Context (${projectName})`);
871
+ sections.push('');
872
+ sections.push('Follow these standards when writing or modifying code:');
873
+ sections.push('');
874
+
875
+ // Include standards as compact rules (no examples/anti-patterns to save tokens)
876
+ if (standards && standards.length > 0) {
877
+ for (const s of standards) {
878
+ sections.push(`- **${s.element}** [${s.category}]: ${s.rule}`);
879
+ }
880
+ sections.push('');
881
+ }
882
+
883
+ // Include team patterns as compact rules
884
+ if (teamPatterns && teamPatterns.length > 0) {
885
+ sections.push('## Team Patterns');
886
+ for (const p of teamPatterns) {
887
+ sections.push(`- **${p.element}** (${(p.correlation * 100).toFixed(0)}% correlation): ${p.rule}`);
888
+ }
889
+ sections.push('');
890
+ }
891
+
892
+ await fs.writeFile(cachePath, sections.join('\n'));
893
+ console.error(`[MindMeld] Cached subagent context (${sections.length} lines)`);
894
+ }
895
+
421
896
  /**
422
897
  * Format context injection for Claude Code
423
898
  * @param {object} data - Context data to inject
@@ -431,7 +906,8 @@ function formatContextInjection(data) {
431
906
  relevantStandards,
432
907
  teamPatterns,
433
908
  recentLearning,
434
- fingerprint
909
+ fingerprint,
910
+ splash
435
911
  } = data;
436
912
 
437
913
  const sections = [];
@@ -453,6 +929,34 @@ function formatContextInjection(data) {
453
929
  sections.push('Licensed for use within MindMeld platform only. Redistribution prohibited.');
454
930
  sections.push('');
455
931
 
932
+ // Weekly splash (shown once per week)
933
+ if (splash && splash.show_splash) {
934
+ if (splash.is_greenfield && splash.tips && splash.tips.length > 0) {
935
+ sections.push('## Getting Started with MindMeld');
936
+ for (const tip of splash.tips) {
937
+ sections.push(`- ${tip}`);
938
+ }
939
+ sections.push('');
940
+ } else if (splash.summary) {
941
+ const s = splash.summary;
942
+ const formatDate = (dateStr) => {
943
+ const d = new Date(dateStr + 'T00:00:00Z');
944
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
945
+ };
946
+ const hours = Math.floor(s.total_duration_minutes / 60);
947
+ const mins = s.total_duration_minutes % 60;
948
+ const duration = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
949
+
950
+ sections.push(`## Weekly Recap (${formatDate(s.week_start)} - ${formatDate(s.week_end)})`);
951
+ sections.push(`${s.sessions_count} sessions | ${duration} total | ${s.active_projects} project${s.active_projects !== 1 ? 's' : ''}`);
952
+ sections.push(`${s.standards_injected} standards injected, ${s.standards_followed} followed, ${s.violations_detected} violation${s.violations_detected !== 1 ? 's' : ''}`);
953
+ if (s.patterns_harvested > 0) {
954
+ sections.push(`${s.patterns_harvested} patterns harvested`);
955
+ }
956
+ sections.push('');
957
+ }
958
+ }
959
+
456
960
  // Collaborators
457
961
  if (collaborators && collaborators.length > 0) {
458
962
  sections.push('## Team');
@@ -460,30 +964,120 @@ function formatContextInjection(data) {
460
964
  sections.push('');
461
965
  }
462
966
 
463
- // Relevant standards (top 10 only)
967
+ // Separate standards and workflows
968
+ const standards = [];
969
+ const workflows = [];
970
+
464
971
  if (relevantStandards && relevantStandards.length > 0) {
972
+ for (const item of relevantStandards) {
973
+ // Detect workflows: check type flag, rule prefix, keywords, or structured examples
974
+ const isWorkflow = item.type === 'workflow' ||
975
+ (item.rule && item.rule.startsWith('WORKFLOW:')) ||
976
+ (Array.isArray(item.keywords) && item.keywords.includes('workflow') && item.examples && item.examples[0]?.type === 'workflow') ||
977
+ (item.tags && item.tags.includes('workflow') && item.examples && item.examples[0]?.type === 'workflow');
978
+
979
+ if (isWorkflow) {
980
+ workflows.push(item);
981
+ } else {
982
+ standards.push(item);
983
+ }
984
+ }
985
+ }
986
+
987
+ // Relevant standards
988
+ if (standards.length > 0) {
465
989
  sections.push('## Relevant Standards');
466
990
  sections.push('');
467
991
 
468
- for (const standard of relevantStandards) {
992
+ for (const standard of standards) {
469
993
  sections.push(`### ${standard.element}`);
470
994
  sections.push(`**Category**: ${standard.category}`);
471
995
  // Add fingerprint to rule text
472
996
  sections.push(`**Rule**: ${standard.rule} ${fingerprintStr}`);
473
997
 
474
998
  if (standard.examples && standard.examples.length > 0) {
475
- sections.push('');
476
- sections.push('**Example**:');
477
- sections.push('```javascript');
478
- sections.push(standard.examples[0].code);
479
- sections.push('```');
999
+ const example = standard.examples[0];
1000
+ const exampleCode = typeof example === 'string' ? example : (example?.code || example?.description || '');
1001
+ if (exampleCode) {
1002
+ sections.push('');
1003
+ sections.push('**Example**:');
1004
+ sections.push('```javascript');
1005
+ sections.push(exampleCode);
1006
+ sections.push('```');
1007
+ }
480
1008
  }
481
1009
 
482
1010
  if (standard.anti_patterns && standard.anti_patterns.length > 0) {
483
1011
  sections.push('');
484
1012
  sections.push('**Anti-patterns**:');
485
1013
  for (const antiPattern of standard.anti_patterns) {
486
- sections.push(`- ${antiPattern.description}`);
1014
+ const desc = typeof antiPattern === 'string' ? antiPattern : (antiPattern?.description || '');
1015
+ if (desc) {
1016
+ sections.push(`- ❌ ${desc}`);
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ sections.push('');
1022
+ }
1023
+ }
1024
+
1025
+ // Workflows (rendered as ordered procedures)
1026
+ if (workflows.length > 0) {
1027
+ sections.push('## Workflows');
1028
+ sections.push('');
1029
+
1030
+ for (const workflow of workflows) {
1031
+ // Extract workflow data from examples[0]
1032
+ const workflowData = workflow.examples && workflow.examples[0]?.type === 'workflow'
1033
+ ? workflow.examples[0]
1034
+ : null;
1035
+
1036
+ sections.push(`### ${workflow.element} ${fingerprintStr}`);
1037
+ sections.push(`**Category**: ${workflow.category}`);
1038
+
1039
+ if (workflowData) {
1040
+ sections.push(`**When**: ${workflowData.trigger}`);
1041
+ sections.push('');
1042
+
1043
+ // Preconditions
1044
+ if (workflowData.preconditions && workflowData.preconditions.length > 0) {
1045
+ sections.push('**Preconditions**:');
1046
+ for (const pre of workflowData.preconditions) {
1047
+ sections.push(`- ${pre}`);
1048
+ }
1049
+ sections.push('');
1050
+ }
1051
+
1052
+ // Steps
1053
+ if (workflowData.steps && workflowData.steps.length > 0) {
1054
+ sections.push('**Steps**:');
1055
+ for (const step of workflowData.steps) {
1056
+ const gateMarker = step.gate ? ' (GATE)' : '';
1057
+ sections.push(`${step.index}. **${step.name}**${gateMarker} — ${step.description}`);
1058
+
1059
+ if (step.command) {
1060
+ // Clean up multiline commands for display
1061
+ const cmd = step.command.trim().split('\n')[0];
1062
+ sections.push(` \`${cmd}\``);
1063
+ }
1064
+
1065
+ if (step.validation) {
1066
+ sections.push(` Verify: ${step.validation}`);
1067
+ }
1068
+ }
1069
+ sections.push('');
1070
+ }
1071
+ }
1072
+
1073
+ // Anti-patterns
1074
+ if (workflow.anti_patterns && workflow.anti_patterns.length > 0) {
1075
+ sections.push('**Anti-patterns**:');
1076
+ for (const antiPattern of workflow.anti_patterns) {
1077
+ const desc = typeof antiPattern === 'string' ? antiPattern : (antiPattern?.description || '');
1078
+ if (desc) {
1079
+ sections.push(`- ❌ ${desc}`);
1080
+ }
487
1081
  }
488
1082
  }
489
1083