@unifiedmemory/cli 1.0.1 → 1.2.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/commands/init.js CHANGED
@@ -8,6 +8,52 @@ import { loadAndRefreshToken } from '../lib/token-validation.js';
8
8
  import { login } from './login.js';
9
9
  import { ProviderDetector } from '../lib/provider-detector.js';
10
10
 
11
+ /**
12
+ * Find project root directory by looking for common markers
13
+ * @param {string} startDir - Directory to start searching from
14
+ * @returns {string} Project root directory path
15
+ */
16
+ function findProjectRoot(startDir = process.cwd()) {
17
+ let currentDir = startDir;
18
+ const root = path.parse(currentDir).root;
19
+ let levelsUp = 0;
20
+ const MAX_DEPTH = 5; // Only walk up 5 levels to avoid finding distant markers
21
+
22
+ while (currentDir !== root && levelsUp < MAX_DEPTH) {
23
+ // Check for common project markers
24
+ const markers = [
25
+ path.join(currentDir, '.git'),
26
+ path.join(currentDir, 'package.json'),
27
+ path.join(currentDir, 'pyproject.toml'),
28
+ path.join(currentDir, 'Cargo.toml'),
29
+ path.join(currentDir, '.um'),
30
+ ];
31
+
32
+ // If any marker exists, this is the project root
33
+ for (const marker of markers) {
34
+ if (fs.existsSync(marker)) {
35
+ return currentDir;
36
+ }
37
+ }
38
+
39
+ // Move up one directory
40
+ currentDir = path.dirname(currentDir);
41
+ levelsUp++;
42
+ }
43
+
44
+ // Fallback: return the starting directory
45
+ return startDir;
46
+ }
47
+
48
+ /**
49
+ * Get default project name based on directory
50
+ * @returns {string} Default project name
51
+ */
52
+ function getDefaultProjectName() {
53
+ const projectRoot = findProjectRoot();
54
+ return path.basename(projectRoot);
55
+ }
56
+
11
57
  export async function init(options = {}) {
12
58
  console.log(chalk.cyan('\nšŸš€ UnifiedMemory Initialization\n'));
13
59
 
@@ -35,9 +81,15 @@ export async function init(options = {}) {
35
81
  // Step 3: Save project config
36
82
  await saveProjectConfig(authData, projectData);
37
83
 
84
+ // Step 3.5: Fetch available MCP tools for permissions
85
+ let mcpToolPermissions = null;
86
+ if (!options.skipConfigure) {
87
+ mcpToolPermissions = await fetchMCPToolPermissions(authData, projectData);
88
+ }
89
+
38
90
  // Step 4: Configure AI tools
39
91
  if (!options.skipConfigure) {
40
- await configureProviders(authData, projectData);
92
+ await configureProviders(authData, projectData, mcpToolPermissions);
41
93
  }
42
94
 
43
95
  console.log(chalk.green('\nāœ… Initialization complete!\n'));
@@ -47,6 +99,25 @@ export async function init(options = {}) {
47
99
  console.log(' 3. Run `um status` to verify configuration\n');
48
100
  }
49
101
 
102
+ /**
103
+ * Validate that we have a real organization context (not fallback to user_id)
104
+ * @param {Object} authData - Auth data with user_id and org_id
105
+ * @returns {Object} { isValid, isPersonalContext, message }
106
+ */
107
+ function validateOrganizationContext(authData) {
108
+ const isPersonalContext = authData.org_id === authData.user_id;
109
+
110
+ if (isPersonalContext) {
111
+ return {
112
+ isValid: false,
113
+ isPersonalContext: true,
114
+ message: 'You are in personal account context. Organization-scoped operations require an organization.',
115
+ };
116
+ }
117
+
118
+ return { isValid: true, isPersonalContext: false, message: null };
119
+ }
120
+
50
121
  async function ensureAuthenticated(options) {
51
122
  // Try to load and refresh token if expired
52
123
  const stored = await loadAndRefreshToken(false); // Don't throw, allow new login
@@ -70,12 +141,6 @@ async function ensureAuthenticated(options) {
70
141
 
71
142
  // Need to login
72
143
  console.log(chalk.yellow('⚠ Authentication required\n'));
73
-
74
- if (options.apiKey) {
75
- return await loginWithApiKey(options.apiKey);
76
- }
77
-
78
- // For now, just use OAuth
79
144
  console.log(chalk.blue('Initiating OAuth login...'));
80
145
  const tokenData = await login();
81
146
 
@@ -100,35 +165,129 @@ async function ensureAuthenticated(options) {
100
165
  async function selectOrCreateProject(authData, options) {
101
166
  const apiUrl = authData.api_url || 'https://rose-asp-main-1c0b114.d2.zuplo.dev';
102
167
 
103
- // Fetch existing projects
104
- const projects = await fetchProjects(authData, apiUrl);
105
-
106
- console.log(chalk.cyan('\nšŸ“‹ Project setup:\n'));
107
- console.log(chalk.green(` 1. Create new project`));
108
- console.log(chalk.green(` 2. Select existing project (${projects.length} available)`));
109
-
110
- const { selection } = await inquirer.prompt([{
111
- type: 'input',
112
- name: 'selection',
113
- message: 'Choose option (1-2):',
114
- default: projects.length > 0 ? '2' : '1',
115
- validate: (input) => {
116
- const num = parseInt(input, 10);
117
- if (isNaN(num) || num < 1 || num > 2) {
118
- return 'Please enter 1 or 2';
168
+ // Validate organization context BEFORE making API calls
169
+ const contextValidation = validateOrganizationContext(authData);
170
+
171
+ if (!contextValidation.isValid && contextValidation.isPersonalContext) {
172
+ console.log(chalk.yellow('\nāš ļø ' + contextValidation.message));
173
+ console.log(chalk.gray('\nOrganization-scoped projects require an organization context.'));
174
+ console.log(chalk.cyan('\nRecommended actions:'));
175
+ console.log(chalk.cyan(' 1. Switch to an organization: um org switch'));
176
+ console.log(chalk.cyan(' 2. Create an organization at: https://unifiedmemory.ai'));
177
+ console.log(chalk.cyan(' 3. Re-run: um init'));
178
+ return null;
179
+ }
180
+
181
+ // Fetch existing projects with enhanced error handling
182
+ console.log(chalk.gray('Fetching projects...'));
183
+ const result = await fetchProjects(authData, apiUrl);
184
+
185
+ // Handle error responses
186
+ if (!result.success) {
187
+ console.log(chalk.red(`\nāŒ Failed to fetch projects: ${result.message}`));
188
+
189
+ if (result.errorType === 'UNAUTHORIZED') {
190
+ console.log(chalk.yellow('\nšŸ” Authentication Issue Detected\n'));
191
+ console.log(chalk.gray('Token appears to be invalid or expired.'));
192
+ console.log(chalk.cyan('\nšŸ”„ Attempting automatic re-authentication...\n'));
193
+
194
+ try {
195
+ // Trigger login flow
196
+ const tokenData = await login();
197
+
198
+ if (!tokenData) {
199
+ console.log(chalk.red('āŒ Re-authentication failed'));
200
+ console.log(chalk.cyan('\nPlease try:'));
201
+ console.log(chalk.cyan(' 1. Run: um login'));
202
+ console.log(chalk.cyan(' 2. Verify organization context: um org switch'));
203
+ return null;
204
+ }
205
+
206
+ // Update authData with fresh credentials
207
+ const savedToken = getToken();
208
+ authData.user_id = savedToken.decoded.sub;
209
+ authData.org_id = savedToken.selectedOrg?.id || authData.user_id;
210
+ authData.access_token = savedToken.idToken || savedToken.accessToken;
211
+ authData.expires_at = savedToken.decoded?.exp * 1000;
212
+
213
+ console.log(chalk.green('āœ“ Re-authentication successful!\n'));
214
+ console.log(chalk.gray('Retrying project fetch...\n'));
215
+
216
+ // Retry fetching projects with new token
217
+ const retryResult = await fetchProjects(authData, apiUrl);
218
+
219
+ if (!retryResult.success) {
220
+ // Still failing after retry
221
+ console.log(chalk.red(`\nāŒ Failed to fetch projects after re-authentication: ${retryResult.message}`));
222
+ console.log(chalk.yellow('\nThis may indicate:'));
223
+ console.log(chalk.gray(' - You don\'t have access to this organization'));
224
+ console.log(chalk.gray(' - The organization no longer exists'));
225
+ console.log(chalk.cyan('\nTry: um org switch'));
226
+ return null;
227
+ }
228
+
229
+ // Success! Update projects and continue
230
+ result.success = true;
231
+ result.projects = retryResult.projects;
232
+ console.log(chalk.green(`āœ“ Found ${retryResult.projects.length} project(s)\n`));
233
+
234
+ } catch (error) {
235
+ console.log(chalk.red(`\nāŒ Re-authentication error: ${error.message}`));
236
+ console.log(chalk.cyan('\nPlease run: um login'));
237
+ return null;
119
238
  }
120
- return true;
239
+ } else if (result.errorType === 'FORBIDDEN') {
240
+ console.log(chalk.yellow('\nšŸ”’ Access Denied\n'));
241
+ console.log(chalk.gray('You do not have permission to view projects in this organization.'));
242
+ console.log(chalk.cyan('\nContact your organization administrator for access.'));
243
+ return null;
244
+ } else if (result.errorType === 'NETWORK_ERROR') {
245
+ console.log(chalk.yellow('\n🌐 Network Error\n'));
246
+ console.log(chalk.gray('Could not connect to the API server.'));
247
+ console.log(chalk.cyan('\nCheck your internet connection and try again.'));
248
+ return null;
249
+ } else {
250
+ console.log(chalk.yellow('\nāš ļø Unexpected Error\n'));
251
+ console.log(chalk.gray(`Details: ${result.message}`));
252
+ console.log(chalk.cyan('\nTry: um login'));
253
+ return null;
254
+ }
255
+ }
256
+
257
+ let projects = result.projects;
258
+
259
+ console.log(chalk.cyan('\nšŸ“‹ Project setup\n'));
260
+
261
+ const choices = [
262
+ {
263
+ name: chalk.green('Create new project'),
264
+ value: 'create',
265
+ short: 'Create new project',
266
+ },
267
+ {
268
+ name: chalk.green(`Select existing project`) + chalk.gray(` (${projects.length} available)`),
269
+ value: 'select',
270
+ short: 'Select existing project',
121
271
  },
272
+ ];
273
+
274
+ const { action } = await inquirer.prompt([{
275
+ type: 'select',
276
+ name: 'action',
277
+ message: 'Choose an option:',
278
+ choices: choices,
279
+ default: projects.length > 0 ? 1 : 0,
122
280
  }]);
123
281
 
124
- const action = parseInt(selection, 10) === 1 ? 'create' : 'select';
125
-
126
282
  if (action === 'create') {
283
+ const defaultName = getDefaultProjectName();
284
+
127
285
  const { name, description } = await inquirer.prompt([
128
286
  {
129
287
  type: 'input',
130
288
  name: 'name',
131
289
  message: 'Project name:',
290
+ default: defaultName,
132
291
  validate: input => input.length > 0 || 'Name is required',
133
292
  },
134
293
  {
@@ -142,32 +301,43 @@ async function selectOrCreateProject(authData, options) {
142
301
  } else {
143
302
  if (projects.length === 0) {
144
303
  console.log(chalk.yellow('No projects found. Creating first project...'));
145
- return await selectOrCreateProject(authData, { ...options, action: 'create' });
304
+ const defaultName = getDefaultProjectName();
305
+
306
+ const { name, description } = await inquirer.prompt([
307
+ {
308
+ type: 'input',
309
+ name: 'name',
310
+ message: 'Project name:',
311
+ default: defaultName,
312
+ validate: input => input.length > 0 || 'Name is required',
313
+ },
314
+ {
315
+ type: 'input',
316
+ name: 'description',
317
+ message: 'Project description (optional):',
318
+ },
319
+ ]);
320
+
321
+ return await createProject(authData, apiUrl, name, description);
146
322
  }
147
323
 
148
- // Display numbered list
149
- console.log(chalk.cyan('\nšŸ“‹ Available projects:\n'));
150
- projects.forEach((p, index) => {
151
- console.log(chalk.green(` ${index + 1}. ${p.display_name || p.name}`) + chalk.gray(` (${p.slug || p.id})`));
152
- });
153
-
154
- const { projectSelection } = await inquirer.prompt([{
155
- type: 'input',
156
- name: 'projectSelection',
157
- message: `Choose project (1-${projects.length}):`,
158
- default: '1',
159
- validate: (input) => {
160
- const num = parseInt(input, 10);
161
- if (isNaN(num) || num < 1 || num > projects.length) {
162
- return `Please enter a number between 1 and ${projects.length}`;
163
- }
164
- return true;
165
- },
324
+ // Display project list with arrow key selection
325
+ console.log(chalk.cyan('\nšŸ“‹ Available projects\n'));
326
+
327
+ const choices = projects.map(p => ({
328
+ name: chalk.green(p.display_name || p.name) + chalk.gray(` (${p.slug || p.id})`),
329
+ value: p,
330
+ short: p.display_name || p.name,
331
+ }));
332
+
333
+ const { project } = await inquirer.prompt([{
334
+ type: 'select',
335
+ name: 'project',
336
+ message: 'Choose a project:',
337
+ choices: choices,
338
+ pageSize: 10,
166
339
  }]);
167
340
 
168
- const selectedIndex = parseInt(projectSelection, 10) - 1;
169
- const project = projects[selectedIndex];
170
-
171
341
  return {
172
342
  project_id: project.id,
173
343
  project_name: project.display_name || project.name,
@@ -187,11 +357,47 @@ async function fetchProjects(authData, apiUrl) {
187
357
  },
188
358
  }
189
359
  );
190
- // API returns {items: [...], next_cursor, has_more}
191
- return response.data?.items || [];
360
+ return { success: true, projects: response.data?.items || [] };
192
361
  } catch (error) {
193
- console.error(chalk.red(`Failed to fetch projects: ${error.message}`));
194
- return [];
362
+ // Detect specific error types
363
+ if (error.response) {
364
+ const status = error.response.status;
365
+
366
+ if (status === 401) {
367
+ return {
368
+ success: false,
369
+ errorType: 'UNAUTHORIZED',
370
+ status: 401,
371
+ message: 'Authentication failed - token may be invalid or expired',
372
+ };
373
+ } else if (status === 403) {
374
+ return {
375
+ success: false,
376
+ errorType: 'FORBIDDEN',
377
+ status: 403,
378
+ message: 'Access denied - you may not have permission to view projects',
379
+ };
380
+ } else if (status >= 500) {
381
+ return {
382
+ success: false,
383
+ errorType: 'SERVER_ERROR',
384
+ status: status,
385
+ message: 'Server error - please try again later',
386
+ };
387
+ }
388
+ } else if (error.request) {
389
+ return {
390
+ success: false,
391
+ errorType: 'NETWORK_ERROR',
392
+ message: 'Network error - could not reach API server',
393
+ };
394
+ }
395
+
396
+ return {
397
+ success: false,
398
+ errorType: 'UNKNOWN',
399
+ message: error.message,
400
+ };
195
401
  }
196
402
  }
197
403
 
@@ -218,7 +424,27 @@ async function createProject(authData, apiUrl, name, description) {
218
424
  project_slug: project.slug,
219
425
  };
220
426
  } catch (error) {
221
- console.error(chalk.red(`Failed to create project: ${error.message}`));
427
+ if (error.response) {
428
+ const status = error.response.status;
429
+ console.error(chalk.red(`\nāŒ Failed to create project (HTTP ${status})`));
430
+
431
+ if (status === 401) {
432
+ console.log(chalk.yellow('šŸ” Authentication failed'));
433
+ console.log(chalk.gray('Your session may have expired.'));
434
+ console.log(chalk.cyan('Run: um login'));
435
+ } else if (status === 403) {
436
+ console.log(chalk.yellow('šŸ”’ Permission denied'));
437
+ console.log(chalk.gray('You do not have permission to create projects in this organization.'));
438
+ } else if (status === 409) {
439
+ console.log(chalk.yellow('āš ļø Project already exists'));
440
+ console.log(chalk.gray('A project with this name or slug already exists.'));
441
+ } else {
442
+ console.log(chalk.gray(`Details: ${error.message}`));
443
+ }
444
+ } else {
445
+ console.error(chalk.red(`\nāŒ Failed to create project: ${error.message}`));
446
+ }
447
+
222
448
  return null;
223
449
  }
224
450
  }
@@ -272,7 +498,66 @@ async function saveProjectConfig(authData, projectData) {
272
498
  }
273
499
  }
274
500
 
275
- async function configureProviders(authData, projectData) {
501
+ /**
502
+ * Fetch MCP tools and prompt user for permission to pre-authorize
503
+ * @param {Object} authData - Auth data with tokens
504
+ * @param {Object} projectData - Project data
505
+ * @returns {Promise<Array<string>|null>} - Array of permission strings or null if declined
506
+ */
507
+ async function fetchMCPToolPermissions(authData, projectData) {
508
+ try {
509
+ // Build auth headers (similar to lib/mcp-server.js)
510
+ const authHeaders = {
511
+ 'Authorization': `Bearer ${authData.access_token}`,
512
+ 'X-Org-Id': authData.org_id,
513
+ 'X-User-Id': authData.user_id,
514
+ };
515
+
516
+ // Fetch tools from gateway
517
+ const { fetchRemoteMCPTools } = await import('../lib/mcp-proxy.js');
518
+ const projectContext = {
519
+ project_id: projectData.project_id,
520
+ org_id: authData.org_id,
521
+ };
522
+
523
+ const toolsResult = await fetchRemoteMCPTools(authHeaders, projectContext);
524
+ const tools = toolsResult.tools || [];
525
+
526
+ if (tools.length === 0) {
527
+ return null; // No tools available
528
+ }
529
+
530
+ // Format tool names for display
531
+ const toolNames = tools.map(t => t.name).join(', ');
532
+
533
+ // Prompt user
534
+ console.log(chalk.cyan('\nšŸ”§ MCP Tool Permissions\n'));
535
+ console.log(chalk.gray(`Found ${tools.length} available tools: ${toolNames}`));
536
+
537
+ const { allowPermissions } = await inquirer.prompt([{
538
+ type: 'confirm',
539
+ name: 'allowPermissions',
540
+ message: 'Pre-authorize these UnifiedMemory tools in Claude Code? (Recommended)',
541
+ default: true,
542
+ }]);
543
+
544
+ if (!allowPermissions) {
545
+ console.log(chalk.yellow('⚠ Tools not pre-authorized. Claude will prompt for permission on first use.'));
546
+ return null;
547
+ }
548
+
549
+ // Convert tool names to permission format
550
+ const permissions = tools.map(tool => `mcp__unifiedmemory__${tool.name}`);
551
+ return permissions;
552
+
553
+ } catch (error) {
554
+ console.error(chalk.yellow(`⚠ Could not fetch MCP tools: ${error.message}`));
555
+ console.log(chalk.gray('Skipping permission setup. You can manually add permissions later.'));
556
+ return null;
557
+ }
558
+ }
559
+
560
+ async function configureProviders(authData, projectData, mcpToolPermissions = null) {
276
561
  console.log(chalk.cyan('\nšŸ”§ Configuring AI code assistants...\n'));
277
562
 
278
563
  // Pass current directory for project-level configs (like Claude Code)
@@ -288,28 +573,42 @@ async function configureProviders(authData, projectData) {
288
573
 
289
574
  // NEW APPROACH: Configure local MCP server (no tokens in config files)
290
575
  for (const provider of detected) {
291
- const success = provider.configureMCP();
576
+ // Configure MCP server (pass permissions for Claude Code)
577
+ const mcpSuccess = provider.configureMCP(mcpToolPermissions);
292
578
 
293
- if (success) {
294
- console.log(chalk.green(`āœ“ Configured ${provider.name}`));
579
+ // Configure memory instructions
580
+ const instructionsResult = provider.configureMemoryInstructions?.();
581
+
582
+ // Display results
583
+ if (mcpSuccess) {
584
+ console.log(chalk.green(`āœ“ Configured ${provider.name} MCP server`));
585
+
586
+ // Show permission status for Claude Code
587
+ if (provider.name === 'Claude Code' && mcpToolPermissions && mcpToolPermissions.length > 0) {
588
+ console.log(chalk.green(` āœ“ Pre-authorized ${mcpToolPermissions.length} MCP tools`));
589
+ }
295
590
  } else {
296
- console.log(chalk.red(`āœ— Failed to configure ${provider.name}`));
591
+ console.log(chalk.red(`āœ— Failed to configure ${provider.name} MCP`));
592
+ }
593
+
594
+ if (instructionsResult) {
595
+ switch (instructionsResult.status) {
596
+ case 'created':
597
+ console.log(chalk.green(` āœ“ Created memory instructions`));
598
+ break;
599
+ case 'appended':
600
+ console.log(chalk.green(` āœ“ Appended memory instructions`));
601
+ break;
602
+ case 'skipped':
603
+ console.log(chalk.gray(` ℹ Memory instructions already present`));
604
+ break;
605
+ case 'error':
606
+ console.log(chalk.yellow(` ⚠ Could not write memory instructions: ${instructionsResult.error}`));
607
+ break;
608
+ }
297
609
  }
298
610
  }
299
611
 
300
612
  console.log(chalk.cyan('\nšŸ’” Important: Restart your AI assistant to load the new configuration'));
301
613
  console.log(chalk.gray(' The MCP server will automatically use your authentication and project context'));
302
614
  }
303
-
304
- async function loginWithApiKey(apiKey) {
305
- // TODO: Implement API key authentication
306
- // Exchange API key for JWT token
307
- console.log(chalk.yellow('API key authentication not yet implemented'));
308
- return null;
309
- }
310
-
311
- async function refreshToken(refreshToken) {
312
- // TODO: Implement token refresh
313
- console.log(chalk.yellow('Token refresh not yet implemented'));
314
- return null;
315
- }
package/commands/login.js CHANGED
@@ -6,10 +6,13 @@ import crypto from 'crypto';
6
6
  import inquirer from 'inquirer';
7
7
  import { config, validateConfig } from '../lib/config.js';
8
8
  import { saveToken, updateSelectedOrg } from '../lib/token-storage.js';
9
- import { getUserOrganizations, getOrganizationsFromToken, formatOrganization, getOrgScopedToken } from '../lib/clerk-api.js';
9
+ import { getUserOrganizations, getOrganizationsFromToken, getOrgScopedToken } from '../lib/clerk-api.js';
10
+ import { parseJWT } from '../lib/jwt-utils.js';
11
+ import { promptOrganizationSelection, displayOrganizationSelection } from '../lib/org-selection-ui.js';
10
12
 
11
13
  function generateRandomState() {
12
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
14
+ // Use cryptographically secure random bytes for CSRF protection
15
+ return crypto.randomBytes(32).toString('hex');
13
16
  }
14
17
 
15
18
  function base64URLEncode(buffer) {
@@ -29,77 +32,6 @@ function generatePKCE() {
29
32
  return { verifier, challenge };
30
33
  }
31
34
 
32
- function parseJWT(token) {
33
- try {
34
- const parts = token.split('.');
35
- if (parts.length !== 3) {
36
- return null;
37
- }
38
- const payload = Buffer.from(parts[1], 'base64').toString('utf8');
39
- return JSON.parse(payload);
40
- } catch (error) {
41
- return null;
42
- }
43
- }
44
-
45
- /**
46
- * Prompt user to select an organization context
47
- * @param {Array} memberships - Array of organization memberships
48
- * @returns {Promise<Object|null>} Selected organization data or null for personal context
49
- */
50
- async function selectOrganization(memberships) {
51
- console.log(chalk.blue('\nšŸ” Checking for organizations...'));
52
-
53
- if (memberships.length === 0) {
54
- console.log(chalk.gray('No organizations found. Using personal account context.'));
55
- return null;
56
- }
57
-
58
- console.log(chalk.green(`\nFound ${memberships.length} organization(s)!`));
59
-
60
- // Debug: Log raw memberships
61
- console.log(chalk.gray('\nRaw memberships data:'));
62
- console.log(JSON.stringify(memberships, null, 2));
63
-
64
- // Format organizations for display
65
- const formattedOrgs = memberships.map(formatOrganization);
66
-
67
- // Print numbered list
68
- console.log(chalk.cyan('\nšŸ“‹ Available contexts:\n'));
69
-
70
- formattedOrgs.forEach((org, index) => {
71
- console.log(chalk.green(` ${index + 1}. ${org.name}`) + chalk.gray(` (${org.slug})`) + chalk.yellow(` [${org.role}]`));
72
- });
73
-
74
- console.log(chalk.cyan(` ${formattedOrgs.length + 1}. Personal Account`) + chalk.gray(' (no organization)'));
75
-
76
- // Simple input prompt
77
- const answer = await inquirer.prompt([
78
- {
79
- type: 'input',
80
- name: 'selection',
81
- message: `Choose context (1-${formattedOrgs.length + 1}):`,
82
- default: '1',
83
- validate: (input) => {
84
- const num = parseInt(input, 10);
85
- if (isNaN(num) || num < 1 || num > formattedOrgs.length + 1) {
86
- return `Please enter a number between 1 and ${formattedOrgs.length + 1}`;
87
- }
88
- return true;
89
- },
90
- },
91
- ]);
92
-
93
- const selectedIndex = parseInt(answer.selection, 10) - 1;
94
-
95
- // If they selected beyond orgs list, that's personal account (null)
96
- if (selectedIndex >= formattedOrgs.length) {
97
- return null;
98
- }
99
-
100
- return formattedOrgs[selectedIndex];
101
- }
102
-
103
35
  export async function login() {
104
36
  validateConfig();
105
37
 
@@ -224,30 +156,10 @@ export async function login() {
224
156
  </html>
225
157
  `);
226
158
 
227
- // Parse JWT to show user info - try both access_token and id_token
159
+ // Parse JWT for user info - do not log tokens
228
160
  const tokenToParse = tokenData.id_token || tokenData.access_token;
229
- console.log(chalk.gray('Parsing token type:'), tokenData.id_token ? 'id_token' : 'access_token');
230
-
231
161
  const decoded = parseJWT(tokenToParse);
232
162
 
233
- if (!decoded) {
234
- console.log(chalk.yellow('āš ļø Could not parse JWT token'));
235
- console.log(chalk.gray('Token preview:'), tokenToParse ? tokenToParse.substring(0, 50) + '...' : 'null');
236
- } else {
237
- // Debug: Show JWT claims to see what's available
238
- console.log(chalk.gray('\nJWT Claims:'));
239
- console.log(chalk.gray(JSON.stringify(decoded, null, 2)));
240
- }
241
-
242
- // Debug: Show token previews to understand format
243
- console.log(chalk.gray('\nToken previews:'));
244
- if (tokenData.access_token) {
245
- console.log(chalk.gray(' Access token:'), tokenData.access_token.substring(0, 50) + '...');
246
- }
247
- if (tokenData.id_token) {
248
- console.log(chalk.gray(' ID token:'), tokenData.id_token.substring(0, 50) + '...');
249
- }
250
-
251
163
  // Save token (save both access_token and id_token if available)
252
164
  saveToken({
253
165
  accessToken: tokenData.access_token,
@@ -289,7 +201,9 @@ export async function login() {
289
201
  memberships = await getUserOrganizations(userId, sessionToken);
290
202
  }
291
203
 
292
- const selectedOrg = await selectOrganization(memberships);
204
+ console.log(chalk.blue('\nšŸ” Checking for organizations...'));
205
+ const selectedOrg = await promptOrganizationSelection(memberships);
206
+ displayOrganizationSelection(selectedOrg);
293
207
 
294
208
  if (selectedOrg) {
295
209
  // Get org-scoped JWT from Clerk