@equilateral_ai/mindmeld 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equilateral_ai/mindmeld",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Intelligent standards injection for AI coding sessions - context-aware, self-documenting, scales to large codebases",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,28 +1,475 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * MindMeld CLI - Initialize project for intelligent standards injection
3
+ * MindMeld CLI - Intelligent standards injection for AI coding sessions
4
4
  *
5
5
  * Usage:
6
- * mindmeld init # Initialize for solo/private use
7
- * mindmeld init --team # Initialize with team collaboration
8
- * mindmeld init /path/to/project
6
+ * mindmeld init # Initialize project (requires authentication)
7
+ * mindmeld inject # Generate context-aware standards
8
+ * mindmeld harvest # Capture patterns from git history
9
+ * mindmeld logout # Clear stored authentication
9
10
  *
10
- * @equilateral_ai/mindmeld v3.0.0
11
+ * @equilateral_ai/mindmeld v3.1.0
11
12
  */
12
13
 
13
14
  const fs = require('fs').promises;
15
+ const fsSync = require('fs');
14
16
  const path = require('path');
17
+ const http = require('http');
18
+ const https = require('https');
19
+ const crypto = require('crypto');
15
20
  const { exec } = require('child_process');
16
21
  const { promisify } = require('util');
22
+ const os = require('os');
17
23
 
18
24
  const execAsync = promisify(exec);
19
25
 
20
- // Parse CLI arguments
26
+ // ============================================================================
27
+ // Configuration
28
+ // ============================================================================
29
+
30
+ const CONFIG = {
31
+ // Cognito settings (dev - production API is currently broken)
32
+ cognito: {
33
+ domain: 'mindmeld-users.auth.us-east-2.amazoncognito.com',
34
+ clientId: '6uif970sisfpvk5r6vg17uvf8r',
35
+ region: 'us-east-2'
36
+ },
37
+ // API settings (using direct API Gateway URL - custom domain has SSL issues)
38
+ api: {
39
+ baseUrl: 'https://u16nv4eqib.execute-api.us-east-2.amazonaws.com/dev'
40
+ },
41
+ // Local callback ports (fixed range for security)
42
+ callbackPorts: [9876, 9877, 9878, 9879, 9880],
43
+ // Auth file location
44
+ authFile: path.join(os.homedir(), '.mindmeld', 'auth.json')
45
+ };
46
+
47
+ // ============================================================================
48
+ // Authentication Module
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Load stored authentication
53
+ */
54
+ function loadAuth() {
55
+ try {
56
+ const data = fsSync.readFileSync(CONFIG.authFile, 'utf-8');
57
+ return JSON.parse(data);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Save authentication to disk
65
+ */
66
+ async function saveAuth(auth) {
67
+ const dir = path.dirname(CONFIG.authFile);
68
+ await fs.mkdir(dir, { recursive: true });
69
+ await fs.writeFile(CONFIG.authFile, JSON.stringify(auth, null, 2));
70
+ }
71
+
72
+ /**
73
+ * Clear stored authentication
74
+ */
75
+ async function clearAuth() {
76
+ try {
77
+ await fs.unlink(CONFIG.authFile);
78
+ } catch {
79
+ // File doesn't exist, that's fine
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Decode JWT payload (without verification - Cognito already verified)
85
+ */
86
+ function decodeJwt(token) {
87
+ const parts = token.split('.');
88
+ if (parts.length !== 3) throw new Error('Invalid JWT');
89
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
90
+ return JSON.parse(payload);
91
+ }
92
+
93
+ /**
94
+ * Check if token is expired (with 5 min buffer)
95
+ */
96
+ function isTokenExpired(expiresAt) {
97
+ return Date.now() > (expiresAt - 5 * 60 * 1000);
98
+ }
99
+
100
+ /**
101
+ * Refresh tokens using refresh token
102
+ */
103
+ async function refreshTokens(refreshToken) {
104
+ const tokenUrl = `https://${CONFIG.cognito.domain}/oauth2/token`;
105
+
106
+ const params = new URLSearchParams({
107
+ grant_type: 'refresh_token',
108
+ client_id: CONFIG.cognito.clientId,
109
+ refresh_token: refreshToken
110
+ });
111
+
112
+ return new Promise((resolve, reject) => {
113
+ const req = https.request(tokenUrl, {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/x-www-form-urlencoded'
117
+ }
118
+ }, (res) => {
119
+ let data = '';
120
+ res.on('data', chunk => data += chunk);
121
+ res.on('end', () => {
122
+ if (res.statusCode !== 200) {
123
+ reject(new Error('Token refresh failed'));
124
+ return;
125
+ }
126
+ try {
127
+ const tokens = JSON.parse(data);
128
+ const decoded = decodeJwt(tokens.id_token);
129
+ resolve({
130
+ id_token: tokens.id_token,
131
+ access_token: tokens.access_token,
132
+ refresh_token: refreshToken, // Cognito doesn't return new refresh token
133
+ expires_at: Date.now() + (tokens.expires_in * 1000),
134
+ email: decoded.email
135
+ });
136
+ } catch (e) {
137
+ reject(e);
138
+ }
139
+ });
140
+ });
141
+
142
+ req.on('error', reject);
143
+ req.write(params.toString());
144
+ req.end();
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Find an available port from the allowed list
150
+ */
151
+ async function findAvailablePort() {
152
+ for (const port of CONFIG.callbackPorts) {
153
+ const available = await new Promise((resolve) => {
154
+ const server = http.createServer();
155
+ server.listen(port, '127.0.0.1');
156
+ server.on('listening', () => {
157
+ server.close();
158
+ resolve(true);
159
+ });
160
+ server.on('error', () => {
161
+ resolve(false);
162
+ });
163
+ });
164
+ if (available) return port;
165
+ }
166
+ throw new Error('No available ports for OAuth callback');
167
+ }
168
+
169
+ /**
170
+ * Start local server to receive OAuth callback
171
+ */
172
+ function startCallbackServer(port, expectedState) {
173
+ return new Promise((resolve, reject) => {
174
+ const server = http.createServer((req, res) => {
175
+ const url = new URL(req.url, `http://localhost:${port}`);
176
+
177
+ if (url.pathname === '/callback') {
178
+ const code = url.searchParams.get('code');
179
+ const state = url.searchParams.get('state');
180
+ const error = url.searchParams.get('error');
181
+
182
+ if (error) {
183
+ res.writeHead(200, { 'Content-Type': 'text/html' });
184
+ res.end(`
185
+ <html><body style="font-family: system-ui; padding: 40px; text-align: center;">
186
+ <h1>Authentication Failed</h1>
187
+ <p>Error: ${error}</p>
188
+ <p>You can close this window.</p>
189
+ </body></html>
190
+ `);
191
+ server.close();
192
+ reject(new Error(`OAuth error: ${error}`));
193
+ return;
194
+ }
195
+
196
+ if (state !== expectedState) {
197
+ res.writeHead(400, { 'Content-Type': 'text/html' });
198
+ res.end(`
199
+ <html><body style="font-family: system-ui; padding: 40px; text-align: center;">
200
+ <h1>Authentication Failed</h1>
201
+ <p>Invalid state parameter (possible CSRF attack)</p>
202
+ <p>You can close this window.</p>
203
+ </body></html>
204
+ `);
205
+ server.close();
206
+ reject(new Error('Invalid state parameter'));
207
+ return;
208
+ }
209
+
210
+ res.writeHead(200, { 'Content-Type': 'text/html' });
211
+ res.end(`
212
+ <html><body style="font-family: system-ui; padding: 40px; text-align: center;">
213
+ <h1>Authentication Successful!</h1>
214
+ <p>You can close this window and return to your terminal.</p>
215
+ </body></html>
216
+ `);
217
+ server.close();
218
+ resolve(code);
219
+ } else {
220
+ res.writeHead(404);
221
+ res.end('Not found');
222
+ }
223
+ });
224
+
225
+ server.listen(port, '127.0.0.1');
226
+
227
+ // Timeout after 5 minutes
228
+ setTimeout(() => {
229
+ server.close();
230
+ reject(new Error('Authentication timed out'));
231
+ }, 5 * 60 * 1000);
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Exchange authorization code for tokens
237
+ */
238
+ async function exchangeCodeForTokens(code, codeVerifier, port) {
239
+ const tokenUrl = `https://${CONFIG.cognito.domain}/oauth2/token`;
240
+ const redirectUri = `http://localhost:${port}/callback`;
241
+
242
+ const params = new URLSearchParams({
243
+ grant_type: 'authorization_code',
244
+ client_id: CONFIG.cognito.clientId,
245
+ code: code,
246
+ redirect_uri: redirectUri,
247
+ code_verifier: codeVerifier
248
+ });
249
+
250
+ return new Promise((resolve, reject) => {
251
+ const req = https.request(tokenUrl, {
252
+ method: 'POST',
253
+ headers: {
254
+ 'Content-Type': 'application/x-www-form-urlencoded'
255
+ }
256
+ }, (res) => {
257
+ let data = '';
258
+ res.on('data', chunk => data += chunk);
259
+ res.on('end', () => {
260
+ if (res.statusCode !== 200) {
261
+ reject(new Error(`Token exchange failed: ${data}`));
262
+ return;
263
+ }
264
+ try {
265
+ const tokens = JSON.parse(data);
266
+ const decoded = decodeJwt(tokens.id_token);
267
+ resolve({
268
+ id_token: tokens.id_token,
269
+ access_token: tokens.access_token,
270
+ refresh_token: tokens.refresh_token,
271
+ expires_at: Date.now() + (tokens.expires_in * 1000),
272
+ email: decoded.email
273
+ });
274
+ } catch (e) {
275
+ reject(e);
276
+ }
277
+ });
278
+ });
279
+
280
+ req.on('error', reject);
281
+ req.write(params.toString());
282
+ req.end();
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Open browser for authentication
288
+ */
289
+ async function openBrowser(url) {
290
+ const platform = process.platform;
291
+ let command;
292
+
293
+ if (platform === 'darwin') {
294
+ command = `open "${url}"`;
295
+ } else if (platform === 'win32') {
296
+ command = `start "" "${url}"`;
297
+ } else {
298
+ command = `xdg-open "${url}"`;
299
+ }
300
+
301
+ try {
302
+ await execAsync(command);
303
+ return true;
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Full browser authentication flow (PKCE)
311
+ */
312
+ async function browserAuth() {
313
+ // Generate PKCE challenge
314
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
315
+ const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
316
+ const state = crypto.randomBytes(16).toString('hex');
317
+
318
+ // Find available port
319
+ const port = await findAvailablePort();
320
+ const redirectUri = `http://localhost:${port}/callback`;
321
+
322
+ // Build Cognito authorization URL
323
+ const authUrl = `https://${CONFIG.cognito.domain}/login?` + new URLSearchParams({
324
+ client_id: CONFIG.cognito.clientId,
325
+ response_type: 'code',
326
+ scope: 'openid email profile',
327
+ redirect_uri: redirectUri,
328
+ state: state,
329
+ code_challenge: codeChallenge,
330
+ code_challenge_method: 'S256'
331
+ }).toString();
332
+
333
+ // Start callback server
334
+ const codePromise = startCallbackServer(port, state);
335
+
336
+ // Open browser
337
+ console.log('\n🔐 Opening browser for authentication...');
338
+ console.log(` Callback server listening on port ${port}`);
339
+
340
+ const opened = await openBrowser(authUrl);
341
+
342
+ if (!opened) {
343
+ console.log('\n Could not open browser automatically.');
344
+ }
345
+
346
+ console.log('\n If login page doesn\'t appear, paste this URL in your browser:\n');
347
+ console.log(` ${authUrl}\n`);
348
+
349
+ console.log(' Waiting for authentication (5 minute timeout)...\n');
350
+
351
+ // Wait for callback
352
+ const code = await codePromise;
353
+
354
+ // Exchange code for tokens
355
+ const tokens = await exchangeCodeForTokens(code, codeVerifier, port);
356
+ await saveAuth(tokens);
357
+
358
+ return tokens;
359
+ }
360
+
361
+ /**
362
+ * Ensure user is authenticated (main entry point)
363
+ */
364
+ async function ensureAuth() {
365
+ let auth = loadAuth();
366
+
367
+ // Check if we have valid tokens
368
+ if (auth?.id_token && !isTokenExpired(auth.expires_at)) {
369
+ return auth;
370
+ }
371
+
372
+ // Try silent refresh
373
+ if (auth?.refresh_token) {
374
+ try {
375
+ console.log('🔄 Refreshing authentication...');
376
+ const newAuth = await refreshTokens(auth.refresh_token);
377
+ await saveAuth(newAuth);
378
+ return newAuth;
379
+ } catch (e) {
380
+ console.log(' Refresh failed, need to re-authenticate.\n');
381
+ }
382
+ }
383
+
384
+ // Full browser auth
385
+ return await browserAuth();
386
+ }
387
+
388
+ // ============================================================================
389
+ // API Module
390
+ // ============================================================================
391
+
392
+ /**
393
+ * Make authenticated API call
394
+ */
395
+ function callApi(method, endpoint, token, body = null) {
396
+ // Construct full URL - endpoint is relative, baseUrl may include path like /dev
397
+ const baseUrl = new URL(CONFIG.api.baseUrl);
398
+ const fullPath = baseUrl.pathname.replace(/\/$/, '') + endpoint;
399
+
400
+ return new Promise((resolve, reject) => {
401
+ const options = {
402
+ method,
403
+ hostname: baseUrl.hostname,
404
+ path: fullPath,
405
+ headers: {
406
+ 'Authorization': `Bearer ${token}`,
407
+ 'Content-Type': 'application/json'
408
+ }
409
+ };
410
+
411
+ const req = https.request(options, (res) => {
412
+ let data = '';
413
+ res.on('data', chunk => data += chunk);
414
+ res.on('end', () => {
415
+ try {
416
+ const parsed = JSON.parse(data);
417
+ if (res.statusCode >= 200 && res.statusCode < 300) {
418
+ resolve({ success: true, data: parsed });
419
+ } else {
420
+ resolve({ success: false, error: parsed.message || parsed.error || 'API error', status: res.statusCode });
421
+ }
422
+ } catch {
423
+ resolve({ success: false, error: data, status: res.statusCode });
424
+ }
425
+ });
426
+ });
427
+
428
+ req.on('error', (e) => {
429
+ resolve({ success: false, error: e.message });
430
+ });
431
+
432
+ if (body) {
433
+ req.write(JSON.stringify(body));
434
+ }
435
+ req.end();
436
+ });
437
+ }
438
+
439
+ /**
440
+ * Poll for subscription activation after Stripe checkout
441
+ */
442
+ async function pollForSubscription(token, maxAttempts = 60) {
443
+ console.log('\n⏳ Waiting for subscription activation...');
444
+ console.log(' (Complete the checkout in your browser)\n');
445
+
446
+ for (let i = 0; i < maxAttempts; i++) {
447
+ await new Promise(r => setTimeout(r, 3000)); // Wait 3 seconds
448
+
449
+ const response = await callApi('GET', '/api/users/me', token);
450
+ if (response.success && response.data?.Records?.[0]) {
451
+ const user = response.data.Records[0];
452
+ const tier = user.subscription?.tier || user.client?.subscription_tier || 'free';
453
+ if (tier !== 'free') {
454
+ console.log(`✅ Subscription activated: ${tier}\n`);
455
+ return tier;
456
+ }
457
+ }
458
+ process.stdout.write('.');
459
+ }
460
+
461
+ console.log('\n⚠️ Subscription not detected. You can continue setup and it will sync later.\n');
462
+ return 'free';
463
+ }
464
+
465
+ // ============================================================================
466
+ // CLI Arguments
467
+ // ============================================================================
468
+
21
469
  function parseArgs(args) {
22
470
  const parsed = {
23
471
  command: null,
24
472
  projectPath: null,
25
- team: false,
26
473
  format: 'raw',
27
474
  since: '7d',
28
475
  commits: 10,
@@ -32,19 +479,17 @@ function parseArgs(args) {
32
479
 
33
480
  for (let i = 0; i < args.length; i++) {
34
481
  const arg = args[i];
35
- if (arg === '--team') parsed.team = true;
36
- else if (arg === '--dry-run') parsed.dryRun = true;
482
+ if (arg === '--dry-run') parsed.dryRun = true;
37
483
  else if (arg === '--help' || arg === '-h') parsed.help = true;
38
484
  else if (arg === '--format' && args[i + 1]) { parsed.format = args[++i]; }
39
485
  else if (arg === '--path' && args[i + 1]) { parsed.projectPath = args[++i]; }
40
486
  else if (arg === '--since' && args[i + 1]) { parsed.since = args[++i]; }
41
487
  else if (arg === '--commits' && args[i + 1]) { parsed.commits = parseInt(args[++i]); }
42
- else if (['init', 'inject', 'harvest'].includes(arg)) parsed.command = arg;
488
+ else if (['init', 'inject', 'harvest', 'logout', 'login', 'status'].includes(arg)) parsed.command = arg;
43
489
  else if (!arg.startsWith('-') && !parsed.command) parsed.command = arg;
44
490
  else if (!arg.startsWith('-') && !parsed.projectPath) parsed.projectPath = arg;
45
491
  }
46
492
 
47
- // Default command is 'init'
48
493
  if (!parsed.command) parsed.command = 'init';
49
494
 
50
495
  return parsed;
@@ -58,24 +503,25 @@ Usage:
58
503
  mindmeld <command> [options]
59
504
 
60
505
  Commands:
61
- init [path] Initialize project for MindMeld
506
+ init [path] Initialize project for MindMeld (requires login)
62
507
  inject Generate context-aware standards for any AI tool
63
508
  harvest Manually capture patterns from recent git history
509
+ login Authenticate with MindMeld
510
+ logout Clear stored authentication
511
+ status Show current authentication status
64
512
 
65
513
  Options:
66
- --team Enable team collaboration (init only)
67
514
  --format <type> Output format: raw, cursorrules, windsurfrules, aider, claude
68
515
  --path <dir> Project path (default: current directory)
69
516
  --help, -h Show this help message
70
517
 
71
518
  Examples:
72
- mindmeld init --team # Init with team collaboration
73
- mindmeld inject --format cursorrules # Update .cursorrules
74
- mindmeld inject --format windsurfrules # Update .windsurfrules
75
- mindmeld inject --format aider # Update aider conventions
76
- mindmeld inject # Raw markdown to stdout
77
- mindmeld inject | ollama run qwen3-coder # Pipe to local model
78
- mindmeld harvest # Capture patterns from git diff
519
+ mindmeld init # Initialize with authentication
520
+ mindmeld inject --format cursorrules # Update .cursorrules
521
+ mindmeld inject --format windsurfrules # Update .windsurfrules
522
+ mindmeld inject # Raw markdown to stdout
523
+ mindmeld harvest # Capture patterns from git diff
524
+ mindmeld status # Check auth status
79
525
 
80
526
  Works with: Claude Code, Cursor, Windsurf, Codex CLI, Aider, Ollama, LM Studio
81
527
 
@@ -83,18 +529,68 @@ Learn more: https://mindmeld.dev
83
529
  `);
84
530
  }
85
531
 
86
- async function initProject(projectPath, options = {}) {
532
+ // ============================================================================
533
+ // Init Command
534
+ // ============================================================================
535
+
536
+ async function initProject(projectPath) {
87
537
  projectPath = projectPath || process.cwd();
88
538
 
89
- console.log(`\n🎯 Initializing MindMeld for: ${projectPath}\n`);
539
+ console.log('\n🎯 MindMeld CLI\n');
540
+
541
+ // 1. Authenticate
542
+ const auth = await ensureAuth();
543
+ console.log(`✅ Authenticated as ${auth.email}\n`);
544
+
545
+ // 2. Check subscription
546
+ const userResponse = await callApi('GET', '/api/users/me', auth.id_token);
547
+ if (!userResponse.success) {
548
+ throw new Error(`Failed to get user info: ${userResponse.error}`);
549
+ }
550
+
551
+ const user = userResponse.data.data?.Records?.[0];
552
+ if (!user) {
553
+ throw new Error('User not found. Please sign up at https://app.mindmeld.dev');
554
+ }
555
+
556
+ let tier = user.subscription?.tier || user.client?.subscription_tier || 'free';
557
+ const clientId = user.client_id;
558
+
559
+ // 3. Handle free tier - prompt for upgrade
560
+ if (tier === 'free') {
561
+ console.log('📋 Current plan: Free\n');
562
+ console.log(' MindMeld requires a subscription for full features.');
563
+ console.log(' Free tier provides local-only pattern storage.\n');
564
+
565
+ const upgrade = await promptYesNo(' Would you like to upgrade now? (y/n): ');
566
+
567
+ if (upgrade) {
568
+ // Create checkout session
569
+ const checkoutResponse = await callApi('POST', '/api/stripe/subscription/create', auth.id_token, {
570
+ tier: 'team',
571
+ contributionMode: 'contributing'
572
+ });
90
573
 
91
- if (options.team) {
92
- console.log(' Mode: Team collaboration\n');
574
+ if (checkoutResponse.success && checkoutResponse.data?.data?.url) {
575
+ console.log('\n🛒 Opening checkout...');
576
+ await openBrowser(checkoutResponse.data.data.url);
577
+ tier = await pollForSubscription(auth.id_token);
578
+ } else {
579
+ console.log(`\n⚠️ Could not create checkout: ${checkoutResponse.error}`);
580
+ console.log(' Visit https://app.mindmeld.dev to subscribe.\n');
581
+ }
582
+ } else {
583
+ console.log('\n Continuing with free tier (local-only mode).\n');
584
+ }
93
585
  } else {
94
- console.log(' Mode: Private (solo)\n');
586
+ console.log(`📋 Current plan: ${tier}\n`);
95
587
  }
96
588
 
97
- // 1. Check if already initialized
589
+ // 4. Get project info
590
+ const projectName = path.basename(projectPath);
591
+ const projectId = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
592
+
593
+ // 5. Check if already initialized
98
594
  const mindmeldDir = path.join(projectPath, '.mindmeld');
99
595
  const configPath = path.join(mindmeldDir, 'config.json');
100
596
 
@@ -103,7 +599,7 @@ async function initProject(projectPath, options = {}) {
103
599
  console.log('⚠️ Project already initialized!');
104
600
  console.log(` Config exists at: ${configPath}\n`);
105
601
 
106
- const answer = await promptYesNo('Reinitialize? (y/n): ');
602
+ const answer = await promptYesNo(' Reinitialize? (y/n): ');
107
603
  if (!answer) {
108
604
  console.log('Aborted.');
109
605
  process.exit(0);
@@ -112,13 +608,30 @@ async function initProject(projectPath, options = {}) {
112
608
  // Not initialized, continue
113
609
  }
114
610
 
115
- // 2. Get project name
116
- const projectName = path.basename(projectPath);
117
- const projectId = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
611
+ // 6. Create project on backend (if subscribed)
612
+ let backendProjectId = null;
613
+ if (tier !== 'free') {
614
+ console.log('📡 Registering project with MindMeld...');
615
+ const projectResponse = await callApi('POST', '/api/projects', auth.id_token, {
616
+ Company_ID: clientId,
617
+ project_name: projectName
618
+ });
118
619
 
119
- // 3. Discover collaborators from git
120
- let collaborators = [];
620
+ if (projectResponse.success) {
621
+ backendProjectId = projectResponse.data?.data?.Records?.[0]?.project_id || projectId;
622
+ console.log(` Project registered: ${backendProjectId}\n`);
623
+ } else if (projectResponse.status === 409) {
624
+ // Project already exists, that's fine
625
+ backendProjectId = projectId;
626
+ console.log(` Project already registered: ${projectId}\n`);
627
+ } else {
628
+ console.log(` Warning: Could not register project: ${projectResponse.error}`);
629
+ console.log(' Continuing with local-only setup.\n');
630
+ }
631
+ }
121
632
 
633
+ // 7. Discover collaborators from git
634
+ let collaborators = [];
122
635
  try {
123
636
  const { stdout } = await execAsync(
124
637
  'git log --format="%an|%ae" | sort | uniq -c | sort -rn | head -10',
@@ -144,95 +657,86 @@ async function initProject(projectPath, options = {}) {
144
657
  }
145
658
 
146
659
  if (collaborators.length > 0) {
147
- console.log('📋 Discovered collaborators from git history:\n');
660
+ console.log('👥 Discovered collaborators from git history:\n');
148
661
  collaborators.forEach((c, i) => {
149
662
  console.log(` ${i + 1}. ${c.name} <${c.email}> (${c.commits} commits)`);
150
663
  });
151
664
  console.log('');
152
665
  }
153
- } catch (error) {
666
+ } catch {
154
667
  console.log('ℹ️ No git history found (not a git repo or no commits)\n');
155
668
  }
156
669
 
157
- // 4. Create .mindmeld directory
670
+ // 8. Create .mindmeld directory and config
158
671
  await fs.mkdir(mindmeldDir, { recursive: true });
159
672
 
160
- // 5. Create config.json
161
673
  const config = {
162
- projectId,
674
+ projectId: backendProjectId || projectId,
163
675
  projectName,
164
- created: new Date().toISOString(),
676
+ userEmail: auth.email,
677
+ clientId,
678
+ subscriptionTier: tier,
165
679
  collaborators,
166
- private: !options.team,
167
- team: options.team,
168
- externalUsersAllowed: false,
169
- mindmeldVersion: '3.0.0'
680
+ created: new Date().toISOString(),
681
+ mindmeldVersion: '3.1.0'
170
682
  };
171
683
 
172
684
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
173
685
 
174
686
  console.log('✅ MindMeld initialized!\n');
175
687
  console.log(` Config: ${configPath}`);
176
- console.log(` Project ID: ${projectId}`);
177
- console.log(` Mode: ${options.team ? 'Team' : 'Private'}`);
178
- console.log(` Collaborators: ${collaborators.length}`);
688
+ console.log(` Project ID: ${config.projectId}`);
689
+ console.log(` User: ${auth.email}`);
690
+ console.log(` Plan: ${tier}`);
179
691
  console.log('');
180
692
 
181
- // 6. Check for community standards
693
+ // 9. Check for community standards
182
694
  const standardsDir = path.join(projectPath, '.equilateral-standards');
183
-
184
695
  try {
185
696
  await fs.access(standardsDir);
186
697
  console.log('ℹ️ Community standards (.equilateral-standards) available');
187
698
  } catch {
188
- // Try to clone the community standards repo
189
- try {
190
- await execAsync(
191
- 'git clone https://github.com/Equilateral-AI/EquilateralAgents-Community-Standards.git .equilateral-standards',
192
- { cwd: projectPath }
193
- );
194
- console.log('✅ Cloned community standards repo');
195
- } catch {
196
- console.log('ℹ️ Community standards not available (clone from:');
197
- console.log(' https://github.com/Equilateral-AI/EquilateralAgents-Community-Standards)');
198
- console.log(' Local patterns will still be captured in .mindmeld/');
699
+ if (tier !== 'free') {
700
+ try {
701
+ await execAsync(
702
+ 'git clone https://github.com/Equilateral-AI/EquilateralAgents-Community-Standards.git .equilateral-standards',
703
+ { cwd: projectPath }
704
+ );
705
+ console.log('✅ Cloned community standards repo');
706
+ } catch {
707
+ console.log('ℹ️ Community standards not available (clone from:');
708
+ console.log(' https://github.com/Equilateral-AI/EquilateralAgents-Community-Standards)');
709
+ }
199
710
  }
200
711
  }
201
712
 
202
- // 7. Bootstrap: harvest patterns from git history and promote to standards
203
- await bootstrapFromHistory(projectPath, { team: options.team });
713
+ // 10. Bootstrap from git history
714
+ await bootstrapFromHistory(projectPath);
204
715
 
205
- // 8. Configure Claude Code hooks
716
+ // 11. Configure Claude Code hooks
206
717
  await configureClaudeHooks(projectPath);
207
718
 
208
- // 9. Register with API if team mode
209
- if (options.team) {
210
- console.log('\n📡 Team mode enabled.');
211
- console.log(' Sign in at https://app.mindmeld.dev to connect this project.');
212
- console.log(' Your coding sessions will contribute to team-wide standards.\n');
213
- }
214
-
215
- // 10. Summary
719
+ // 12. Summary
216
720
  console.log('\n🚀 Next steps:');
217
721
  console.log(' 1. Start a Claude Code session in this project');
218
722
  console.log(' 2. MindMeld hooks will inject relevant standards automatically');
219
723
  console.log(' 3. Patterns from your sessions will be harvested and validated');
220
- if (options.team) {
221
- console.log(' 4. Sign in at https://app.mindmeld.dev to manage team standards');
724
+ if (tier !== 'free') {
725
+ console.log(' 4. Visit https://app.mindmeld.dev to manage team standards');
222
726
  }
223
727
  console.log('');
224
728
  }
225
729
 
226
- /**
227
- * Bootstrap: scan 90 days of git history, detect patterns, promote to standards
228
- */
229
- async function bootstrapFromHistory(projectPath, options = {}) {
730
+ // ============================================================================
731
+ // Bootstrap & Hooks (unchanged)
732
+ // ============================================================================
733
+
734
+ async function bootstrapFromHistory(projectPath) {
230
735
  console.log('\n📊 Bootstrapping from git history...\n');
231
736
 
232
737
  try {
233
738
  const { getGitHistory, detectPatterns, savePatterns, promotePatterns, harvestPlans, promoteDecisions } = require('./harvest');
234
739
 
235
- // Get extended git history
236
740
  const gitHistory = await getGitHistory(projectPath, { since: '90d', commits: 50 });
237
741
  const patterns = detectPatterns(gitHistory);
238
742
 
@@ -242,10 +746,7 @@ async function bootstrapFromHistory(projectPath, options = {}) {
242
746
  return;
243
747
  }
244
748
 
245
- // Save raw patterns
246
749
  await savePatterns(projectPath, patterns);
247
-
248
- // Promote high-confidence patterns to provisional standards
249
750
  const promoted = await promotePatterns(projectPath, patterns, { threshold: 0.5 });
250
751
 
251
752
  console.log(` Detected ${patterns.length} pattern(s) from git history`);
@@ -256,7 +757,6 @@ async function bootstrapFromHistory(projectPath, options = {}) {
256
757
  }
257
758
  }
258
759
 
259
- // Harvest decisions from Claude Code plan files
260
760
  const planDecisions = await harvestPlans(projectPath);
261
761
  if (planDecisions.length > 0) {
262
762
  const promotedDecisions = await promoteDecisions(projectPath, planDecisions);
@@ -279,20 +779,14 @@ async function bootstrapFromHistory(projectPath, options = {}) {
279
779
  }
280
780
  }
281
781
 
282
- /**
283
- * Configure Claude Code hooks in .claude/settings.json
284
- * This wires up MindMeld's session-start and pre-compact hooks
285
- */
286
782
  async function configureClaudeHooks(projectPath) {
287
783
  const claudeDir = path.join(projectPath, '.claude');
288
784
  const settingsPath = path.join(claudeDir, 'settings.json');
289
785
 
290
- // Resolve hook paths from installed package location
291
786
  const packageRoot = path.resolve(__dirname, '..');
292
787
  const sessionStartHook = path.join(packageRoot, 'hooks', 'session-start.js');
293
788
  const preCompactHook = path.join(packageRoot, 'hooks', 'pre-compact.js');
294
789
 
295
- // Verify hooks exist
296
790
  try {
297
791
  await fs.access(sessionStartHook);
298
792
  await fs.access(preCompactHook);
@@ -302,50 +796,37 @@ async function configureClaudeHooks(projectPath) {
302
796
  return;
303
797
  }
304
798
 
305
- // MindMeld hook definitions
306
799
  const mindmeldHooks = {
307
- SessionStart: [
308
- {
309
- hooks: [
310
- {
311
- type: 'command',
312
- command: `node "${sessionStartHook}"`,
313
- timeout: 5
314
- }
315
- ]
316
- }
317
- ],
318
- PreCompact: [
319
- {
320
- hooks: [
321
- {
322
- type: 'command',
323
- command: `node "${preCompactHook}"`,
324
- timeout: 30
325
- }
326
- ]
327
- }
328
- ]
800
+ SessionStart: [{
801
+ hooks: [{
802
+ type: 'command',
803
+ command: `node "${sessionStartHook}"`,
804
+ timeout: 5
805
+ }]
806
+ }],
807
+ PreCompact: [{
808
+ hooks: [{
809
+ type: 'command',
810
+ command: `node "${preCompactHook}"`,
811
+ timeout: 30
812
+ }]
813
+ }]
329
814
  };
330
815
 
331
- // Create .claude directory
332
816
  await fs.mkdir(claudeDir, { recursive: true });
333
817
 
334
- // Load existing settings if present
335
818
  let settings = {};
336
819
  try {
337
820
  const existing = await fs.readFile(settingsPath, 'utf-8');
338
821
  settings = JSON.parse(existing);
339
822
  } catch {
340
- // No existing settings, start fresh
823
+ // No existing settings
341
824
  }
342
825
 
343
- // Merge hooks (preserve existing non-MindMeld hooks)
344
826
  if (!settings.hooks) {
345
827
  settings.hooks = {};
346
828
  }
347
829
 
348
- // Check if MindMeld hooks already configured
349
830
  const hasSessionStart = (settings.hooks.SessionStart || []).some(h =>
350
831
  h.hooks?.some(hk => hk.command?.includes('mindmeld') || hk.command?.includes('session-start'))
351
832
  );
@@ -358,7 +839,6 @@ async function configureClaudeHooks(projectPath) {
358
839
  return;
359
840
  }
360
841
 
361
- // Add MindMeld hooks (append to existing, don't replace)
362
842
  if (!hasSessionStart) {
363
843
  settings.hooks.SessionStart = [
364
844
  ...(settings.hooks.SessionStart || []),
@@ -379,6 +859,39 @@ async function configureClaudeHooks(projectPath) {
379
859
  console.log(` PreCompact: ${preCompactHook}`);
380
860
  }
381
861
 
862
+ // ============================================================================
863
+ // Utility Commands
864
+ // ============================================================================
865
+
866
+ async function showStatus() {
867
+ const auth = loadAuth();
868
+
869
+ console.log('\n🎯 MindMeld Status\n');
870
+
871
+ if (!auth) {
872
+ console.log(' Not authenticated.');
873
+ console.log(' Run "mindmeld login" to authenticate.\n');
874
+ return;
875
+ }
876
+
877
+ console.log(` Email: ${auth.email}`);
878
+ console.log(` Token expires: ${new Date(auth.expires_at).toLocaleString()}`);
879
+ console.log(` Token status: ${isTokenExpired(auth.expires_at) ? 'Expired' : 'Valid'}`);
880
+ console.log(` Refresh token: ${auth.refresh_token ? 'Present' : 'Missing'}`);
881
+ console.log('');
882
+ }
883
+
884
+ async function logout() {
885
+ await clearAuth();
886
+ console.log('\n✅ Logged out successfully.\n');
887
+ }
888
+
889
+ async function login() {
890
+ console.log('\n🎯 MindMeld Login\n');
891
+ const auth = await browserAuth();
892
+ console.log(`\n✅ Logged in as ${auth.email}\n`);
893
+ }
894
+
382
895
  async function promptYesNo(question) {
383
896
  process.stdout.write(question);
384
897
 
@@ -390,7 +903,10 @@ async function promptYesNo(question) {
390
903
  });
391
904
  }
392
905
 
906
+ // ============================================================================
393
907
  // Main
908
+ // ============================================================================
909
+
394
910
  const args = parseArgs(process.argv.slice(2));
395
911
 
396
912
  if (args.help && !args.command) {
@@ -400,7 +916,7 @@ if (args.help && !args.command) {
400
916
 
401
917
  if (args.command === 'init') {
402
918
  if (args.help) { showHelp(); process.exit(0); }
403
- initProject(args.projectPath, { team: args.team })
919
+ initProject(args.projectPath)
404
920
  .then(() => process.exit(0))
405
921
  .catch(error => {
406
922
  console.error('\n❌ Error:', error.message);
@@ -408,10 +924,7 @@ if (args.command === 'init') {
408
924
  });
409
925
  } else if (args.command === 'inject') {
410
926
  const { inject, showInjectHelp } = require('./inject');
411
- if (args.help) {
412
- showInjectHelp();
413
- process.exit(0);
414
- }
927
+ if (args.help) { showInjectHelp(); process.exit(0); }
415
928
  inject({ format: args.format, path: args.projectPath })
416
929
  .then(() => process.exit(0))
417
930
  .catch(error => {
@@ -420,16 +933,34 @@ if (args.command === 'init') {
420
933
  });
421
934
  } else if (args.command === 'harvest') {
422
935
  const { harvest, showHarvestHelp } = require('./harvest');
423
- if (args.help) {
424
- showHarvestHelp();
425
- process.exit(0);
426
- }
936
+ if (args.help) { showHarvestHelp(); process.exit(0); }
427
937
  harvest({ path: args.projectPath, since: args.since, commits: args.commits, dryRun: args.dryRun })
428
938
  .then(() => process.exit(0))
429
939
  .catch(error => {
430
940
  console.error('\n❌ Error:', error.message);
431
941
  process.exit(1);
432
942
  });
943
+ } else if (args.command === 'logout') {
944
+ logout()
945
+ .then(() => process.exit(0))
946
+ .catch(error => {
947
+ console.error('\n❌ Error:', error.message);
948
+ process.exit(1);
949
+ });
950
+ } else if (args.command === 'login') {
951
+ login()
952
+ .then(() => process.exit(0))
953
+ .catch(error => {
954
+ console.error('\n❌ Error:', error.message);
955
+ process.exit(1);
956
+ });
957
+ } else if (args.command === 'status') {
958
+ showStatus()
959
+ .then(() => process.exit(0))
960
+ .catch(error => {
961
+ console.error('\n❌ Error:', error.message);
962
+ process.exit(1);
963
+ });
433
964
  } else {
434
965
  console.error(`Unknown command: ${args.command}`);
435
966
  console.error('Run "mindmeld --help" for usage.');