@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/.env.example +27 -6
- package/CHANGELOG.md +264 -0
- package/README.md +64 -3
- package/commands/init.js +370 -71
- 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 +91 -79
- 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
|
@@ -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
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
191
|
-
return response.data?.items || [];
|
|
360
|
+
return { success: true, projects: response.data?.items || [] };
|
|
192
361
|
} catch (error) {
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
+
// Configure MCP server (pass permissions for Claude Code)
|
|
577
|
+
const mcpSuccess = provider.configureMCP(mcpToolPermissions);
|
|
292
578
|
|
|
293
|
-
|
|
294
|
-
|
|
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,
|
|
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
|