@unifiedmemory/cli 1.3.12 → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/org.js CHANGED
@@ -1,115 +1,30 @@
1
1
  import chalk from 'chalk';
2
- import { updateSelectedOrg, getSelectedOrg, saveToken, getToken } from '../lib/token-storage.js';
3
- import { loadAndRefreshToken } from '../lib/token-validation.js';
4
- import { refreshAccessToken } from '../lib/token-refresh.js';
5
- import { getUserOrganizations, getOrganizationsFromToken, getOrgScopedToken } from '../lib/clerk-api.js';
6
- import { parseJWT } from '../lib/jwt-utils.js';
7
- import { promptOrganizationSelection, displayOrganizationSelection } from '../lib/org-selection-ui.js';
2
+ import { getOrgContext } from '../lib/token-storage.js';
8
3
 
9
4
  /**
10
5
  * Switch organization context
6
+ * Re-runs the login flow which shows the org picker on the frontend
11
7
  */
12
8
  export async function switchOrg() {
13
- // Load token and refresh if expired
14
- const tokenData = await loadAndRefreshToken();
9
+ console.log(chalk.blue('\nšŸ”„ Opening browser to select organization...\n'));
15
10
 
16
- const userId = tokenData.decoded?.sub;
17
- const accessToken = tokenData.idToken || tokenData.accessToken;
18
-
19
- if (!userId || !accessToken) {
20
- console.error(chalk.red('āŒ Invalid session'));
21
- console.log(chalk.gray('Run `um login` to re-authenticate'));
22
- process.exit(1);
23
- }
24
-
25
- console.log(chalk.blue('\nšŸ” Fetching organizations...'));
26
-
27
- // First try to get organizations from JWT token
28
- let memberships = tokenData.decoded
29
- ? getOrganizationsFromToken(tokenData.decoded)
30
- : [];
31
-
32
- // If not in JWT, fetch from Clerk Frontend API
33
- if (memberships.length === 0) {
34
- const sessionToken = accessToken;
35
- memberships = await getUserOrganizations(userId, sessionToken);
36
- }
37
-
38
- if (memberships.length === 0) {
39
- console.log(chalk.yellow('\nāš ļø No organizations found'));
40
- console.log(chalk.gray('You are using a personal account context.'));
41
- console.log(chalk.gray('Create an organization at https://unifiedmemory.ai to collaborate with your team.'));
42
- process.exit(0);
43
- }
44
-
45
- // Get current selection
46
- const currentOrg = getSelectedOrg();
47
-
48
- // Prompt user to select
49
- const selectedOrg = await promptOrganizationSelection(memberships, currentOrg);
50
-
51
- // Update selected organization with org-scoped token
52
- if (selectedOrg) {
53
- try {
54
- // Get session ID from current token
55
- const sessionId = tokenData.decoded?.sid || tokenData.originalSid;
56
- const currentToken = tokenData.idToken || tokenData.accessToken;
57
-
58
- if (sessionId) {
59
- console.log(chalk.cyan('\nšŸ”„ Getting organization-scoped token...'));
60
- const orgToken = await getOrgScopedToken(sessionId, selectedOrg.id, currentToken);
61
-
62
- // Update with org-scoped token
63
- saveToken({
64
- ...tokenData,
65
- idToken: orgToken.jwt,
66
- decoded: parseJWT(orgToken.jwt),
67
- selectedOrg: selectedOrg,
68
- originalSid: sessionId
69
- });
70
-
71
- console.log(chalk.green(`\nāœ… Switched to organization: ${chalk.bold(selectedOrg.name)}`));
72
- console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
73
- console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
74
- console.log(chalk.gray(' āœ“ Token updated with organization context'));
75
- } else {
76
- // Fall back to just updating selectedOrg (recovery will try later)
77
- updateSelectedOrg(selectedOrg);
78
- console.log(chalk.green(`\nāœ… Switched to organization: ${chalk.bold(selectedOrg.name)}`));
79
- console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
80
- console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
81
- console.log(chalk.yellow(' āš ļø No session ID available - token lacks org claims'));
82
- console.log(chalk.gray(' Try: um login'));
83
- }
84
- } catch (error) {
85
- console.error(chalk.yellow(`\nāš ļø Failed to get org-scoped token: ${error.message}`));
86
- updateSelectedOrg(selectedOrg);
87
- console.log(chalk.green(`\nāœ… Switched to organization: ${chalk.bold(selectedOrg.name)}`));
88
- console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
89
- console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
90
- console.log(chalk.gray(' Saved org selection, but token may need refresh'));
91
- console.log(chalk.gray(' Try: um org fix'));
92
- }
93
- } else {
94
- updateSelectedOrg(null);
95
- console.log(chalk.green('\nāœ… Switched to personal account context'));
96
- }
97
-
98
- console.log(chalk.gray('\nRun `um status` to verify your current context'));
11
+ // Re-use login flow - frontend will show org picker
12
+ const { login } = await import('./login.js');
13
+ await login();
99
14
  }
100
15
 
101
16
  /**
102
- * Show current organization
17
+ * Show current organization context from JWT
103
18
  */
104
19
  export async function showOrg() {
105
- const selectedOrg = getSelectedOrg();
20
+ const orgContext = getOrgContext();
106
21
 
107
22
  console.log(chalk.blue('\nšŸ“‹ Current Organization Context\n'));
108
23
 
109
- if (selectedOrg) {
110
- console.log(chalk.green(` ${selectedOrg.name} (${selectedOrg.slug})`));
111
- console.log(chalk.gray(` ID: ${selectedOrg.id}`));
112
- console.log(chalk.gray(` Role: ${selectedOrg.role}`));
24
+ if (orgContext) {
25
+ console.log(chalk.green(` ${orgContext.name}${orgContext.slug ? ` (${orgContext.slug})` : ''}`));
26
+ console.log(chalk.gray(` ID: ${orgContext.id}`));
27
+ console.log(chalk.gray(` Role: ${orgContext.role || 'member'}`));
113
28
  } else {
114
29
  console.log(chalk.cyan(' Personal Account'));
115
30
  console.log(chalk.gray(' (no organization selected)'));
@@ -119,90 +34,23 @@ export async function showOrg() {
119
34
  }
120
35
 
121
36
  /**
122
- * Fix organization token if API calls fail with org mismatch
123
- * Attempts to get org-scoped token for the currently selected organization
37
+ * Fix organization token - deprecated
38
+ * JWT is now the single source of truth, so this is no longer needed.
39
+ * Kept for backwards compatibility but just shows a message.
124
40
  */
125
41
  export async function fixOrg() {
126
- const tokenData = getToken();
127
-
128
- if (!tokenData) {
129
- console.error(chalk.red('āŒ Not authenticated. Run: um login'));
130
- return;
131
- }
132
-
133
- if (!tokenData.selectedOrg) {
134
- console.error(chalk.yellow('āš ļø No organization selected. Run: um org switch'));
135
- return;
136
- }
137
-
138
- console.log(chalk.blue('\nšŸ”§ Organization Token Fix\n'));
139
- console.log(chalk.gray(` Selected org: ${tokenData.selectedOrg.name}`));
140
- console.log(chalk.gray(` Org ID: ${tokenData.selectedOrg.id}`));
141
-
142
- // Check if already has org claims
143
- if (tokenData.decoded?.o?.o_id) {
144
- console.log(chalk.green('\nāœ“ Token already has organization claims'));
145
- console.log(chalk.gray(` Org in token: ${tokenData.decoded.o.o_id}`));
146
- if (tokenData.decoded.o.o_id === tokenData.selectedOrg.id) {
147
- console.log(chalk.green(' āœ“ Org matches selected organization'));
148
- } else {
149
- console.log(chalk.yellow(' āš ļø Org mismatch - token has different org than selected'));
150
- console.log(chalk.gray(' Run: um org switch'));
151
- }
152
- return;
153
- }
42
+ const orgContext = getOrgContext();
154
43
 
155
- console.log(chalk.yellow('\nāš ļø Token missing org claims. Attempting fix...'));
44
+ console.log(chalk.blue('\nšŸ”§ Organization Token Check\n'));
156
45
 
157
- // Try to get session ID
158
- let sessionId = tokenData.decoded?.sid || tokenData.originalSid;
159
- let currentToken = tokenData.idToken || tokenData.accessToken;
160
-
161
- // If no sessionId, try OAuth refresh first
162
- if (!sessionId && tokenData.refresh_token) {
163
- console.log(chalk.gray(' Refreshing token to obtain session ID...'));
164
- try {
165
- const refreshed = await refreshAccessToken(tokenData);
166
- sessionId = refreshed.decoded?.sid;
167
- currentToken = refreshed.idToken || refreshed.accessToken;
168
-
169
- if (sessionId) {
170
- console.log(chalk.gray(' āœ“ Got session ID from refresh'));
171
- }
172
- } catch (error) {
173
- console.error(chalk.red(` Refresh failed: ${error.message}`));
174
- }
175
- }
176
-
177
- if (!sessionId) {
178
- console.error(chalk.red('\nāŒ Cannot obtain session ID'));
179
- console.log(chalk.gray(' The token does not contain a session ID needed for org-scoped tokens.'));
180
- console.log(chalk.gray(' Run: um login'));
181
- return;
46
+ if (orgContext) {
47
+ console.log(chalk.green('āœ“ Token has organization claims'));
48
+ console.log(chalk.gray(` Organization: ${orgContext.name}`));
49
+ console.log(chalk.gray(` ID: ${orgContext.id}`));
50
+ } else {
51
+ console.log(chalk.cyan('āœ“ Token is for personal account context'));
182
52
  }
183
53
 
184
- // Get org-scoped token
185
- try {
186
- console.log(chalk.cyan('\nšŸ”„ Getting organization-scoped token...'));
187
- const orgToken = await getOrgScopedToken(
188
- sessionId,
189
- tokenData.selectedOrg.id,
190
- currentToken
191
- );
192
-
193
- const decoded = parseJWT(orgToken.jwt);
194
- saveToken({
195
- ...tokenData,
196
- idToken: orgToken.jwt,
197
- decoded: decoded,
198
- originalSid: sessionId
199
- });
200
-
201
- console.log(chalk.green('\nāœ… Token fixed with organization claims'));
202
- console.log(chalk.gray(` Org: ${tokenData.selectedOrg.name}`));
203
- console.log(chalk.gray(` Org ID in token: ${decoded?.o?.o_id || 'N/A'}`));
204
- } catch (error) {
205
- console.error(chalk.red(`\nāŒ Failed to get org token: ${error.message}`));
206
- console.log(chalk.gray('\nYou may need to run: um login'));
207
- }
54
+ console.log(chalk.gray('\nJWT is now the single source of truth for org context.'));
55
+ console.log(chalk.gray('Use `um org switch` to change organization.'));
208
56
  }
@@ -28,27 +28,30 @@ export async function record(summary, options = {}) {
28
28
  throw new Error('Invalid project configuration: missing project_id');
29
29
  }
30
30
 
31
- // 3. Build auth headers
31
+ // 3. Build auth headers (JWT is single source of truth)
32
32
  const authHeaders = {
33
33
  'Authorization': `Bearer ${tokenData.idToken || tokenData.accessToken}`,
34
34
  };
35
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
36
+ // Add user ID (always present)
44
37
  if (tokenData.decoded?.sub) {
45
38
  authHeaders['X-User-Id'] = tokenData.decoded.sub;
46
39
  }
47
40
 
48
- // 4. Build auth context for parameter injection
41
+ // Only add X-Org-Id if JWT has org claims
42
+ // NO FALLBACK - gateway handles personal context
43
+ if (tokenData.decoded?.o?.o_id) {
44
+ authHeaders['X-Org-Id'] = tokenData.decoded.o.o_id;
45
+ }
46
+
47
+ // 4. Build auth context for parameter injection (from JWT claims)
49
48
  const authContext = {
50
49
  decoded: tokenData.decoded,
51
- selectedOrg: tokenData.selectedOrg
50
+ orgContext: tokenData.decoded?.o ? {
51
+ id: tokenData.decoded.o.o_id,
52
+ name: tokenData.decoded.o.o_name,
53
+ role: tokenData.decoded.o.o_role,
54
+ } : null
52
55
  };
53
56
 
54
57
  // 5. Prepare tool arguments
@@ -0,0 +1,163 @@
1
+ import chalk from 'chalk';
2
+ import { loadAndRefreshToken } from '../lib/token-validation.js';
3
+ import { config } from '../lib/config.js';
4
+
5
+ /**
6
+ * Show current usage and quota allowances
7
+ */
8
+ export async function usage() {
9
+ // Load token and refresh if expired
10
+ const tokenData = await loadAndRefreshToken();
11
+
12
+ const accessToken = tokenData.idToken || tokenData.accessToken;
13
+
14
+ if (!accessToken) {
15
+ console.error(chalk.red('āŒ Not authenticated'));
16
+ console.log(chalk.gray('Run `um login` to authenticate'));
17
+ process.exit(1);
18
+ }
19
+
20
+ console.log(chalk.blue('\nšŸ“Š Usage & Quota\n'));
21
+
22
+ try {
23
+ // Build headers with org context (selectedOrgId fallback for when JWT has no org claims)
24
+ const userId = tokenData.decoded?.sub;
25
+ const orgId = tokenData.decoded?.o?.o_id || tokenData.selectedOrgId;
26
+
27
+ const headers = {
28
+ 'Authorization': `Bearer ${accessToken}`,
29
+ };
30
+ if (userId) headers['X-User-Id'] = userId;
31
+ if (orgId) headers['X-Org-Id'] = orgId;
32
+
33
+ // Fetch quota usage from API gateway
34
+ const response = await fetch(`${config.apiEndpoint}/v1/quota/usage`, { headers });
35
+
36
+ if (!response.ok) {
37
+ if (response.status === 401) {
38
+ console.error(chalk.red('āŒ Authentication failed'));
39
+ console.log(chalk.gray('Run `um login` to re-authenticate'));
40
+ process.exit(1);
41
+ }
42
+ throw new Error(`API returned ${response.status}: ${response.statusText}`);
43
+ }
44
+
45
+ const data = await response.json();
46
+ displayUsage(data, tokenData);
47
+
48
+ } catch (error) {
49
+ console.error(chalk.red('āŒ Failed to fetch usage data:'), error.message);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Display formatted usage information
56
+ */
57
+ function displayUsage(quotaData, tokenData) {
58
+ const { quota_type, usage, period } = quotaData;
59
+ const { used, total, percentage_used, remaining } = usage;
60
+
61
+ // Display account context (use selectedOrgName fallback for when JWT has no org claims)
62
+ console.log(chalk.yellow('Account:'));
63
+ if (quota_type === 'organization') {
64
+ // Get org name from JWT claims, fallback to selectedOrgName from login state
65
+ const orgName = tokenData.decoded?.o?.o_name || tokenData.selectedOrgName || 'Organization';
66
+ console.log(chalk.white(` Organization: ${orgName}`));
67
+ } else {
68
+ console.log(chalk.white(' Personal Account'));
69
+ }
70
+ console.log('');
71
+
72
+ // Handle special cases
73
+ if (total === 0) {
74
+ console.log(chalk.yellow('Monthly Queries:'));
75
+ console.log(chalk.yellow(' No active subscription'));
76
+ console.log(chalk.gray(' Start at: https://unifiedmemory.ai/pricing'));
77
+ console.log('');
78
+ return;
79
+ }
80
+
81
+ if (total < 0 || total > 1000000) {
82
+ console.log(chalk.yellow('Monthly Queries:'));
83
+ console.log(chalk.green(' Unlimited ā™¾ļø'));
84
+ console.log('');
85
+ return;
86
+ }
87
+
88
+ // Determine color based on usage percentage
89
+ const color = getColorForUsage(percentage_used);
90
+ const colorFn = getChalkFunction(color);
91
+
92
+ // Display usage count and percentage
93
+ console.log(chalk.yellow('Monthly Queries:'));
94
+ console.log(colorFn(` ${used.toLocaleString()} / ${total.toLocaleString()} (${percentage_used.toFixed(1)}%)`));
95
+
96
+ // Display progress bar
97
+ const progressBar = buildProgressBar(percentage_used, 20);
98
+ console.log(colorFn(` [${progressBar}]`));
99
+
100
+ // Display remaining quota
101
+ console.log(colorFn(` Remaining: ${remaining.toLocaleString()} queries`));
102
+
103
+ // Display reset date if available
104
+ if (period?.next_reset_date) {
105
+ const resetDate = new Date(period.next_reset_date);
106
+ const formattedDate = resetDate.toLocaleDateString('en-US', {
107
+ month: 'numeric',
108
+ day: 'numeric',
109
+ year: 'numeric'
110
+ });
111
+ console.log(chalk.gray(` Resets: ${formattedDate}`));
112
+ }
113
+
114
+ console.log('');
115
+
116
+ // Show upgrade prompt if at or above threshold
117
+ if (percentage_used >= 90) {
118
+ console.log(chalk.red('āš ļø Running low on quota!'));
119
+ console.log(chalk.gray('Upgrade your plan: https://unifiedmemory.ai/pricing'));
120
+ console.log('');
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get color category based on usage percentage
126
+ * @param {number} percentage - Usage percentage (0-100)
127
+ * @returns {string} - Color category: 'green', 'yellow', or 'red'
128
+ */
129
+ function getColorForUsage(percentage) {
130
+ if (percentage >= 90) return 'red';
131
+ if (percentage >= 70) return 'yellow';
132
+ return 'green';
133
+ }
134
+
135
+ /**
136
+ * Get chalk color function based on color name
137
+ * @param {string} color - Color name
138
+ * @returns {Function} - Chalk color function
139
+ */
140
+ function getChalkFunction(color) {
141
+ switch (color) {
142
+ case 'red':
143
+ return chalk.red;
144
+ case 'yellow':
145
+ return chalk.yellow;
146
+ case 'green':
147
+ return chalk.green;
148
+ default:
149
+ return chalk.white;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Build a text-based progress bar
155
+ * @param {number} percentage - Usage percentage (0-100)
156
+ * @param {number} length - Total length of the bar in characters
157
+ * @returns {string} - Progress bar string
158
+ */
159
+ function buildProgressBar(percentage, length) {
160
+ const filled = Math.round((percentage / 100) * length);
161
+ const empty = length - filled;
162
+ return 'ā–ˆ'.repeat(filled) + 'ā–‘'.repeat(empty);
163
+ }
package/index.js CHANGED
@@ -11,8 +11,10 @@ import { login } from './commands/login.js';
11
11
  import { init } from './commands/init.js';
12
12
  import { switchOrg, showOrg, fixOrg } from './commands/org.js';
13
13
  import { record } from './commands/record.js';
14
+ import { usage } from './commands/usage.js';
15
+ import { debugAuth } from './commands/debug.js';
14
16
  import { config } from './lib/config.js';
15
- import { getSelectedOrg } from './lib/token-storage.js';
17
+ import { getOrgContext } from './lib/token-storage.js';
16
18
  import { loadAndRefreshToken } from './lib/token-validation.js';
17
19
  import { showWelcome } from './lib/welcome.js';
18
20
 
@@ -74,7 +76,8 @@ program
74
76
  try {
75
77
  // Try to load and refresh token if expired
76
78
  const tokenData = await loadAndRefreshToken(false);
77
- const selectedOrg = getSelectedOrg();
79
+ // Get org context from JWT claims (single source of truth)
80
+ const orgContext = getOrgContext();
78
81
 
79
82
  console.log(chalk.blue('\nšŸ“‹ UnifiedMemory Status\n'));
80
83
 
@@ -94,10 +97,10 @@ program
94
97
  }
95
98
 
96
99
  console.log(chalk.yellow('\nOrganization Context:'));
97
- if (selectedOrg) {
98
- console.log(chalk.green(` ${selectedOrg.name} (${selectedOrg.slug})`));
99
- console.log(chalk.gray(` ID: ${selectedOrg.id}`));
100
- console.log(chalk.gray(` Role: ${selectedOrg.role}`));
100
+ if (orgContext) {
101
+ console.log(chalk.green(` ${orgContext.name}${orgContext.slug ? ` (${orgContext.slug})` : ''}`));
102
+ console.log(chalk.gray(` ID: ${orgContext.id}`));
103
+ console.log(chalk.gray(` Role: ${orgContext.role || 'member'}`));
101
104
  } else {
102
105
  console.log(chalk.cyan(' Personal Account'));
103
106
  }
@@ -128,6 +131,34 @@ program
128
131
  }
129
132
  });
130
133
 
134
+ // Usage command
135
+ program
136
+ .command('usage')
137
+ .description('Show current usage and quota allowances')
138
+ .action(async () => {
139
+ try {
140
+ await usage();
141
+ process.exit(0);
142
+ } catch (error) {
143
+ console.error(chalk.red('Failed to show usage:'), error.message);
144
+ process.exit(1);
145
+ }
146
+ });
147
+
148
+ // Debug command
149
+ program
150
+ .command('debug')
151
+ .description('Debug authentication and token issues')
152
+ .action(async () => {
153
+ try {
154
+ await debugAuth();
155
+ process.exit(0);
156
+ } catch (error) {
157
+ console.error(chalk.red('Debug failed:'), error.message);
158
+ process.exit(1);
159
+ }
160
+ });
161
+
131
162
  // Organization management
132
163
  const orgCommand = program
133
164
  .command('org')
package/lib/clerk-api.js CHANGED
@@ -121,30 +121,27 @@ export function formatOrganization(membership) {
121
121
  }
122
122
 
123
123
  /**
124
- * Get an organization-scoped JWT token via backend API
124
+ * Get a JWT token with specific org context via backend API
125
125
  * @param {string} sessionId - Session ID from JWT sid claim
126
- * @param {string} orgId - Organization ID to set as active
126
+ * @param {string|null} orgId - Organization ID to set as active, or null for personal context
127
127
  * @param {string} currentToken - Current session token (for authentication)
128
- * @returns {Promise<{jwt: string, org_id: string}>} New token with org context
128
+ * @returns {Promise<{jwt: string, org_id: string|null}>} New token with specified context
129
129
  */
130
130
  export async function getOrgScopedToken(sessionId, orgId, currentToken) {
131
131
  if (!sessionId) {
132
132
  throw new Error('Session ID is required');
133
133
  }
134
- if (!orgId) {
135
- throw new Error('Organization ID is required');
136
- }
137
134
  if (!currentToken) {
138
135
  throw new Error('Current token is required');
139
136
  }
137
+ // orgId can be null for personal context
140
138
 
141
- console.log(chalk.gray(`Requesting org-scoped token from backend...`));
139
+ const contextDesc = orgId ? `org ${orgId}` : 'personal context';
140
+ console.log(chalk.gray(`Requesting token for ${contextDesc}...`));
142
141
  console.log(chalk.gray(` Session: ${sessionId}`));
143
- console.log(chalk.gray(` Org: ${orgId}`));
144
142
 
145
143
  // Call backend API which proxies to Clerk Backend API
146
144
  const apiUrl = `${config.apiEndpoint}/v1/auth/org-token`;
147
- console.log(chalk.gray(`Calling backend API: ${apiUrl}`));
148
145
 
149
146
  const response = await fetch(apiUrl, {
150
147
  method: 'POST',
@@ -154,7 +151,7 @@ export async function getOrgScopedToken(sessionId, orgId, currentToken) {
154
151
  },
155
152
  body: JSON.stringify({
156
153
  session_id: sessionId,
157
- org_id: orgId
154
+ org_id: orgId // null for personal context
158
155
  })
159
156
  });
160
157
 
@@ -162,11 +159,11 @@ export async function getOrgScopedToken(sessionId, orgId, currentToken) {
162
159
  const errorText = await response.text();
163
160
  console.error(chalk.red(`Backend API failed: ${response.status}`));
164
161
  console.error(chalk.gray(errorText));
165
- throw new Error(`Failed to get org-scoped token: ${response.status} - ${errorText}`);
162
+ throw new Error(`Failed to get token: ${response.status} - ${errorText}`);
166
163
  }
167
164
 
168
165
  const tokenData = await response.json();
169
- console.log(chalk.gray('āœ“ Received org-scoped token from backend'));
166
+ console.log(chalk.gray(`āœ“ Received token for ${contextDesc}`));
170
167
 
171
168
  return tokenData;
172
169
  }
package/lib/config.js CHANGED
@@ -19,7 +19,13 @@ export const config = {
19
19
 
20
20
  // OAuth flow configuration (localhost defaults for callback server)
21
21
  redirectUri: process.env.REDIRECT_URI || 'http://localhost:3333/callback',
22
- port: parseInt(process.env.PORT || '3333', 10)
22
+ port: parseInt(process.env.PORT || '3333', 10),
23
+
24
+ // OAuth success page configuration (optional website link for account management)
25
+ oauthSuccessUrl: process.env.OAUTH_SUCCESS_URL || 'https://unifiedmemory.ai/oauth/callback',
26
+
27
+ // Frontend URL for CLI auth page (org picker)
28
+ frontendUrl: process.env.FRONTEND_URL || 'https://unifiedmemory.ai'
23
29
  };
24
30
 
25
31
  // Validation function - validates configuration values
@@ -41,5 +47,19 @@ export function validateConfig() {
41
47
  throw new Error('CLERK_CLIENT_ID cannot be empty');
42
48
  }
43
49
 
50
+ // Validate oauthSuccessUrl format if provided
51
+ try {
52
+ new URL(config.oauthSuccessUrl);
53
+ } catch (e) {
54
+ throw new Error(`OAUTH_SUCCESS_URL must be a valid URL (got: ${config.oauthSuccessUrl})`);
55
+ }
56
+
57
+ // Validate frontendUrl format
58
+ try {
59
+ new URL(config.frontendUrl);
60
+ } catch (e) {
61
+ throw new Error(`FRONTEND_URL must be a valid URL (got: ${config.frontendUrl})`);
62
+ }
63
+
44
64
  return true;
45
65
  }
package/lib/mcp-proxy.js CHANGED
@@ -82,8 +82,9 @@ function transformToolSchema(tool) {
82
82
 
83
83
  /**
84
84
  * Inject context parameters into tool arguments
85
+ * JWT is the single source of truth for org context
85
86
  * @param {Object} args - Tool arguments from AI agent
86
- * @param {Object} authContext - Auth context with user/org info
87
+ * @param {Object} authContext - Auth context with user/org info (from JWT claims)
87
88
  * @param {Object|null} projectContext - Project config from .um/config.json
88
89
  * @returns {Object} - Arguments with injected context params
89
90
  */
@@ -101,21 +102,19 @@ function injectContextParams(args, authContext, projectContext) {
101
102
  injected.headers = {};
102
103
  }
103
104
 
104
- // Inject user ID from decoded JWT
105
+ // Inject user ID from decoded JWT (always present)
105
106
  if (authContext?.decoded?.sub) {
106
107
  injected.pathParams.user = authContext.decoded.sub;
107
108
  injected.headers['X-User-Id'] = authContext.decoded.sub;
108
109
  }
109
110
 
110
- // Inject org ID (from selectedOrg or fallback to user ID for personal account)
111
- if (authContext?.selectedOrg?.id) {
112
- injected.pathParams.org = authContext.selectedOrg.id;
113
- injected.headers['X-Org-Id'] = authContext.selectedOrg.id;
114
- } else if (authContext?.decoded?.sub) {
115
- // Fallback to user ID for personal account context
116
- injected.pathParams.org = authContext.decoded.sub;
117
- injected.headers['X-Org-Id'] = authContext.decoded.sub;
111
+ // ONLY inject org ID if JWT has org claims
112
+ // NO FALLBACK - gateway handles personal context by using userId
113
+ if (authContext?.orgContext?.id) {
114
+ injected.pathParams.org = authContext.orgContext.id;
115
+ injected.headers['X-Org-Id'] = authContext.orgContext.id;
118
116
  }
117
+ // For personal context: no X-Org-Id header set, gateway falls back to userId
119
118
 
120
119
  // Inject project ID from project config
121
120
  if (projectContext?.project_id) {