@unifiedmemory/cli 1.0.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 +9 -0
- package/CHANGELOG.md +34 -0
- package/HOOK_SETUP.md +338 -0
- package/LICENSE +51 -0
- package/README.md +220 -0
- package/bin/um-mcp-serve +62 -0
- package/commands/init.js +315 -0
- package/commands/login.js +390 -0
- package/commands/org.js +111 -0
- package/commands/record.js +114 -0
- package/index.js +215 -0
- package/lib/clerk-api.js +172 -0
- package/lib/config.js +39 -0
- package/lib/hooks.js +43 -0
- package/lib/mcp-proxy.js +227 -0
- package/lib/mcp-server.js +284 -0
- package/lib/provider-detector.js +291 -0
- package/lib/token-refresh.js +113 -0
- package/lib/token-storage.js +63 -0
- package/lib/token-validation.js +47 -0
- package/package.json +49 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { config, validateConfig } from '../lib/config.js';
|
|
8
|
+
import { saveToken, updateSelectedOrg } from '../lib/token-storage.js';
|
|
9
|
+
import { getUserOrganizations, getOrganizationsFromToken, formatOrganization, getOrgScopedToken } from '../lib/clerk-api.js';
|
|
10
|
+
|
|
11
|
+
function generateRandomState() {
|
|
12
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function base64URLEncode(buffer) {
|
|
16
|
+
return buffer.toString('base64')
|
|
17
|
+
.replace(/\+/g, '-')
|
|
18
|
+
.replace(/\//g, '_')
|
|
19
|
+
.replace(/=/g, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sha256(buffer) {
|
|
23
|
+
return crypto.createHash('sha256').update(buffer).digest();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generatePKCE() {
|
|
27
|
+
const verifier = base64URLEncode(crypto.randomBytes(32));
|
|
28
|
+
const challenge = base64URLEncode(sha256(verifier));
|
|
29
|
+
return { verifier, challenge };
|
|
30
|
+
}
|
|
31
|
+
|
|
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
|
+
export async function login() {
|
|
104
|
+
validateConfig();
|
|
105
|
+
|
|
106
|
+
const state = generateRandomState();
|
|
107
|
+
const pkce = generatePKCE();
|
|
108
|
+
|
|
109
|
+
// Build OAuth2 authorization URL with PKCE
|
|
110
|
+
const authUrl = new URL(`https://${config.clerkDomain}/oauth/authorize`);
|
|
111
|
+
authUrl.searchParams.append('client_id', config.clerkClientId);
|
|
112
|
+
authUrl.searchParams.append('redirect_uri', config.redirectUri);
|
|
113
|
+
authUrl.searchParams.append('response_type', 'code');
|
|
114
|
+
authUrl.searchParams.append('scope', 'openid profile email');
|
|
115
|
+
authUrl.searchParams.append('state', state);
|
|
116
|
+
authUrl.searchParams.append('code_challenge', pkce.challenge);
|
|
117
|
+
authUrl.searchParams.append('code_challenge_method', 'S256');
|
|
118
|
+
|
|
119
|
+
console.log(chalk.blue('š Starting OAuth2 authentication flow...'));
|
|
120
|
+
console.log(chalk.gray(`Opening browser to: ${authUrl.toString()}`));
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const server = http.createServer(async (req, res) => {
|
|
124
|
+
const url = new URL(req.url, `http://localhost:${config.port}`);
|
|
125
|
+
|
|
126
|
+
if (url.pathname === '/callback') {
|
|
127
|
+
const code = url.searchParams.get('code');
|
|
128
|
+
const returnedState = url.searchParams.get('state');
|
|
129
|
+
const error = url.searchParams.get('error');
|
|
130
|
+
|
|
131
|
+
if (error) {
|
|
132
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
133
|
+
res.end(`
|
|
134
|
+
<html>
|
|
135
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
136
|
+
<h1>ā Authentication Failed</h1>
|
|
137
|
+
<p>Error: ${error}</p>
|
|
138
|
+
<p>You can close this window.</p>
|
|
139
|
+
</body>
|
|
140
|
+
</html>
|
|
141
|
+
`);
|
|
142
|
+
server.close();
|
|
143
|
+
reject(new Error(`Authentication error: ${error}`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (returnedState !== state) {
|
|
148
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
149
|
+
res.end(`
|
|
150
|
+
<html>
|
|
151
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
152
|
+
<h1>ā Invalid State</h1>
|
|
153
|
+
<p>Security validation failed. Please try again.</p>
|
|
154
|
+
<p>You can close this window.</p>
|
|
155
|
+
</body>
|
|
156
|
+
</html>
|
|
157
|
+
`);
|
|
158
|
+
server.close();
|
|
159
|
+
reject(new Error('State mismatch - possible CSRF attack'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!code) {
|
|
164
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
165
|
+
res.end(`
|
|
166
|
+
<html>
|
|
167
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
168
|
+
<h1>ā No Authorization Code</h1>
|
|
169
|
+
<p>No authorization code received.</p>
|
|
170
|
+
<p>You can close this window.</p>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
173
|
+
`);
|
|
174
|
+
server.close();
|
|
175
|
+
reject(new Error('No authorization code received'));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// Exchange code for token
|
|
181
|
+
console.log(chalk.yellow('š Exchanging authorization code for token...'));
|
|
182
|
+
|
|
183
|
+
const tokenParams = {
|
|
184
|
+
client_id: config.clerkClientId,
|
|
185
|
+
code: code,
|
|
186
|
+
redirect_uri: config.redirectUri,
|
|
187
|
+
grant_type: 'authorization_code',
|
|
188
|
+
code_verifier: pkce.verifier,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Add client secret if provided (Clerk requires it even with PKCE)
|
|
192
|
+
if (config.clerkClientSecret) {
|
|
193
|
+
tokenParams.client_secret = config.clerkClientSecret;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const tokenResponse = await fetch(`https://${config.clerkDomain}/oauth/token`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
200
|
+
},
|
|
201
|
+
body: new URLSearchParams(tokenParams),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!tokenResponse.ok) {
|
|
205
|
+
const errorText = await tokenResponse.text();
|
|
206
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const tokenData = await tokenResponse.json();
|
|
210
|
+
|
|
211
|
+
// Debug: Log the token response structure
|
|
212
|
+
console.log(chalk.gray('\nToken response keys:'), Object.keys(tokenData));
|
|
213
|
+
|
|
214
|
+
// Send success response to browser first
|
|
215
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
216
|
+
res.end(`
|
|
217
|
+
<html>
|
|
218
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
219
|
+
<h1 style="color: #10B981;">ā
Authentication Successful!</h1>
|
|
220
|
+
<p>You have successfully authenticated with Clerk.</p>
|
|
221
|
+
<p>You can close this window and return to the CLI.</p>
|
|
222
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
223
|
+
</body>
|
|
224
|
+
</html>
|
|
225
|
+
`);
|
|
226
|
+
|
|
227
|
+
// Parse JWT to show user info - try both access_token and id_token
|
|
228
|
+
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
|
+
const decoded = parseJWT(tokenToParse);
|
|
232
|
+
|
|
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
|
+
// Save token (save both access_token and id_token if available)
|
|
252
|
+
saveToken({
|
|
253
|
+
accessToken: tokenData.access_token,
|
|
254
|
+
idToken: tokenData.id_token,
|
|
255
|
+
refresh_token: tokenData.refresh_token,
|
|
256
|
+
tokenType: tokenData.token_type || 'Bearer',
|
|
257
|
+
expiresIn: tokenData.expires_in,
|
|
258
|
+
receivedAt: Date.now(),
|
|
259
|
+
decoded: decoded
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
console.log(chalk.green('\nā
Authentication successful!'));
|
|
263
|
+
if (decoded) {
|
|
264
|
+
console.log(chalk.gray('\nToken information:'));
|
|
265
|
+
console.log(chalk.gray(` User ID: ${decoded.sub || 'N/A'}`));
|
|
266
|
+
console.log(chalk.gray(` Email: ${decoded.email || 'N/A'}`));
|
|
267
|
+
console.log(chalk.gray(` Issued at: ${decoded.iat ? new Date(decoded.iat * 1000).toLocaleString() : 'N/A'}`));
|
|
268
|
+
console.log(chalk.gray(` Expires at: ${decoded.exp ? new Date(decoded.exp * 1000).toLocaleString() : 'N/A'}`));
|
|
269
|
+
|
|
270
|
+
if (decoded.scope) {
|
|
271
|
+
console.log(chalk.gray(` Scopes: ${decoded.scope}`));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Close server first
|
|
276
|
+
server.close(async () => {
|
|
277
|
+
console.log(chalk.gray('ā Callback server closed'));
|
|
278
|
+
|
|
279
|
+
// Prompt for organization selection
|
|
280
|
+
try {
|
|
281
|
+
const userId = decoded?.sub;
|
|
282
|
+
if (userId) {
|
|
283
|
+
// First try to get organizations from JWT token
|
|
284
|
+
let memberships = getOrganizationsFromToken(decoded);
|
|
285
|
+
|
|
286
|
+
// If not in JWT, fetch from Clerk Frontend API
|
|
287
|
+
if (memberships.length === 0) {
|
|
288
|
+
const sessionToken = tokenData.id_token || tokenData.access_token;
|
|
289
|
+
memberships = await getUserOrganizations(userId, sessionToken);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const selectedOrg = await selectOrganization(memberships);
|
|
293
|
+
|
|
294
|
+
if (selectedOrg) {
|
|
295
|
+
// Get org-scoped JWT from Clerk
|
|
296
|
+
try {
|
|
297
|
+
console.log(chalk.cyan('\nš Getting organization-scoped token...'));
|
|
298
|
+
|
|
299
|
+
const sessionId = decoded.sid;
|
|
300
|
+
if (!sessionId) {
|
|
301
|
+
throw new Error('No session ID found in token');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const orgToken = await getOrgScopedToken(
|
|
305
|
+
sessionId,
|
|
306
|
+
selectedOrg.id,
|
|
307
|
+
tokenData.id_token
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Update saved token with org-scoped version
|
|
311
|
+
saveToken({
|
|
312
|
+
accessToken: tokenData.access_token,
|
|
313
|
+
idToken: orgToken.jwt,
|
|
314
|
+
refresh_token: tokenData.refresh_token,
|
|
315
|
+
tokenType: 'Bearer',
|
|
316
|
+
expiresIn: tokenData.expires_in,
|
|
317
|
+
receivedAt: Date.now(),
|
|
318
|
+
decoded: parseJWT(orgToken.jwt),
|
|
319
|
+
selectedOrg: selectedOrg
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
console.log(chalk.green(`\nā
Using organization context: ${chalk.bold(selectedOrg.name)}`));
|
|
323
|
+
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
324
|
+
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
325
|
+
console.log(chalk.gray(' ā Token updated with organization context'));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error(chalk.yellow('\nā ļø Failed to get org-scoped token:'), error.message);
|
|
328
|
+
console.log(chalk.gray(' Continuing with original token (may have limited org access)'));
|
|
329
|
+
|
|
330
|
+
// Fall back to updating selected org without new token
|
|
331
|
+
updateSelectedOrg(selectedOrg);
|
|
332
|
+
|
|
333
|
+
console.log(chalk.green(`\nā
Using organization context: ${chalk.bold(selectedOrg.name)}`));
|
|
334
|
+
console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
|
|
335
|
+
console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
console.log(chalk.green('\nā
Using personal account context'));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(chalk.gray('\nYou can switch organizations anytime with: um org switch'));
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.log(chalk.yellow('\nā ļø Could not fetch organizations. Continuing with personal account context.'));
|
|
345
|
+
console.log(chalk.gray(`Error: ${error.message}`));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
resolve(tokenData);
|
|
349
|
+
});
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error(chalk.red('Token exchange error:'), error.message);
|
|
352
|
+
|
|
353
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
354
|
+
res.end(`
|
|
355
|
+
<html>
|
|
356
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
357
|
+
<h1>ā Token Exchange Failed</h1>
|
|
358
|
+
<p>${error.message}</p>
|
|
359
|
+
<p>You can close this window.</p>
|
|
360
|
+
</body>
|
|
361
|
+
</html>
|
|
362
|
+
`);
|
|
363
|
+
|
|
364
|
+
server.close();
|
|
365
|
+
reject(error);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
369
|
+
res.end('Not Found');
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
server.listen(config.port, () => {
|
|
374
|
+
console.log(chalk.gray(`ā Local callback server running on port ${config.port}`));
|
|
375
|
+
console.log(chalk.yellow('\nš Please complete the authentication in your browser...'));
|
|
376
|
+
|
|
377
|
+
// Open browser
|
|
378
|
+
open(authUrl.toString()).catch(err => {
|
|
379
|
+
console.log(chalk.yellow('\nCouldn\'t open browser automatically. Please open this URL manually:'));
|
|
380
|
+
console.log(chalk.cyan(authUrl.toString()));
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Timeout after 5 minutes
|
|
385
|
+
setTimeout(() => {
|
|
386
|
+
server.close();
|
|
387
|
+
reject(new Error('Authentication timeout - please try again'));
|
|
388
|
+
}, 5 * 60 * 1000);
|
|
389
|
+
});
|
|
390
|
+
}
|
package/commands/org.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { updateSelectedOrg, getSelectedOrg } from '../lib/token-storage.js';
|
|
4
|
+
import { loadAndRefreshToken } from '../lib/token-validation.js';
|
|
5
|
+
import { getUserOrganizations, getOrganizationsFromToken, formatOrganization } from '../lib/clerk-api.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Switch organization context
|
|
9
|
+
*/
|
|
10
|
+
export async function switchOrg() {
|
|
11
|
+
// Load token and refresh if expired
|
|
12
|
+
const tokenData = await loadAndRefreshToken();
|
|
13
|
+
|
|
14
|
+
const userId = tokenData.decoded?.sub;
|
|
15
|
+
const accessToken = tokenData.idToken || tokenData.accessToken;
|
|
16
|
+
|
|
17
|
+
if (!userId || !accessToken) {
|
|
18
|
+
console.error(chalk.red('ā Invalid session'));
|
|
19
|
+
console.log(chalk.gray('Run `um login` to re-authenticate'));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(chalk.blue('\nš Fetching organizations...'));
|
|
24
|
+
|
|
25
|
+
// First try to get organizations from JWT token
|
|
26
|
+
let memberships = tokenData.decoded
|
|
27
|
+
? getOrganizationsFromToken(tokenData.decoded)
|
|
28
|
+
: [];
|
|
29
|
+
|
|
30
|
+
// If not in JWT, fetch from Clerk Frontend API
|
|
31
|
+
if (memberships.length === 0) {
|
|
32
|
+
const sessionToken = accessToken;
|
|
33
|
+
memberships = await getUserOrganizations(userId, sessionToken);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (memberships.length === 0) {
|
|
37
|
+
console.log(chalk.yellow('\nā ļø No organizations found'));
|
|
38
|
+
console.log(chalk.gray('You are using a personal account context.'));
|
|
39
|
+
console.log(chalk.gray('Create an organization at https://unifiedmemory.ai to collaborate with your team.'));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
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
|
+
// Get current selection
|
|
49
|
+
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
|
+
|
|
67
|
+
// Prompt user to select
|
|
68
|
+
const answer = await inquirer.prompt([
|
|
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
|
+
]);
|
|
78
|
+
|
|
79
|
+
// Update selected organization
|
|
80
|
+
updateSelectedOrg(answer.organization);
|
|
81
|
+
|
|
82
|
+
if (answer.organization) {
|
|
83
|
+
console.log(chalk.green(`\nā
Switched to organization: ${chalk.bold(answer.organization.name)}`));
|
|
84
|
+
console.log(chalk.gray(` Organization ID: ${answer.organization.id}`));
|
|
85
|
+
console.log(chalk.gray(` Your role: ${answer.organization.role}`));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.green('\nā
Switched to personal account context'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(chalk.gray('\nRun `um status` to verify your current context'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Show current organization
|
|
95
|
+
*/
|
|
96
|
+
export async function showOrg() {
|
|
97
|
+
const selectedOrg = getSelectedOrg();
|
|
98
|
+
|
|
99
|
+
console.log(chalk.blue('\nš Current Organization Context\n'));
|
|
100
|
+
|
|
101
|
+
if (selectedOrg) {
|
|
102
|
+
console.log(chalk.green(` ${selectedOrg.name} (${selectedOrg.slug})`));
|
|
103
|
+
console.log(chalk.gray(` ID: ${selectedOrg.id}`));
|
|
104
|
+
console.log(chalk.gray(` Role: ${selectedOrg.role}`));
|
|
105
|
+
} else {
|
|
106
|
+
console.log(chalk.cyan(' Personal Account'));
|
|
107
|
+
console.log(chalk.gray(' (no organization selected)'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(chalk.gray('\nRun `um org switch` to change organization\n'));
|
|
111
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { loadAndRefreshToken } from '../lib/token-validation.js';
|
|
5
|
+
import { callRemoteMCPTool } from '../lib/mcp-proxy.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Record a note to the vault
|
|
9
|
+
* @param {string} summary - Note summary text
|
|
10
|
+
* @param {Object} options - Command options
|
|
11
|
+
*/
|
|
12
|
+
export async function record(summary, options = {}) {
|
|
13
|
+
try {
|
|
14
|
+
// 1. Validate authentication and refresh if expired
|
|
15
|
+
const tokenData = await loadAndRefreshToken();
|
|
16
|
+
|
|
17
|
+
// 2. Load project context
|
|
18
|
+
const configPath = path.join(process.cwd(), '.um', 'config.json');
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'No project configuration found in current directory.\n' +
|
|
22
|
+
'Run "um init" to configure a project.'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const projectContext = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
27
|
+
if (!projectContext.project_id) {
|
|
28
|
+
throw new Error('Invalid project configuration: missing project_id');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. Build auth headers
|
|
32
|
+
const authHeaders = {
|
|
33
|
+
'Authorization': `Bearer ${tokenData.idToken || tokenData.accessToken}`,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Add org context
|
|
37
|
+
if (tokenData.selectedOrg?.id) {
|
|
38
|
+
authHeaders['X-Org-Id'] = tokenData.selectedOrg.id;
|
|
39
|
+
} else if (tokenData.decoded?.sub) {
|
|
40
|
+
authHeaders['X-Org-Id'] = tokenData.decoded.sub;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Add user ID
|
|
44
|
+
if (tokenData.decoded?.sub) {
|
|
45
|
+
authHeaders['X-User-Id'] = tokenData.decoded.sub;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. Build auth context for parameter injection
|
|
49
|
+
const authContext = {
|
|
50
|
+
decoded: tokenData.decoded,
|
|
51
|
+
selectedOrg: tokenData.selectedOrg
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// 5. Prepare tool arguments
|
|
55
|
+
const toolArgs = {
|
|
56
|
+
body: {
|
|
57
|
+
summary_text: summary,
|
|
58
|
+
topic: options.topic || 'general',
|
|
59
|
+
source: options.source || 'um-cli',
|
|
60
|
+
confidence: options.confidence ? parseFloat(options.confidence) : 0.7,
|
|
61
|
+
tags: options.tags ? options.tags.split(',') : []
|
|
62
|
+
},
|
|
63
|
+
headers: {
|
|
64
|
+
'X-Org-Id': tokenData.selectedOrg?.id || tokenData.decoded?.sub,
|
|
65
|
+
'X-User-Id': tokenData.decoded?.sub
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Add optional metadata
|
|
70
|
+
if (options.metadata) {
|
|
71
|
+
try {
|
|
72
|
+
toolArgs.body.metadata = JSON.parse(options.metadata);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error(chalk.yellow('Warning: Invalid metadata JSON, ignoring'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 6. Call vault tool
|
|
79
|
+
console.error(chalk.blue('ā Creating note in vault...'));
|
|
80
|
+
console.error(chalk.gray(` Project: ${projectContext.project_name}`));
|
|
81
|
+
console.error(chalk.gray(` Topic: ${toolArgs.body.topic}`));
|
|
82
|
+
console.error(chalk.gray(` Source: ${toolArgs.body.source}`));
|
|
83
|
+
|
|
84
|
+
const result = await callRemoteMCPTool(
|
|
85
|
+
'create_note',
|
|
86
|
+
toolArgs,
|
|
87
|
+
authHeaders,
|
|
88
|
+
authContext,
|
|
89
|
+
projectContext
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// 7. Handle response
|
|
93
|
+
if (result.content && Array.isArray(result.content)) {
|
|
94
|
+
const textContent = result.content.find(c => c.type === 'text');
|
|
95
|
+
if (textContent) {
|
|
96
|
+
const response = JSON.parse(textContent.text);
|
|
97
|
+
console.log(chalk.green('ā Note created successfully'));
|
|
98
|
+
console.log(chalk.gray(` Note ID: ${response.note_id || 'N/A'}`));
|
|
99
|
+
if (response.confidence) {
|
|
100
|
+
console.log(chalk.gray(` Confidence: ${response.confidence}`));
|
|
101
|
+
}
|
|
102
|
+
return response;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(chalk.green('ā Note created'));
|
|
107
|
+
return result;
|
|
108
|
+
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(chalk.red('ā Failed to create note:'));
|
|
111
|
+
console.error(chalk.red(` ${error.message}`));
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|