@equilateral_ai/mindmeld 3.0.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equilateral_ai/mindmeld",
3
- "version": "3.0.0",
3
+ "version": "3.1.2",
4
4
  "description": "Intelligent standards injection for AI coding sessions - context-aware, self-documenting, scales to large codebases",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 (production)
32
+ cognito: {
33
+ domain: 'mindmeld-auth.auth.us-east-2.amazoncognito.com',
34
+ clientId: '1al1vftuoh55a8n08ea6kr8heq',
35
+ region: 'us-east-2'
36
+ },
37
+ // API settings (production)
38
+ api: {
39
+ baseUrl: 'https://api.mindmeld.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,32 @@ 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
+ // Company_ID is client_id + '_main' for personal workspaces
616
+ const companyId = `${clientId}_main`;
617
+ const projectResponse = await callApi('POST', '/api/projects', auth.id_token, {
618
+ Company_ID: companyId,
619
+ project_name: projectName
620
+ });
118
621
 
119
- // 3. Discover collaborators from git
120
- let collaborators = [];
622
+ if (projectResponse.success) {
623
+ backendProjectId = projectResponse.data?.data?.Records?.[0]?.project_id || projectId;
624
+ console.log(` Project registered: ${backendProjectId}\n`);
625
+ } else if (projectResponse.status === 409) {
626
+ // Project already exists, that's fine
627
+ backendProjectId = projectId;
628
+ console.log(` Project already registered: ${projectId}\n`);
629
+ } else {
630
+ console.log(` Warning: Could not register project: ${projectResponse.error}`);
631
+ console.log(' Continuing with local-only setup.\n');
632
+ }
633
+ }
121
634
 
635
+ // 7. Discover collaborators from git
636
+ let collaborators = [];
122
637
  try {
123
638
  const { stdout } = await execAsync(
124
639
  'git log --format="%an|%ae" | sort | uniq -c | sort -rn | head -10',
@@ -144,95 +659,86 @@ async function initProject(projectPath, options = {}) {
144
659
  }
145
660
 
146
661
  if (collaborators.length > 0) {
147
- console.log('📋 Discovered collaborators from git history:\n');
662
+ console.log('👥 Discovered collaborators from git history:\n');
148
663
  collaborators.forEach((c, i) => {
149
664
  console.log(` ${i + 1}. ${c.name} <${c.email}> (${c.commits} commits)`);
150
665
  });
151
666
  console.log('');
152
667
  }
153
- } catch (error) {
668
+ } catch {
154
669
  console.log('ℹ️ No git history found (not a git repo or no commits)\n');
155
670
  }
156
671
 
157
- // 4. Create .mindmeld directory
672
+ // 8. Create .mindmeld directory and config
158
673
  await fs.mkdir(mindmeldDir, { recursive: true });
159
674
 
160
- // 5. Create config.json
161
675
  const config = {
162
- projectId,
676
+ projectId: backendProjectId || projectId,
163
677
  projectName,
164
- created: new Date().toISOString(),
678
+ userEmail: auth.email,
679
+ clientId,
680
+ subscriptionTier: tier,
165
681
  collaborators,
166
- private: !options.team,
167
- team: options.team,
168
- externalUsersAllowed: false,
169
- mindmeldVersion: '3.0.0'
682
+ created: new Date().toISOString(),
683
+ mindmeldVersion: '3.1.0'
170
684
  };
171
685
 
172
686
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
173
687
 
174
688
  console.log('✅ MindMeld initialized!\n');
175
689
  console.log(` Config: ${configPath}`);
176
- console.log(` Project ID: ${projectId}`);
177
- console.log(` Mode: ${options.team ? 'Team' : 'Private'}`);
178
- console.log(` Collaborators: ${collaborators.length}`);
690
+ console.log(` Project ID: ${config.projectId}`);
691
+ console.log(` User: ${auth.email}`);
692
+ console.log(` Plan: ${tier}`);
179
693
  console.log('');
180
694
 
181
- // 6. Check for community standards
695
+ // 9. Check for community standards
182
696
  const standardsDir = path.join(projectPath, '.equilateral-standards');
183
-
184
697
  try {
185
698
  await fs.access(standardsDir);
186
699
  console.log('ℹ️ Community standards (.equilateral-standards) available');
187
700
  } 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/');
701
+ if (tier !== 'free') {
702
+ try {
703
+ await execAsync(
704
+ 'git clone https://github.com/Equilateral-AI/EquilateralAgents-Community-Standards.git .equilateral-standards',
705
+ { cwd: projectPath }
706
+ );
707
+ console.log('✅ Cloned community standards repo');
708
+ } catch {
709
+ console.log('ℹ️ Community standards not available (clone from:');
710
+ console.log(' https://github.com/Equilateral-AI/EquilateralAgents-Community-Standards)');
711
+ }
199
712
  }
200
713
  }
201
714
 
202
- // 7. Bootstrap: harvest patterns from git history and promote to standards
203
- await bootstrapFromHistory(projectPath, { team: options.team });
715
+ // 10. Bootstrap from git history
716
+ await bootstrapFromHistory(projectPath);
204
717
 
205
- // 8. Configure Claude Code hooks
718
+ // 11. Configure Claude Code hooks
206
719
  await configureClaudeHooks(projectPath);
207
720
 
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
721
+ // 12. Summary
216
722
  console.log('\n🚀 Next steps:');
217
723
  console.log(' 1. Start a Claude Code session in this project');
218
724
  console.log(' 2. MindMeld hooks will inject relevant standards automatically');
219
725
  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');
726
+ if (tier !== 'free') {
727
+ console.log(' 4. Visit https://app.mindmeld.dev to manage team standards');
222
728
  }
223
729
  console.log('');
224
730
  }
225
731
 
226
- /**
227
- * Bootstrap: scan 90 days of git history, detect patterns, promote to standards
228
- */
229
- async function bootstrapFromHistory(projectPath, options = {}) {
732
+ // ============================================================================
733
+ // Bootstrap & Hooks (unchanged)
734
+ // ============================================================================
735
+
736
+ async function bootstrapFromHistory(projectPath) {
230
737
  console.log('\n📊 Bootstrapping from git history...\n');
231
738
 
232
739
  try {
233
740
  const { getGitHistory, detectPatterns, savePatterns, promotePatterns, harvestPlans, promoteDecisions } = require('./harvest');
234
741
 
235
- // Get extended git history
236
742
  const gitHistory = await getGitHistory(projectPath, { since: '90d', commits: 50 });
237
743
  const patterns = detectPatterns(gitHistory);
238
744
 
@@ -242,10 +748,7 @@ async function bootstrapFromHistory(projectPath, options = {}) {
242
748
  return;
243
749
  }
244
750
 
245
- // Save raw patterns
246
751
  await savePatterns(projectPath, patterns);
247
-
248
- // Promote high-confidence patterns to provisional standards
249
752
  const promoted = await promotePatterns(projectPath, patterns, { threshold: 0.5 });
250
753
 
251
754
  console.log(` Detected ${patterns.length} pattern(s) from git history`);
@@ -256,7 +759,6 @@ async function bootstrapFromHistory(projectPath, options = {}) {
256
759
  }
257
760
  }
258
761
 
259
- // Harvest decisions from Claude Code plan files
260
762
  const planDecisions = await harvestPlans(projectPath);
261
763
  if (planDecisions.length > 0) {
262
764
  const promotedDecisions = await promoteDecisions(projectPath, planDecisions);
@@ -279,20 +781,14 @@ async function bootstrapFromHistory(projectPath, options = {}) {
279
781
  }
280
782
  }
281
783
 
282
- /**
283
- * Configure Claude Code hooks in .claude/settings.json
284
- * This wires up MindMeld's session-start and pre-compact hooks
285
- */
286
784
  async function configureClaudeHooks(projectPath) {
287
785
  const claudeDir = path.join(projectPath, '.claude');
288
786
  const settingsPath = path.join(claudeDir, 'settings.json');
289
787
 
290
- // Resolve hook paths from installed package location
291
788
  const packageRoot = path.resolve(__dirname, '..');
292
789
  const sessionStartHook = path.join(packageRoot, 'hooks', 'session-start.js');
293
790
  const preCompactHook = path.join(packageRoot, 'hooks', 'pre-compact.js');
294
791
 
295
- // Verify hooks exist
296
792
  try {
297
793
  await fs.access(sessionStartHook);
298
794
  await fs.access(preCompactHook);
@@ -302,50 +798,37 @@ async function configureClaudeHooks(projectPath) {
302
798
  return;
303
799
  }
304
800
 
305
- // MindMeld hook definitions
306
801
  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
- ]
802
+ SessionStart: [{
803
+ hooks: [{
804
+ type: 'command',
805
+ command: `node "${sessionStartHook}"`,
806
+ timeout: 5
807
+ }]
808
+ }],
809
+ PreCompact: [{
810
+ hooks: [{
811
+ type: 'command',
812
+ command: `node "${preCompactHook}"`,
813
+ timeout: 30
814
+ }]
815
+ }]
329
816
  };
330
817
 
331
- // Create .claude directory
332
818
  await fs.mkdir(claudeDir, { recursive: true });
333
819
 
334
- // Load existing settings if present
335
820
  let settings = {};
336
821
  try {
337
822
  const existing = await fs.readFile(settingsPath, 'utf-8');
338
823
  settings = JSON.parse(existing);
339
824
  } catch {
340
- // No existing settings, start fresh
825
+ // No existing settings
341
826
  }
342
827
 
343
- // Merge hooks (preserve existing non-MindMeld hooks)
344
828
  if (!settings.hooks) {
345
829
  settings.hooks = {};
346
830
  }
347
831
 
348
- // Check if MindMeld hooks already configured
349
832
  const hasSessionStart = (settings.hooks.SessionStart || []).some(h =>
350
833
  h.hooks?.some(hk => hk.command?.includes('mindmeld') || hk.command?.includes('session-start'))
351
834
  );
@@ -358,7 +841,6 @@ async function configureClaudeHooks(projectPath) {
358
841
  return;
359
842
  }
360
843
 
361
- // Add MindMeld hooks (append to existing, don't replace)
362
844
  if (!hasSessionStart) {
363
845
  settings.hooks.SessionStart = [
364
846
  ...(settings.hooks.SessionStart || []),
@@ -379,6 +861,39 @@ async function configureClaudeHooks(projectPath) {
379
861
  console.log(` PreCompact: ${preCompactHook}`);
380
862
  }
381
863
 
864
+ // ============================================================================
865
+ // Utility Commands
866
+ // ============================================================================
867
+
868
+ async function showStatus() {
869
+ const auth = loadAuth();
870
+
871
+ console.log('\n🎯 MindMeld Status\n');
872
+
873
+ if (!auth) {
874
+ console.log(' Not authenticated.');
875
+ console.log(' Run "mindmeld login" to authenticate.\n');
876
+ return;
877
+ }
878
+
879
+ console.log(` Email: ${auth.email}`);
880
+ console.log(` Token expires: ${new Date(auth.expires_at).toLocaleString()}`);
881
+ console.log(` Token status: ${isTokenExpired(auth.expires_at) ? 'Expired' : 'Valid'}`);
882
+ console.log(` Refresh token: ${auth.refresh_token ? 'Present' : 'Missing'}`);
883
+ console.log('');
884
+ }
885
+
886
+ async function logout() {
887
+ await clearAuth();
888
+ console.log('\n✅ Logged out successfully.\n');
889
+ }
890
+
891
+ async function login() {
892
+ console.log('\n🎯 MindMeld Login\n');
893
+ const auth = await browserAuth();
894
+ console.log(`\n✅ Logged in as ${auth.email}\n`);
895
+ }
896
+
382
897
  async function promptYesNo(question) {
383
898
  process.stdout.write(question);
384
899
 
@@ -390,7 +905,10 @@ async function promptYesNo(question) {
390
905
  });
391
906
  }
392
907
 
908
+ // ============================================================================
393
909
  // Main
910
+ // ============================================================================
911
+
394
912
  const args = parseArgs(process.argv.slice(2));
395
913
 
396
914
  if (args.help && !args.command) {
@@ -400,7 +918,7 @@ if (args.help && !args.command) {
400
918
 
401
919
  if (args.command === 'init') {
402
920
  if (args.help) { showHelp(); process.exit(0); }
403
- initProject(args.projectPath, { team: args.team })
921
+ initProject(args.projectPath)
404
922
  .then(() => process.exit(0))
405
923
  .catch(error => {
406
924
  console.error('\n❌ Error:', error.message);
@@ -408,10 +926,7 @@ if (args.command === 'init') {
408
926
  });
409
927
  } else if (args.command === 'inject') {
410
928
  const { inject, showInjectHelp } = require('./inject');
411
- if (args.help) {
412
- showInjectHelp();
413
- process.exit(0);
414
- }
929
+ if (args.help) { showInjectHelp(); process.exit(0); }
415
930
  inject({ format: args.format, path: args.projectPath })
416
931
  .then(() => process.exit(0))
417
932
  .catch(error => {
@@ -420,16 +935,34 @@ if (args.command === 'init') {
420
935
  });
421
936
  } else if (args.command === 'harvest') {
422
937
  const { harvest, showHarvestHelp } = require('./harvest');
423
- if (args.help) {
424
- showHarvestHelp();
425
- process.exit(0);
426
- }
938
+ if (args.help) { showHarvestHelp(); process.exit(0); }
427
939
  harvest({ path: args.projectPath, since: args.since, commits: args.commits, dryRun: args.dryRun })
428
940
  .then(() => process.exit(0))
429
941
  .catch(error => {
430
942
  console.error('\n❌ Error:', error.message);
431
943
  process.exit(1);
432
944
  });
945
+ } else if (args.command === 'logout') {
946
+ logout()
947
+ .then(() => process.exit(0))
948
+ .catch(error => {
949
+ console.error('\n❌ Error:', error.message);
950
+ process.exit(1);
951
+ });
952
+ } else if (args.command === 'login') {
953
+ login()
954
+ .then(() => process.exit(0))
955
+ .catch(error => {
956
+ console.error('\n❌ Error:', error.message);
957
+ process.exit(1);
958
+ });
959
+ } else if (args.command === 'status') {
960
+ showStatus()
961
+ .then(() => process.exit(0))
962
+ .catch(error => {
963
+ console.error('\n❌ Error:', error.message);
964
+ process.exit(1);
965
+ });
433
966
  } else {
434
967
  console.error(`Unknown command: ${args.command}`);
435
968
  console.error('Run "mindmeld --help" for usage.');