@equilateral_ai/mindmeld 3.1.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/hooks/README.md +46 -4
- package/hooks/pre-compact.js +115 -12
- package/hooks/session-end.js +292 -0
- package/hooks/session-start.js +302 -25
- package/package.json +5 -2
- package/scripts/auth-login.js +53 -0
- package/scripts/harvest.js +59 -19
- package/scripts/init-project.js +134 -374
- package/scripts/inject.js +30 -9
- package/scripts/repo-analyzer.js +870 -0
- package/src/core/AuthManager.js +498 -0
- package/src/core/CrossReferenceEngine.js +624 -0
- package/src/core/DeprecationScheduler.js +183 -0
- package/src/core/LLMPatternDetector.js +218 -0
- package/src/core/RapportOrchestrator.js +186 -0
- package/src/core/RelevanceDetector.js +32 -2
- package/src/core/StandardLifecycle.js +244 -0
- package/src/core/StandardsIngestion.js +341 -28
- package/src/core/parsers/adrParser.js +479 -0
- package/src/core/parsers/cursorRulesParser.js +564 -0
- package/src/core/parsers/eslintParser.js +439 -0
- package/src/handlers/alerts/alertsAcknowledge.js +4 -3
- package/src/handlers/analytics/activitySummaryGet.js +235 -0
- package/src/handlers/analytics/coachingGet.js +361 -0
- package/src/handlers/analytics/developerScoreGet.js +207 -0
- package/src/handlers/collaborators/collaboratorAdd.js +4 -5
- package/src/handlers/collaborators/collaboratorInvite.js +6 -5
- package/src/handlers/collaborators/collaboratorList.js +3 -3
- package/src/handlers/collaborators/collaboratorRemove.js +5 -4
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -11
- package/src/handlers/correlations/correlationsGet.js +1 -1
- package/src/handlers/correlations/correlationsProjectGet.js +7 -6
- package/src/handlers/enterprise/enterpriseAuditGet.js +108 -0
- package/src/handlers/enterprise/enterpriseContributorsGet.js +85 -0
- package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +53 -0
- package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +77 -0
- package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +71 -0
- package/src/handlers/enterprise/enterpriseKnowledgeGet.js +87 -0
- package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +122 -0
- package/src/handlers/enterprise/enterpriseOnboardingComplete.js +77 -0
- package/src/handlers/enterprise/enterpriseOnboardingInvite.js +138 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +89 -0
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +90 -0
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +264 -5
- package/src/handlers/github/githubOAuthCallback.js +14 -2
- package/src/handlers/github/githubOAuthStart.js +1 -1
- package/src/handlers/github/githubPatternsReview.js +1 -1
- package/src/handlers/github/githubReposList.js +1 -1
- package/src/handlers/helpers/auditLogger.js +201 -0
- package/src/handlers/helpers/index.js +19 -1
- package/src/handlers/helpers/lambdaWrapper.js +1 -1
- package/src/handlers/notifications/sendNotification.js +1 -1
- package/src/handlers/projects/projectCreate.js +28 -1
- package/src/handlers/projects/projectDelete.js +3 -3
- package/src/handlers/projects/projectUpdate.js +4 -5
- package/src/handlers/scheduled/analyzeCorrelations.js +3 -3
- package/src/handlers/scheduled/generateAlerts.js +1 -1
- package/src/handlers/standards/catalogGet.js +185 -0
- package/src/handlers/standards/catalogSync.js +120 -0
- package/src/handlers/standards/projectStandardsGet.js +135 -0
- package/src/handlers/standards/projectStandardsPut.js +131 -0
- package/src/handlers/standards/standardsAuditGet.js +65 -0
- package/src/handlers/standards/standardsParseUpload.js +153 -0
- package/src/handlers/standards/standardsRelevantPost.js +213 -0
- package/src/handlers/standards/standardsTransition.js +64 -0
- package/src/handlers/user/userSplashAck.js +91 -0
- package/src/handlers/user/userSplashGet.js +194 -0
- package/src/handlers/users/userProfilePut.js +77 -0
- package/src/index.js +37 -29
package/README.md
CHANGED
|
@@ -58,8 +58,8 @@ open https://mindmeld.dev/login
|
|
|
58
58
|
- < 500ms hook execution target
|
|
59
59
|
|
|
60
60
|
### 2. Standards Ingestion (Phase 1)
|
|
61
|
-
- Ingests 100+ patterns from .equilateral-standards/
|
|
62
|
-
-
|
|
61
|
+
- Ingests 100+ patterns from .equilateral-standards/ (YAML format)
|
|
62
|
+
- Parses structured rules, anti-patterns, examples, cost impacts
|
|
63
63
|
- Stores in `rapport.standards_patterns` database table
|
|
64
64
|
- Enables intelligent context injection (only relevant standards)
|
|
65
65
|
|
|
@@ -239,8 +239,8 @@ NODE_ENV, APP_URL
|
|
|
239
239
|
Rapport integrates with `.equilateral-standards/` to provide context-aware standards injection:
|
|
240
240
|
|
|
241
241
|
**What it does**:
|
|
242
|
-
- Ingests 100+ patterns from .equilateral-standards/
|
|
243
|
-
-
|
|
242
|
+
- Ingests 100+ patterns from .equilateral-standards/ YAML files
|
|
243
|
+
- Parses structured rules (ALWAYS/NEVER/USE/PREFER/AVOID), anti-patterns, examples, cost impacts
|
|
244
244
|
- Stores in `rapport.standards_patterns` database table
|
|
245
245
|
- Enables intelligent context injection (only relevant standards)
|
|
246
246
|
|
package/hooks/README.md
CHANGED
|
@@ -74,6 +74,31 @@ npm run test:pre-compact
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### 3. `session-end.js` - Session Completion
|
|
78
|
+
|
|
79
|
+
**Purpose**: Record session completion and outcomes when Claude Code exits
|
|
80
|
+
|
|
81
|
+
**Execution**: Called when Claude Code session terminates (exit command, logout, clear)
|
|
82
|
+
|
|
83
|
+
**Input** (stdin JSON from Claude Code):
|
|
84
|
+
- `session_id` - Session identifier
|
|
85
|
+
- `transcript_path` - Path to conversation JSONL
|
|
86
|
+
- `cwd` - Working directory
|
|
87
|
+
- `reason` - Exit reason (clear, logout, prompt_input_exit, other)
|
|
88
|
+
|
|
89
|
+
**Output**: API call to POST /api/sessions/end with session metadata
|
|
90
|
+
|
|
91
|
+
**Configuration**:
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"claudeCode": {
|
|
95
|
+
"hooks": {
|
|
96
|
+
"sessionEnd": "hooks/session-end.js"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
77
102
|
---
|
|
78
103
|
|
|
79
104
|
## How Hooks Work
|
|
@@ -110,6 +135,20 @@ npm run test:pre-compact
|
|
|
110
135
|
5. Return summary to Claude Code
|
|
111
136
|
```
|
|
112
137
|
|
|
138
|
+
### SessionEnd Flow
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
1. Claude Code sends session metadata via stdin
|
|
142
|
+
2. Hook checks if Rapport configured
|
|
143
|
+
3. If not configured → exit immediately
|
|
144
|
+
4. If configured:
|
|
145
|
+
a. Load auth token
|
|
146
|
+
b. Get project ID, git branch, session duration
|
|
147
|
+
c. POST /api/sessions/end (fire-and-forget, 3s timeout)
|
|
148
|
+
d. API records session completion and calculates compliance
|
|
149
|
+
5. Exit (never blocks session termination)
|
|
150
|
+
```
|
|
151
|
+
|
|
113
152
|
---
|
|
114
153
|
|
|
115
154
|
## Performance Characteristics
|
|
@@ -392,10 +431,13 @@ hooks/
|
|
|
392
431
|
│ ├── checkRapportConfiguration()
|
|
393
432
|
│ └── formatContextInjection()
|
|
394
433
|
│
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
434
|
+
├── pre-compact.js
|
|
435
|
+
│ ├── harvestPatterns()
|
|
436
|
+
│ ├── validatePatterns()
|
|
437
|
+
│ └── checkPromotionCandidates()
|
|
438
|
+
│
|
|
439
|
+
└── session-end.js
|
|
440
|
+
└── recordSessionEnd()
|
|
399
441
|
|
|
400
442
|
↓ Uses ↓
|
|
401
443
|
|
package/hooks/pre-compact.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const fs = require('fs').promises;
|
|
19
|
+
const crypto = require('crypto');
|
|
19
20
|
|
|
20
21
|
// LLM Pattern Detection (optional - requires ANTHROPIC_API_KEY)
|
|
21
22
|
let LLMPatternDetector = null;
|
|
@@ -26,6 +27,86 @@ try {
|
|
|
26
27
|
// LLM module not available - will use regex fallback
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Load auth token for API calls
|
|
32
|
+
* Priority: env var → project credentials.json → global ~/.mindmeld/auth.json
|
|
33
|
+
* @returns {Promise<string|null>} Auth token or null
|
|
34
|
+
*/
|
|
35
|
+
async function loadAuthToken() {
|
|
36
|
+
// 1. Env var (highest priority)
|
|
37
|
+
if (process.env.MINDMELD_AUTH_TOKEN) {
|
|
38
|
+
return process.env.MINDMELD_AUTH_TOKEN;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Project-level credentials.json
|
|
42
|
+
try {
|
|
43
|
+
const credPath = path.join(process.cwd(), '.mindmeld', 'credentials.json');
|
|
44
|
+
const content = await fs.readFile(credPath, 'utf-8');
|
|
45
|
+
const creds = JSON.parse(content);
|
|
46
|
+
if (creds.auth_token || creds.token) {
|
|
47
|
+
return creds.auth_token || creds.token;
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// No project-level credentials
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Global ~/.mindmeld/auth.json (from browser login / mindmeld CLI)
|
|
54
|
+
try {
|
|
55
|
+
const { AuthManager } = require('../src/core/AuthManager');
|
|
56
|
+
const cognitoConfig = await loadCognitoConfig();
|
|
57
|
+
const authManager = new AuthManager(cognitoConfig);
|
|
58
|
+
const token = await authManager.getValidToken();
|
|
59
|
+
if (token) {
|
|
60
|
+
return token;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load Cognito config from .myworld.json
|
|
70
|
+
* @returns {Promise<Object>} Cognito constructor options or empty object
|
|
71
|
+
*/
|
|
72
|
+
async function loadCognitoConfig() {
|
|
73
|
+
try {
|
|
74
|
+
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
75
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
76
|
+
const config = JSON.parse(content);
|
|
77
|
+
const auth = config.deployments?.backend?.auth;
|
|
78
|
+
if (auth?.domain && auth?.client_id) {
|
|
79
|
+
return {
|
|
80
|
+
cognitoDomain: `${auth.domain}.auth.us-east-2.amazoncognito.com`,
|
|
81
|
+
cognitoClientId: auth.client_id
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// No .myworld.json — use production defaults
|
|
86
|
+
}
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load API configuration from .myworld.json
|
|
92
|
+
* @returns {Promise<{apiUrl: string}>}
|
|
93
|
+
*/
|
|
94
|
+
async function loadApiConfig() {
|
|
95
|
+
try {
|
|
96
|
+
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
97
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
98
|
+
const config = JSON.parse(content);
|
|
99
|
+
const backend = config.deployments?.backend;
|
|
100
|
+
return {
|
|
101
|
+
apiUrl: backend?.api?.base_url || 'https://api.mindmeld.dev'
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
apiUrl: process.env.MINDMELD_API_URL || 'https://api.mindmeld.dev'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
29
110
|
/**
|
|
30
111
|
* Main hook execution
|
|
31
112
|
* @param {Object} sessionTranscript - Claude Code session data
|
|
@@ -40,11 +121,17 @@ async function harvestPatterns(sessionTranscript) {
|
|
|
40
121
|
return { skipped: true, reason: 'No MindMeld configuration' };
|
|
41
122
|
}
|
|
42
123
|
|
|
124
|
+
// Load auth token and API config for authenticated API calls
|
|
125
|
+
const authToken = await loadAuthToken();
|
|
126
|
+
const apiConfig = await loadApiConfig();
|
|
127
|
+
|
|
43
128
|
// Load MindmeldClient with graceful degradation
|
|
44
129
|
const { MindmeldClient } = require('../src/index');
|
|
45
130
|
|
|
46
131
|
const mindmeld = new MindmeldClient({
|
|
47
|
-
projectPath: process.cwd()
|
|
132
|
+
projectPath: process.cwd(),
|
|
133
|
+
authToken: authToken,
|
|
134
|
+
apiUrl: apiConfig.apiUrl
|
|
48
135
|
});
|
|
49
136
|
|
|
50
137
|
// Extract session metadata
|
|
@@ -200,7 +287,11 @@ async function checkMindmeldConfiguration() {
|
|
|
200
287
|
const mindmeldConfig = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
201
288
|
await fs.access(mindmeldConfig);
|
|
202
289
|
return true;
|
|
203
|
-
} catch {
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// Expected: config doesn't exist
|
|
292
|
+
if (error.code !== 'ENOENT') {
|
|
293
|
+
console.error('Unexpected error checking MindMeld config:', error.message);
|
|
294
|
+
}
|
|
204
295
|
return false;
|
|
205
296
|
}
|
|
206
297
|
}
|
|
@@ -248,10 +339,10 @@ async function checkPromotionCandidates(mindmeld, validPatterns) {
|
|
|
248
339
|
}
|
|
249
340
|
|
|
250
341
|
/**
|
|
251
|
-
* Generate session ID
|
|
342
|
+
* Generate session ID using crypto for consistency
|
|
252
343
|
*/
|
|
253
344
|
function generateSessionId() {
|
|
254
|
-
return `session_${Date.now()}_${
|
|
345
|
+
return `session_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`;
|
|
255
346
|
}
|
|
256
347
|
|
|
257
348
|
/**
|
|
@@ -261,8 +352,11 @@ function parseSessionTranscript(input) {
|
|
|
261
352
|
if (typeof input === 'string') {
|
|
262
353
|
try {
|
|
263
354
|
return JSON.parse(input);
|
|
264
|
-
} catch {
|
|
265
|
-
//
|
|
355
|
+
} catch (error) {
|
|
356
|
+
// Expected: not valid JSON, treat as raw transcript text
|
|
357
|
+
if (!(error instanceof SyntaxError)) {
|
|
358
|
+
console.error('Unexpected error parsing transcript:', error.message);
|
|
359
|
+
}
|
|
266
360
|
return {
|
|
267
361
|
transcript: input,
|
|
268
362
|
sessionId: generateSessionId()
|
|
@@ -329,11 +423,17 @@ async function generatePostCompactContext(summary, llmAnalysis) {
|
|
|
329
423
|
}
|
|
330
424
|
sections.push('');
|
|
331
425
|
}
|
|
332
|
-
} catch {
|
|
333
|
-
//
|
|
426
|
+
} catch (error) {
|
|
427
|
+
// Expected: no index file
|
|
428
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) {
|
|
429
|
+
console.error('Unexpected error reading standards index:', error.message);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
// Expected: no standards directory
|
|
434
|
+
if (error.code !== 'ENOENT') {
|
|
435
|
+
console.error('Unexpected error accessing standards:', error.message);
|
|
334
436
|
}
|
|
335
|
-
} catch {
|
|
336
|
-
// No standards directory - that's OK
|
|
337
437
|
}
|
|
338
438
|
|
|
339
439
|
// Load project-specific patterns from .mindmeld if available
|
|
@@ -349,8 +449,11 @@ async function generatePostCompactContext(summary, llmAnalysis) {
|
|
|
349
449
|
sections.push('**Team**: ' + config.team.map(t => t.name || t.email).join(', '));
|
|
350
450
|
}
|
|
351
451
|
sections.push('');
|
|
352
|
-
} catch {
|
|
353
|
-
//
|
|
452
|
+
} catch (error) {
|
|
453
|
+
// Expected: no mindmeld config
|
|
454
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) {
|
|
455
|
+
console.error('Unexpected error reading mindmeld config:', error.message);
|
|
456
|
+
}
|
|
354
457
|
}
|
|
355
458
|
|
|
356
459
|
sections.push('---');
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MindMeld - Claude Code Session End Hook
|
|
4
|
+
*
|
|
5
|
+
* Records session completion and outcomes when a Claude Code session ends.
|
|
6
|
+
* Calls POST /api/sessions/end with session metadata.
|
|
7
|
+
*
|
|
8
|
+
* Input (stdin JSON from Claude Code):
|
|
9
|
+
* { session_id, transcript_path, cwd, reason, hook_event_name }
|
|
10
|
+
*
|
|
11
|
+
* @equilateral_ai/mindmeld v3.3.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs').promises;
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load auth token for API calls
|
|
20
|
+
* Priority: env var → project credentials.json → global ~/.mindmeld/auth.json
|
|
21
|
+
* @returns {Promise<string|null>} Auth token or null
|
|
22
|
+
*/
|
|
23
|
+
async function loadAuthToken() {
|
|
24
|
+
// 1. Env var (highest priority)
|
|
25
|
+
if (process.env.MINDMELD_AUTH_TOKEN) {
|
|
26
|
+
return process.env.MINDMELD_AUTH_TOKEN;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Project-level credentials.json
|
|
30
|
+
try {
|
|
31
|
+
const credPath = path.join(process.cwd(), '.mindmeld', 'credentials.json');
|
|
32
|
+
const content = await fs.readFile(credPath, 'utf-8');
|
|
33
|
+
const creds = JSON.parse(content);
|
|
34
|
+
if (creds.auth_token || creds.token) {
|
|
35
|
+
return creds.auth_token || creds.token;
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// No project-level credentials
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Global ~/.mindmeld/auth.json (from browser login / mindmeld CLI)
|
|
42
|
+
try {
|
|
43
|
+
const { AuthManager } = require('../src/core/AuthManager');
|
|
44
|
+
const cognitoConfig = await loadCognitoConfig();
|
|
45
|
+
const authManager = new AuthManager(cognitoConfig);
|
|
46
|
+
const token = await authManager.getValidToken();
|
|
47
|
+
if (token) {
|
|
48
|
+
return token;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load Cognito config from .myworld.json
|
|
58
|
+
* @returns {Promise<Object>} Cognito constructor options or empty object
|
|
59
|
+
*/
|
|
60
|
+
async function loadCognitoConfig() {
|
|
61
|
+
try {
|
|
62
|
+
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
63
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
64
|
+
const config = JSON.parse(content);
|
|
65
|
+
const auth = config.deployments?.backend?.auth;
|
|
66
|
+
if (auth?.domain && auth?.client_id) {
|
|
67
|
+
return {
|
|
68
|
+
cognitoDomain: `${auth.domain}.auth.us-east-2.amazoncognito.com`,
|
|
69
|
+
cognitoClientId: auth.client_id
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// No .myworld.json — use production defaults
|
|
74
|
+
}
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load API configuration from .myworld.json
|
|
80
|
+
* @returns {Promise<{apiUrl: string}>}
|
|
81
|
+
*/
|
|
82
|
+
async function loadApiConfig() {
|
|
83
|
+
try {
|
|
84
|
+
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
85
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
86
|
+
const config = JSON.parse(content);
|
|
87
|
+
const backend = config.deployments?.backend;
|
|
88
|
+
return {
|
|
89
|
+
apiUrl: backend?.api?.base_url || 'https://api.mindmeld.dev'
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
apiUrl: process.env.MINDMELD_API_URL || 'https://api.mindmeld.dev'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get current git branch
|
|
100
|
+
* @param {string} cwd - Working directory
|
|
101
|
+
* @returns {string|null} Branch name or null
|
|
102
|
+
*/
|
|
103
|
+
function getGitBranch(cwd) {
|
|
104
|
+
try {
|
|
105
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
106
|
+
cwd,
|
|
107
|
+
encoding: 'utf-8',
|
|
108
|
+
timeout: 2000
|
|
109
|
+
}).trim();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Estimate session duration from transcript file creation time
|
|
117
|
+
* @param {string} transcriptPath - Path to session transcript
|
|
118
|
+
* @returns {Promise<number|null>} Duration in seconds or null
|
|
119
|
+
*/
|
|
120
|
+
async function estimateSessionDuration(transcriptPath) {
|
|
121
|
+
if (!transcriptPath) return null;
|
|
122
|
+
try {
|
|
123
|
+
const stat = await fs.stat(transcriptPath);
|
|
124
|
+
const createdAt = stat.birthtime || stat.ctime;
|
|
125
|
+
return Math.round((Date.now() - createdAt.getTime()) / 1000);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Load project ID from .mindmeld/config.json
|
|
133
|
+
* @returns {Promise<string|null>} Project ID or null
|
|
134
|
+
*/
|
|
135
|
+
async function loadProjectId() {
|
|
136
|
+
try {
|
|
137
|
+
const configPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
138
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
139
|
+
const config = JSON.parse(content);
|
|
140
|
+
return config.projectId || config.project_id || null;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read hook input from stdin
|
|
148
|
+
* @returns {Promise<Object>} Parsed JSON input
|
|
149
|
+
*/
|
|
150
|
+
function readStdin() {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
let data = '';
|
|
153
|
+
process.stdin.setEncoding('utf-8');
|
|
154
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
155
|
+
process.stdin.on('end', () => {
|
|
156
|
+
try {
|
|
157
|
+
resolve(JSON.parse(data));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
resolve({});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// If stdin is not piped, resolve immediately
|
|
163
|
+
if (process.stdin.isTTY) {
|
|
164
|
+
resolve({});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Main hook execution
|
|
171
|
+
* Records session end via API call (fire-and-forget)
|
|
172
|
+
*/
|
|
173
|
+
async function recordSessionEnd() {
|
|
174
|
+
const startTime = Date.now();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Fast bail if MindMeld not configured
|
|
178
|
+
try {
|
|
179
|
+
await fs.access(path.join(process.cwd(), '.mindmeld', 'config.json'));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return { skipped: true, reason: 'No MindMeld configuration' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Read hook input from stdin
|
|
185
|
+
const input = await readStdin();
|
|
186
|
+
const sessionId = input.session_id;
|
|
187
|
+
const transcriptPath = input.transcript_path;
|
|
188
|
+
const cwd = input.cwd || process.cwd();
|
|
189
|
+
const reason = input.reason;
|
|
190
|
+
|
|
191
|
+
if (!sessionId) {
|
|
192
|
+
console.error('[MindMeld] session-end: No session_id provided');
|
|
193
|
+
return { skipped: true, reason: 'No session_id' };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Load auth and config in parallel
|
|
197
|
+
const [authToken, apiConfig, projectId, duration, gitBranch] = await Promise.all([
|
|
198
|
+
loadAuthToken(),
|
|
199
|
+
loadApiConfig(),
|
|
200
|
+
loadProjectId(),
|
|
201
|
+
estimateSessionDuration(transcriptPath),
|
|
202
|
+
Promise.resolve(getGitBranch(cwd))
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
if (!authToken) {
|
|
206
|
+
console.error('[MindMeld] session-end: No auth token available, skipping');
|
|
207
|
+
return { skipped: true, reason: 'No auth token' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Build request payload
|
|
211
|
+
const payload = {
|
|
212
|
+
session_id: sessionId,
|
|
213
|
+
project_id: projectId,
|
|
214
|
+
user_id: process.env.USER || 'unknown',
|
|
215
|
+
duration_seconds: duration,
|
|
216
|
+
working_directory: cwd,
|
|
217
|
+
git_branch: gitBranch,
|
|
218
|
+
session_data: {
|
|
219
|
+
end_reason: reason,
|
|
220
|
+
hook_version: '3.3.0'
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Fire-and-forget API call with timeout
|
|
225
|
+
const url = `${apiConfig.apiUrl}/api/sessions/end`;
|
|
226
|
+
const http = require(url.startsWith('https') ? 'https' : 'http');
|
|
227
|
+
const urlObj = new URL(url);
|
|
228
|
+
|
|
229
|
+
const options = {
|
|
230
|
+
hostname: urlObj.hostname,
|
|
231
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
232
|
+
path: urlObj.pathname,
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: {
|
|
235
|
+
'Content-Type': 'application/json',
|
|
236
|
+
'Authorization': `Bearer ${authToken}`
|
|
237
|
+
},
|
|
238
|
+
timeout: 3000
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
await new Promise((resolve) => {
|
|
242
|
+
const req = http.request(options, (res) => {
|
|
243
|
+
let body = '';
|
|
244
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
245
|
+
res.on('end', () => {
|
|
246
|
+
const elapsed = Date.now() - startTime;
|
|
247
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
248
|
+
console.error(`[MindMeld] Session end recorded in ${elapsed}ms (session: ${sessionId}, reason: ${reason})`);
|
|
249
|
+
} else {
|
|
250
|
+
console.error(`[MindMeld] Session end API returned ${res.statusCode}: ${body}`);
|
|
251
|
+
}
|
|
252
|
+
resolve();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
req.on('error', (err) => {
|
|
257
|
+
console.error(`[MindMeld] Session end API error (non-fatal): ${err.message}`);
|
|
258
|
+
resolve(); // Never block exit
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
req.on('timeout', () => {
|
|
262
|
+
req.destroy();
|
|
263
|
+
console.error('[MindMeld] Session end API timeout (non-fatal)');
|
|
264
|
+
resolve(); // Never block exit
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
req.write(JSON.stringify(payload));
|
|
268
|
+
req.end();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
sessionId,
|
|
273
|
+
reason,
|
|
274
|
+
duration,
|
|
275
|
+
elapsed: Date.now() - startTime
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Graceful degradation - never block session exit
|
|
280
|
+
console.error('[MindMeld] session-end hook error (non-fatal):', error.message);
|
|
281
|
+
return { error: error.message };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Execute if called directly
|
|
286
|
+
if (require.main === module) {
|
|
287
|
+
recordSessionEnd()
|
|
288
|
+
.then(() => process.exit(0))
|
|
289
|
+
.catch(() => process.exit(0)); // Never block exit
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = { recordSessionEnd };
|