@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.
@@ -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
+ }
@@ -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
+ }