@unifiedmemory/cli 1.0.0 ā 1.1.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/.env.example +27 -6
- package/CHANGELOG.md +232 -0
- package/README.md +207 -132
- package/commands/init.js +246 -69
- package/commands/login.js +9 -95
- package/commands/org.js +9 -38
- package/index.js +17 -26
- package/lib/config.js +42 -24
- package/lib/jwt-utils.js +63 -0
- package/lib/mcp-server.js +1 -1
- package/lib/memory-instructions.js +72 -0
- package/lib/org-selection-ui.js +104 -0
- package/lib/provider-detector.js +69 -75
- package/lib/token-refresh.js +1 -18
- package/lib/token-storage.js +15 -2
- package/lib/welcome.js +40 -0
- package/package.json +6 -4
- package/HOOK_SETUP.md +0 -338
- package/lib/hooks.js +0 -43
package/commands/init.js
CHANGED
|
@@ -47,6 +47,25 @@ export async function init(options = {}) {
|
|
|
47
47
|
console.log(' 3. Run `um status` to verify configuration\n');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Validate that we have a real organization context (not fallback to user_id)
|
|
52
|
+
* @param {Object} authData - Auth data with user_id and org_id
|
|
53
|
+
* @returns {Object} { isValid, isPersonalContext, message }
|
|
54
|
+
*/
|
|
55
|
+
function validateOrganizationContext(authData) {
|
|
56
|
+
const isPersonalContext = authData.org_id === authData.user_id;
|
|
57
|
+
|
|
58
|
+
if (isPersonalContext) {
|
|
59
|
+
return {
|
|
60
|
+
isValid: false,
|
|
61
|
+
isPersonalContext: true,
|
|
62
|
+
message: 'You are in personal account context. Organization-scoped operations require an organization.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { isValid: true, isPersonalContext: false, message: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
async function ensureAuthenticated(options) {
|
|
51
70
|
// Try to load and refresh token if expired
|
|
52
71
|
const stored = await loadAndRefreshToken(false); // Don't throw, allow new login
|
|
@@ -70,12 +89,6 @@ async function ensureAuthenticated(options) {
|
|
|
70
89
|
|
|
71
90
|
// Need to login
|
|
72
91
|
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
92
|
console.log(chalk.blue('Initiating OAuth login...'));
|
|
80
93
|
const tokenData = await login();
|
|
81
94
|
|
|
@@ -100,29 +113,120 @@ async function ensureAuthenticated(options) {
|
|
|
100
113
|
async function selectOrCreateProject(authData, options) {
|
|
101
114
|
const apiUrl = authData.api_url || 'https://rose-asp-main-1c0b114.d2.zuplo.dev';
|
|
102
115
|
|
|
103
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
// Validate organization context BEFORE making API calls
|
|
117
|
+
const contextValidation = validateOrganizationContext(authData);
|
|
118
|
+
|
|
119
|
+
if (!contextValidation.isValid && contextValidation.isPersonalContext) {
|
|
120
|
+
console.log(chalk.yellow('\nā ļø ' + contextValidation.message));
|
|
121
|
+
console.log(chalk.gray('\nOrganization-scoped projects require an organization context.'));
|
|
122
|
+
console.log(chalk.cyan('\nRecommended actions:'));
|
|
123
|
+
console.log(chalk.cyan(' 1. Switch to an organization: um org switch'));
|
|
124
|
+
console.log(chalk.cyan(' 2. Create an organization at: https://unifiedmemory.ai'));
|
|
125
|
+
console.log(chalk.cyan(' 3. Re-run: um init'));
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fetch existing projects with enhanced error handling
|
|
130
|
+
console.log(chalk.gray('Fetching projects...'));
|
|
131
|
+
const result = await fetchProjects(authData, apiUrl);
|
|
132
|
+
|
|
133
|
+
// Handle error responses
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
console.log(chalk.red(`\nā Failed to fetch projects: ${result.message}`));
|
|
136
|
+
|
|
137
|
+
if (result.errorType === 'UNAUTHORIZED') {
|
|
138
|
+
console.log(chalk.yellow('\nš Authentication Issue Detected\n'));
|
|
139
|
+
console.log(chalk.gray('Token appears to be invalid or expired.'));
|
|
140
|
+
console.log(chalk.cyan('\nš Attempting automatic re-authentication...\n'));
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Trigger login flow
|
|
144
|
+
const tokenData = await login();
|
|
145
|
+
|
|
146
|
+
if (!tokenData) {
|
|
147
|
+
console.log(chalk.red('ā Re-authentication failed'));
|
|
148
|
+
console.log(chalk.cyan('\nPlease try:'));
|
|
149
|
+
console.log(chalk.cyan(' 1. Run: um login'));
|
|
150
|
+
console.log(chalk.cyan(' 2. Verify organization context: um org switch'));
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Update authData with fresh credentials
|
|
155
|
+
const savedToken = getToken();
|
|
156
|
+
authData.user_id = savedToken.decoded.sub;
|
|
157
|
+
authData.org_id = savedToken.selectedOrg?.id || authData.user_id;
|
|
158
|
+
authData.access_token = savedToken.idToken || savedToken.accessToken;
|
|
159
|
+
authData.expires_at = savedToken.decoded?.exp * 1000;
|
|
160
|
+
|
|
161
|
+
console.log(chalk.green('ā Re-authentication successful!\n'));
|
|
162
|
+
console.log(chalk.gray('Retrying project fetch...\n'));
|
|
163
|
+
|
|
164
|
+
// Retry fetching projects with new token
|
|
165
|
+
const retryResult = await fetchProjects(authData, apiUrl);
|
|
166
|
+
|
|
167
|
+
if (!retryResult.success) {
|
|
168
|
+
// Still failing after retry
|
|
169
|
+
console.log(chalk.red(`\nā Failed to fetch projects after re-authentication: ${retryResult.message}`));
|
|
170
|
+
console.log(chalk.yellow('\nThis may indicate:'));
|
|
171
|
+
console.log(chalk.gray(' - You don\'t have access to this organization'));
|
|
172
|
+
console.log(chalk.gray(' - The organization no longer exists'));
|
|
173
|
+
console.log(chalk.cyan('\nTry: um org switch'));
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Success! Update projects and continue
|
|
178
|
+
result.success = true;
|
|
179
|
+
result.projects = retryResult.projects;
|
|
180
|
+
console.log(chalk.green(`ā Found ${retryResult.projects.length} project(s)\n`));
|
|
181
|
+
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.log(chalk.red(`\nā Re-authentication error: ${error.message}`));
|
|
184
|
+
console.log(chalk.cyan('\nPlease run: um login'));
|
|
185
|
+
return null;
|
|
119
186
|
}
|
|
120
|
-
|
|
187
|
+
} else if (result.errorType === 'FORBIDDEN') {
|
|
188
|
+
console.log(chalk.yellow('\nš Access Denied\n'));
|
|
189
|
+
console.log(chalk.gray('You do not have permission to view projects in this organization.'));
|
|
190
|
+
console.log(chalk.cyan('\nContact your organization administrator for access.'));
|
|
191
|
+
return null;
|
|
192
|
+
} else if (result.errorType === 'NETWORK_ERROR') {
|
|
193
|
+
console.log(chalk.yellow('\nš Network Error\n'));
|
|
194
|
+
console.log(chalk.gray('Could not connect to the API server.'));
|
|
195
|
+
console.log(chalk.cyan('\nCheck your internet connection and try again.'));
|
|
196
|
+
return null;
|
|
197
|
+
} else {
|
|
198
|
+
console.log(chalk.yellow('\nā ļø Unexpected Error\n'));
|
|
199
|
+
console.log(chalk.gray(`Details: ${result.message}`));
|
|
200
|
+
console.log(chalk.cyan('\nTry: um login'));
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let projects = result.projects;
|
|
206
|
+
|
|
207
|
+
console.log(chalk.cyan('\nš Project setup\n'));
|
|
208
|
+
|
|
209
|
+
const choices = [
|
|
210
|
+
{
|
|
211
|
+
name: chalk.green('Create new project'),
|
|
212
|
+
value: 'create',
|
|
213
|
+
short: 'Create new project',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: chalk.green(`Select existing project`) + chalk.gray(` (${projects.length} available)`),
|
|
217
|
+
value: 'select',
|
|
218
|
+
short: 'Select existing project',
|
|
121
219
|
},
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const { action } = await inquirer.prompt([{
|
|
223
|
+
type: 'select',
|
|
224
|
+
name: 'action',
|
|
225
|
+
message: 'Choose an option:',
|
|
226
|
+
choices: choices,
|
|
227
|
+
default: projects.length > 0 ? 1 : 0,
|
|
122
228
|
}]);
|
|
123
229
|
|
|
124
|
-
const action = parseInt(selection, 10) === 1 ? 'create' : 'select';
|
|
125
|
-
|
|
126
230
|
if (action === 'create') {
|
|
127
231
|
const { name, description } = await inquirer.prompt([
|
|
128
232
|
{
|
|
@@ -142,32 +246,40 @@ async function selectOrCreateProject(authData, options) {
|
|
|
142
246
|
} else {
|
|
143
247
|
if (projects.length === 0) {
|
|
144
248
|
console.log(chalk.yellow('No projects found. Creating first project...'));
|
|
145
|
-
|
|
249
|
+
const { name, description } = await inquirer.prompt([
|
|
250
|
+
{
|
|
251
|
+
type: 'input',
|
|
252
|
+
name: 'name',
|
|
253
|
+
message: 'Project name:',
|
|
254
|
+
validate: input => input.length > 0 || 'Name is required',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
type: 'input',
|
|
258
|
+
name: 'description',
|
|
259
|
+
message: 'Project description (optional):',
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
return await createProject(authData, apiUrl, name, description);
|
|
146
264
|
}
|
|
147
265
|
|
|
148
|
-
// Display
|
|
149
|
-
console.log(chalk.cyan('\nš Available projects
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
return true;
|
|
165
|
-
},
|
|
266
|
+
// Display project list with arrow key selection
|
|
267
|
+
console.log(chalk.cyan('\nš Available projects\n'));
|
|
268
|
+
|
|
269
|
+
const choices = projects.map(p => ({
|
|
270
|
+
name: chalk.green(p.display_name || p.name) + chalk.gray(` (${p.slug || p.id})`),
|
|
271
|
+
value: p,
|
|
272
|
+
short: p.display_name || p.name,
|
|
273
|
+
}));
|
|
274
|
+
|
|
275
|
+
const { project } = await inquirer.prompt([{
|
|
276
|
+
type: 'select',
|
|
277
|
+
name: 'project',
|
|
278
|
+
message: 'Choose a project:',
|
|
279
|
+
choices: choices,
|
|
280
|
+
pageSize: 10,
|
|
166
281
|
}]);
|
|
167
282
|
|
|
168
|
-
const selectedIndex = parseInt(projectSelection, 10) - 1;
|
|
169
|
-
const project = projects[selectedIndex];
|
|
170
|
-
|
|
171
283
|
return {
|
|
172
284
|
project_id: project.id,
|
|
173
285
|
project_name: project.display_name || project.name,
|
|
@@ -187,11 +299,47 @@ async function fetchProjects(authData, apiUrl) {
|
|
|
187
299
|
},
|
|
188
300
|
}
|
|
189
301
|
);
|
|
190
|
-
|
|
191
|
-
return response.data?.items || [];
|
|
302
|
+
return { success: true, projects: response.data?.items || [] };
|
|
192
303
|
} catch (error) {
|
|
193
|
-
|
|
194
|
-
|
|
304
|
+
// Detect specific error types
|
|
305
|
+
if (error.response) {
|
|
306
|
+
const status = error.response.status;
|
|
307
|
+
|
|
308
|
+
if (status === 401) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
errorType: 'UNAUTHORIZED',
|
|
312
|
+
status: 401,
|
|
313
|
+
message: 'Authentication failed - token may be invalid or expired',
|
|
314
|
+
};
|
|
315
|
+
} else if (status === 403) {
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
errorType: 'FORBIDDEN',
|
|
319
|
+
status: 403,
|
|
320
|
+
message: 'Access denied - you may not have permission to view projects',
|
|
321
|
+
};
|
|
322
|
+
} else if (status >= 500) {
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
errorType: 'SERVER_ERROR',
|
|
326
|
+
status: status,
|
|
327
|
+
message: 'Server error - please try again later',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
} else if (error.request) {
|
|
331
|
+
return {
|
|
332
|
+
success: false,
|
|
333
|
+
errorType: 'NETWORK_ERROR',
|
|
334
|
+
message: 'Network error - could not reach API server',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
errorType: 'UNKNOWN',
|
|
341
|
+
message: error.message,
|
|
342
|
+
};
|
|
195
343
|
}
|
|
196
344
|
}
|
|
197
345
|
|
|
@@ -218,7 +366,27 @@ async function createProject(authData, apiUrl, name, description) {
|
|
|
218
366
|
project_slug: project.slug,
|
|
219
367
|
};
|
|
220
368
|
} catch (error) {
|
|
221
|
-
|
|
369
|
+
if (error.response) {
|
|
370
|
+
const status = error.response.status;
|
|
371
|
+
console.error(chalk.red(`\nā Failed to create project (HTTP ${status})`));
|
|
372
|
+
|
|
373
|
+
if (status === 401) {
|
|
374
|
+
console.log(chalk.yellow('š Authentication failed'));
|
|
375
|
+
console.log(chalk.gray('Your session may have expired.'));
|
|
376
|
+
console.log(chalk.cyan('Run: um login'));
|
|
377
|
+
} else if (status === 403) {
|
|
378
|
+
console.log(chalk.yellow('š Permission denied'));
|
|
379
|
+
console.log(chalk.gray('You do not have permission to create projects in this organization.'));
|
|
380
|
+
} else if (status === 409) {
|
|
381
|
+
console.log(chalk.yellow('ā ļø Project already exists'));
|
|
382
|
+
console.log(chalk.gray('A project with this name or slug already exists.'));
|
|
383
|
+
} else {
|
|
384
|
+
console.log(chalk.gray(`Details: ${error.message}`));
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
console.error(chalk.red(`\nā Failed to create project: ${error.message}`));
|
|
388
|
+
}
|
|
389
|
+
|
|
222
390
|
return null;
|
|
223
391
|
}
|
|
224
392
|
}
|
|
@@ -288,28 +456,37 @@ async function configureProviders(authData, projectData) {
|
|
|
288
456
|
|
|
289
457
|
// NEW APPROACH: Configure local MCP server (no tokens in config files)
|
|
290
458
|
for (const provider of detected) {
|
|
291
|
-
|
|
459
|
+
// Configure MCP server
|
|
460
|
+
const mcpSuccess = provider.configureMCP();
|
|
461
|
+
|
|
462
|
+
// Configure memory instructions
|
|
463
|
+
const instructionsResult = provider.configureMemoryInstructions?.();
|
|
292
464
|
|
|
293
|
-
|
|
294
|
-
|
|
465
|
+
// Display results
|
|
466
|
+
if (mcpSuccess) {
|
|
467
|
+
console.log(chalk.green(`ā Configured ${provider.name} MCP server`));
|
|
295
468
|
} else {
|
|
296
|
-
console.log(chalk.red(`ā Failed to configure ${provider.name}`));
|
|
469
|
+
console.log(chalk.red(`ā Failed to configure ${provider.name} MCP`));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (instructionsResult) {
|
|
473
|
+
switch (instructionsResult.status) {
|
|
474
|
+
case 'created':
|
|
475
|
+
console.log(chalk.green(` ā Created memory instructions`));
|
|
476
|
+
break;
|
|
477
|
+
case 'appended':
|
|
478
|
+
console.log(chalk.green(` ā Appended memory instructions`));
|
|
479
|
+
break;
|
|
480
|
+
case 'skipped':
|
|
481
|
+
console.log(chalk.gray(` ā¹ Memory instructions already present`));
|
|
482
|
+
break;
|
|
483
|
+
case 'error':
|
|
484
|
+
console.log(chalk.yellow(` ā Could not write memory instructions: ${instructionsResult.error}`));
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
297
487
|
}
|
|
298
488
|
}
|
|
299
489
|
|
|
300
490
|
console.log(chalk.cyan('\nš” Important: Restart your AI assistant to load the new configuration'));
|
|
301
491
|
console.log(chalk.gray(' The MCP server will automatically use your authentication and project context'));
|
|
302
492
|
}
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/commands/org.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import inquirer from 'inquirer';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
2
|
import { updateSelectedOrg, getSelectedOrg } from '../lib/token-storage.js';
|
|
4
3
|
import { loadAndRefreshToken } from '../lib/token-validation.js';
|
|
5
|
-
import { getUserOrganizations, getOrganizationsFromToken
|
|
4
|
+
import { getUserOrganizations, getOrganizationsFromToken } from '../lib/clerk-api.js';
|
|
5
|
+
import { promptOrganizationSelection, displayOrganizationSelection } from '../lib/org-selection-ui.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Switch organization context
|
|
@@ -40,49 +40,20 @@ export async function switchOrg() {
|
|
|
40
40
|
process.exit(0);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
console.log(chalk.green(`\nFound ${memberships.length} organization(s)!`));
|
|
44
|
-
|
|
45
|
-
// Format organizations for display
|
|
46
|
-
const formattedOrgs = memberships.map(formatOrganization);
|
|
47
|
-
|
|
48
43
|
// Get current selection
|
|
49
44
|
const currentOrg = getSelectedOrg();
|
|
50
|
-
const currentOrgId = currentOrg?.id;
|
|
51
|
-
|
|
52
|
-
// Build choices for inquirer
|
|
53
|
-
const choices = [
|
|
54
|
-
{
|
|
55
|
-
name: chalk.cyan('Personal Account') + chalk.gray(' (no organization)') + (currentOrgId ? '' : chalk.green(' ā current')),
|
|
56
|
-
value: null,
|
|
57
|
-
short: 'Personal Account',
|
|
58
|
-
},
|
|
59
|
-
new inquirer.Separator(chalk.gray('--- Organizations ---')),
|
|
60
|
-
...formattedOrgs.map(org => ({
|
|
61
|
-
name: `${chalk.green(org.name)} ${chalk.gray(`(${org.slug})`)} ${chalk.yellow(`[${org.role}]`)}${org.id === currentOrgId ? chalk.green(' ā current') : ''}`,
|
|
62
|
-
value: org,
|
|
63
|
-
short: org.name,
|
|
64
|
-
})),
|
|
65
|
-
];
|
|
66
45
|
|
|
67
46
|
// Prompt user to select
|
|
68
|
-
const
|
|
69
|
-
{
|
|
70
|
-
type: 'list',
|
|
71
|
-
name: 'organization',
|
|
72
|
-
message: 'Select account context:',
|
|
73
|
-
choices: choices,
|
|
74
|
-
pageSize: 15,
|
|
75
|
-
default: currentOrgId ? formattedOrgs.findIndex(org => org.id === currentOrgId) + 2 : 0, // +2 for personal + separator
|
|
76
|
-
},
|
|
77
|
-
]);
|
|
47
|
+
const selectedOrg = await promptOrganizationSelection(memberships, currentOrg);
|
|
78
48
|
|
|
79
49
|
// Update selected organization
|
|
80
|
-
updateSelectedOrg(
|
|
50
|
+
updateSelectedOrg(selectedOrg);
|
|
81
51
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.log(chalk.
|
|
85
|
-
console.log(chalk.gray(`
|
|
52
|
+
// Display result (with "Switched to" instead of "Selected")
|
|
53
|
+
if (selectedOrg) {
|
|
54
|
+
console.log(chalk.green(`\nā
Switched to organization: ${chalk.bold(selectedOrg.name)}`));
|
|
55
|
+
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
56
|
+
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
86
57
|
} else {
|
|
87
58
|
console.log(chalk.green('\nā
Switched to personal account context'));
|
|
88
59
|
}
|
package/index.js
CHANGED
|
@@ -11,13 +11,14 @@ import { record } from './commands/record.js';
|
|
|
11
11
|
import { config } from './lib/config.js';
|
|
12
12
|
import { getSelectedOrg } from './lib/token-storage.js';
|
|
13
13
|
import { loadAndRefreshToken } from './lib/token-validation.js';
|
|
14
|
+
import { showWelcome } from './lib/welcome.js';
|
|
14
15
|
|
|
15
16
|
const program = new Command();
|
|
16
17
|
|
|
17
18
|
program
|
|
18
19
|
.name('um')
|
|
19
20
|
.description('UnifiedMemory CLI - AI code assistant integration')
|
|
20
|
-
.version('1.
|
|
21
|
+
.version('1.1.0');
|
|
21
22
|
|
|
22
23
|
// Unified command (primary)
|
|
23
24
|
program
|
|
@@ -36,11 +37,19 @@ program
|
|
|
36
37
|
}
|
|
37
38
|
});
|
|
38
39
|
|
|
40
|
+
// Welcome command
|
|
41
|
+
program
|
|
42
|
+
.command('welcome')
|
|
43
|
+
.description('Show welcome message')
|
|
44
|
+
.action(() => {
|
|
45
|
+
showWelcome();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
39
49
|
// Individual commands (power users)
|
|
40
50
|
program
|
|
41
51
|
.command('login')
|
|
42
52
|
.description('Login to UnifiedMemory')
|
|
43
|
-
.option('--device', 'Use device flow for headless environments')
|
|
44
53
|
.action(async (options) => {
|
|
45
54
|
try {
|
|
46
55
|
await login();
|
|
@@ -113,30 +122,6 @@ program
|
|
|
113
122
|
}
|
|
114
123
|
});
|
|
115
124
|
|
|
116
|
-
// Project management command
|
|
117
|
-
program
|
|
118
|
-
.command('project')
|
|
119
|
-
.description('Manage project configuration')
|
|
120
|
-
.option('--create', 'Create new project')
|
|
121
|
-
.option('--link <id>', 'Link existing project')
|
|
122
|
-
.action(async (options) => {
|
|
123
|
-
console.log(chalk.yellow('Project management not yet implemented'));
|
|
124
|
-
console.log(chalk.gray('Use `um init` to configure a project'));
|
|
125
|
-
process.exit(0);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Configure command
|
|
129
|
-
program
|
|
130
|
-
.command('configure')
|
|
131
|
-
.description('Configure AI code assistants')
|
|
132
|
-
.option('--all', 'Configure all detected assistants')
|
|
133
|
-
.option('--provider <name>', 'Configure specific provider')
|
|
134
|
-
.action(async (options) => {
|
|
135
|
-
console.log(chalk.yellow('Provider configuration not yet implemented'));
|
|
136
|
-
console.log(chalk.gray('Use `um init` to auto-configure all providers'));
|
|
137
|
-
process.exit(0);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
125
|
// Organization management
|
|
141
126
|
const orgCommand = program
|
|
142
127
|
.command('org')
|
|
@@ -212,4 +197,10 @@ noteCommand
|
|
|
212
197
|
}
|
|
213
198
|
});
|
|
214
199
|
|
|
200
|
+
// Show welcome splash if no command provided
|
|
201
|
+
if (process.argv.length === 2) {
|
|
202
|
+
showWelcome();
|
|
203
|
+
program.help();
|
|
204
|
+
}
|
|
205
|
+
|
|
215
206
|
program.parse();
|