@equilateral_ai/mindmeld 3.3.1 → 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 +635 -41
  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 +8 -5
@@ -10,16 +10,35 @@ const crypto = require('crypto');
10
10
  const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
11
11
 
12
12
  exports.handler = wrapHandler(async ({ body, headers }) => {
13
- // Verify webhook signature
13
+ // Verify webhook signature — try per-repo secret first, then global fallback
14
14
  const signature = headers['x-hub-signature-256'] || headers['X-Hub-Signature-256'];
15
- const secret = process.env.GITHUB_WEBHOOK_SECRET;
15
+ const rawBody = JSON.stringify(body);
16
+ const repoFullName = body?.repository?.full_name;
16
17
 
17
- if (!secret) {
18
- console.error('GITHUB_WEBHOOK_SECRET not configured');
19
- return createErrorResponse(500, 'Webhook not configured');
18
+ let verified = false;
19
+
20
+ // Try per-repo secret from git_repositories
21
+ if (repoFullName) {
22
+ const repoResult = await executeQuery(`
23
+ SELECT webhook_secret FROM rapport.git_repositories
24
+ WHERE repo_name = $1 AND webhook_secret IS NOT NULL
25
+ LIMIT 1
26
+ `, [repoFullName]);
27
+
28
+ if (repoResult.rowCount > 0 && repoResult.rows[0].webhook_secret) {
29
+ verified = verifySignature(rawBody, signature, repoResult.rows[0].webhook_secret);
30
+ }
31
+ }
32
+
33
+ // Fallback to global secret
34
+ if (!verified) {
35
+ const globalSecret = process.env.GITHUB_WEBHOOK_SECRET;
36
+ if (globalSecret) {
37
+ verified = verifySignature(rawBody, signature, globalSecret);
38
+ }
20
39
  }
21
40
 
22
- if (!verifySignature(JSON.stringify(body), signature, secret)) {
41
+ if (!verified) {
23
42
  return createErrorResponse(401, 'Invalid signature');
24
43
  }
25
44
 
@@ -58,94 +77,93 @@ async function handlePushEvent(payload) {
58
77
  const repoFullName = payload.repository?.full_name;
59
78
  if (!repoFullName) return;
60
79
 
61
- // Find project by repo URL
62
- const projectResult = await executeQuery(`
63
- SELECT project_id FROM rapport.projects
64
- WHERE repo_url LIKE $1
65
- `, [`%${repoFullName}%`]);
80
+ const repoUrl = payload.repository?.html_url || `https://github.com/${repoFullName}`;
81
+ const branch = payload.ref?.replace('refs/heads/', '') || 'unknown';
66
82
 
67
- if (projectResult.rowCount === 0) {
68
- console.log(`Project not found for repo: ${repoFullName}`);
69
- return;
83
+ // Find or create repo in git_repositories
84
+ let repoResult = await executeQuery(`
85
+ SELECT repo_id, company_id FROM rapport.git_repositories
86
+ WHERE repo_name = $1 OR repo_url = $2
87
+ `, [repoFullName, repoUrl]);
88
+
89
+ if (repoResult.rowCount === 0) {
90
+ // Look up company from projects table
91
+ const projectResult = await executeQuery(`
92
+ SELECT project_id, company_id FROM rapport.projects
93
+ WHERE repo_url LIKE $1
94
+ LIMIT 1
95
+ `, [`%${repoFullName}%`]);
96
+
97
+ const companyId = projectResult.rows[0]?.company_id || null;
98
+
99
+ // Auto-register the repo
100
+ repoResult = await executeQuery(`
101
+ INSERT INTO rapport.git_repositories (repo_id, repo_name, repo_url, company_id, default_branch, created_at, updated_at)
102
+ VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW())
103
+ RETURNING repo_id, company_id
104
+ `, [repoFullName, repoUrl, companyId, branch]);
105
+
106
+ console.log(`Auto-registered repo: ${repoFullName} (repo_id: ${repoResult.rows[0].repo_id})`);
70
107
  }
71
108
 
72
- const projectId = projectResult.rows[0].project_id;
73
- const branch = payload.ref?.replace('refs/heads/', '') || 'unknown';
109
+ const repoId = repoResult.rows[0].repo_id;
74
110
 
75
111
  for (const commit of (payload.commits || [])) {
76
- await recordCommit(commit, projectId, branch);
112
+ await recordCommit(commit, repoId, repoFullName, repoUrl);
113
+ }
114
+
115
+ // Update last_commit_sha on the repo
116
+ const lastCommit = payload.head_commit?.id || payload.commits?.[payload.commits.length - 1]?.id;
117
+ if (lastCommit) {
118
+ await executeQuery(`
119
+ UPDATE rapport.git_repositories SET last_commit_sha = $1, updated_at = NOW()
120
+ WHERE repo_id = $2
121
+ `, [lastCommit, repoId]);
77
122
  }
78
123
  }
79
124
 
80
- async function recordCommit(commit, projectId, branch) {
125
+ async function recordCommit(commit, repoId, repoName, repoUrl) {
81
126
  const authorEmail = commit.author?.email;
82
127
  if (!authorEmail) return;
83
128
 
84
- // Find user by email
85
- const userResult = await executeQuery(`
86
- SELECT "Email_Address" FROM "Users"
87
- WHERE "Email_Address" = $1 AND active = true
88
- `, [authorEmail]);
129
+ // Check for duplicate
130
+ const existing = await executeQuery(`
131
+ SELECT 1 FROM rapport.commits WHERE commit_sha = $1 LIMIT 1
132
+ `, [commit.id]);
89
133
 
90
- if (userResult.rowCount === 0) {
91
- console.log(`User not found for email: ${authorEmail}`);
134
+ if (existing.rowCount > 0) {
135
+ console.log(`Commit ${commit.id.substring(0, 8)} already recorded, skipping`);
92
136
  return;
93
137
  }
94
138
 
95
- const email = userResult.rows[0].Email_Address;
96
-
97
- // Find recent session for correlation
98
- const sessionResult = await executeQuery(`
99
- SELECT session_id, ended_at, started_at
100
- FROM rapport.sessions
101
- WHERE email_address = $1
102
- AND project_id = $2
103
- ORDER BY started_at DESC
104
- LIMIT 1
105
- `, [email, projectId]);
106
-
107
- let sessionId = null;
108
- let withinSession = false;
109
- let timeSinceSession = null;
110
-
111
- if (sessionResult.rowCount > 0) {
112
- const session = sessionResult.rows[0];
113
- const commitTime = new Date(commit.timestamp);
114
- const sessionEnd = session.ended_at ? new Date(session.ended_at) : new Date();
115
- const sessionStart = new Date(session.started_at);
116
-
117
- // Commit within session if after start and before/during end
118
- withinSession = commitTime >= sessionStart && (!session.ended_at || commitTime <= sessionEnd);
119
-
120
- if (!withinSession && session.ended_at) {
121
- timeSinceSession = Math.round((commitTime - sessionEnd) / (1000 * 60));
122
- }
139
+ // Build changed_files JSONB from GitHub payload
140
+ const changedFiles = [
141
+ ...(commit.added || []).map(f => ({ path: f, action: 'added' })),
142
+ ...(commit.modified || []).map(f => ({ path: f, action: 'modified' })),
143
+ ...(commit.removed || []).map(f => ({ path: f, action: 'removed' }))
144
+ ];
123
145
 
124
- sessionId = session.session_id;
125
- }
126
-
127
- // Insert commit
128
146
  await executeQuery(`
129
147
  INSERT INTO rapport.commits (
130
- commit_hash, project_id, email_address, branch, message,
131
- committed_at, files_changed,
132
- session_id, within_session, time_since_session_minutes
133
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
134
- ON CONFLICT (commit_hash) DO NOTHING
148
+ commit_id, commit_sha, repo_id, repo_url, repo_name,
149
+ author_email, author_name, commit_timestamp, message,
150
+ changed_files, is_merge, created_at
151
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
135
152
  `, [
136
153
  commit.id,
137
- projectId,
138
- email,
139
- branch,
140
- commit.message?.substring(0, 500), // Truncate long messages
154
+ commit.id,
155
+ repoId,
156
+ repoUrl,
157
+ repoName,
158
+ authorEmail,
159
+ commit.author?.name || authorEmail,
141
160
  commit.timestamp,
142
- (commit.added?.length || 0) + (commit.modified?.length || 0) + (commit.removed?.length || 0),
143
- sessionId,
144
- withinSession,
145
- timeSinceSession
161
+ commit.message?.substring(0, 2000),
162
+ JSON.stringify(changedFiles),
163
+ commit.message?.toLowerCase().startsWith('merge') || false
146
164
  ]);
147
165
 
148
- console.log(`Recorded commit ${commit.id} for ${email}`);
166
+ console.log(`Recorded commit ${commit.id.substring(0, 8)} by ${authorEmail}`);
149
167
  }
150
168
 
151
169
  async function handlePREvent(payload) {
@@ -153,71 +171,45 @@ async function handlePREvent(payload) {
153
171
  const repoFullName = payload.repository?.full_name;
154
172
  if (!pr || !repoFullName) return;
155
173
 
156
- // Find project by repo URL
157
- const projectResult = await executeQuery(`
158
- SELECT project_id FROM rapport.projects
159
- WHERE repo_url LIKE $1
160
- `, [`%${repoFullName}%`]);
161
-
162
- if (projectResult.rowCount === 0) {
163
- console.log(`Project not found for repo: ${repoFullName}`);
164
- return;
165
- }
174
+ const repoUrl = payload.repository?.html_url || `https://github.com/${repoFullName}`;
166
175
 
167
- const projectId = projectResult.rows[0].project_id;
168
- const prId = `github:${repoFullName}:${pr.number}`;
176
+ // Find repo
177
+ const repoResult = await executeQuery(`
178
+ SELECT repo_id FROM rapport.git_repositories
179
+ WHERE repo_name = $1 OR repo_url = $2
180
+ `, [repoFullName, repoUrl]);
169
181
 
170
- // Try to find user by email or login
171
- const authorEmail = pr.user?.email || `${pr.user?.login}@users.noreply.github.com`;
172
- const userResult = await executeQuery(`
173
- SELECT "Email_Address" FROM "Users"
174
- WHERE "Email_Address" = $1 AND active = true
175
- `, [authorEmail]);
176
-
177
- if (userResult.rowCount === 0) {
178
- console.log(`User not found for: ${authorEmail}`);
182
+ if (repoResult.rowCount === 0) {
183
+ console.log(`Repo not found for: ${repoFullName} — push event must arrive first`);
179
184
  return;
180
185
  }
181
186
 
182
- const email = userResult.rows[0].Email_Address;
183
-
184
- // Calculate review time if merged
185
- let reviewTimeHours = null;
186
- if (pr.merged_at && pr.created_at) {
187
- const created = new Date(pr.created_at);
188
- const merged = new Date(pr.merged_at);
189
- reviewTimeHours = (merged - created) / (1000 * 60 * 60);
190
- }
187
+ const repoId = repoResult.rows[0].repo_id;
188
+ const authorEmail = pr.user?.email || `${pr.user?.login}@users.noreply.github.com`;
189
+ const status = pr.merged ? 'merged' : pr.state;
191
190
 
192
- // Upsert PR
191
+ // Upsert PR by repo_id + pr_number
193
192
  await executeQuery(`
194
193
  INSERT INTO rapport.pull_requests (
195
- pr_id, project_id, email_address, title, branch, base_branch,
196
- status, opened_at, merged_at, closed_at,
197
- commits_count, files_changed, additions, deletions, review_time_hours
198
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
199
- ON CONFLICT (pr_id) DO UPDATE SET
200
- status = EXCLUDED.status,
201
- merged_at = EXCLUDED.merged_at,
202
- closed_at = EXCLUDED.closed_at,
203
- review_time_hours = EXCLUDED.review_time_hours
194
+ pr_id, repo_id, pr_number, title, author_email,
195
+ status, created_at, merged_at, closed_at,
196
+ lines_added, lines_removed, files_changed, review_comments
197
+ ) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
198
+ ON CONFLICT (pr_id) DO NOTHING
204
199
  `, [
205
- prId,
206
- projectId,
207
- email,
200
+ repoId,
201
+ pr.number,
208
202
  pr.title?.substring(0, 500),
209
- pr.head?.ref,
210
- pr.base?.ref,
211
- pr.merged ? 'merged' : pr.state,
203
+ authorEmail,
204
+ status,
212
205
  pr.created_at,
213
- pr.merged_at,
214
- pr.closed_at,
215
- pr.commits,
216
- pr.changed_files,
217
- pr.additions,
218
- pr.deletions,
219
- reviewTimeHours
206
+ pr.merged_at || null,
207
+ pr.closed_at || null,
208
+ pr.additions || 0,
209
+ pr.deletions || 0,
210
+ pr.changed_files || 0,
211
+ pr.review_comments || 0
220
212
  ]);
221
213
 
222
- console.log(`Recorded PR ${prId} for ${email}`);
214
+ console.log(`Recorded PR #${pr.number} (${status}) for ${authorEmail}`);
223
215
  }
package/src/index.js CHANGED
@@ -65,7 +65,7 @@ class MindmeldClient {
65
65
  companyId: config.companyId || null,
66
66
  collaborators: config.collaborators || [],
67
67
  userEmail: config.userEmail || null,
68
- subscriptionTier: config.subscriptionTier || 'free'
68
+ subscriptionTier: config.subscriptionTier || 'enterprise'
69
69
  };
70
70
 
71
71
  return this.currentProject;
@@ -713,14 +713,14 @@ class MindmeldClient {
713
713
  * @param {string} sessionId - Session identifier
714
714
  * @param {Array} standards - Array of standards that were injected
715
715
  */
716
- recordStandardsShown(sessionId, standards) {
716
+ recordStandardsShown(sessionId, standards, projectId, userId) {
717
717
  // Fire and forget - don't await to keep hook fast
718
- this._recordStandardsAsync(sessionId, standards).catch(err => {
718
+ this._recordStandardsAsync(sessionId, standards, projectId, userId).catch(err => {
719
719
  console.error('[Rapport] recordStandardsShown error:', err.message);
720
720
  });
721
721
  }
722
722
 
723
- async _recordStandardsAsync(sessionId, standards) {
723
+ async _recordStandardsAsync(sessionId, standards, projectId, userId) {
724
724
  if (!standards || standards.length === 0) return;
725
725
 
726
726
  const apiUrl = this.config.apiUrl;
@@ -730,9 +730,12 @@ class MindmeldClient {
730
730
  // Prepare payload
731
731
  const payload = JSON.stringify({
732
732
  session_id: sessionId,
733
+ project_id: projectId || undefined,
734
+ user_id: userId || undefined,
733
735
  standards: standards.map(s => ({
734
736
  pattern_id: s.pattern_id || s.element,
735
- relevance_score: s.score || 0
737
+ title: s.element || s.pattern_id,
738
+ relevance_score: s.relevance_score || s.score || 0
736
739
  }))
737
740
  });
738
741