@unifiedmemory/cli 1.3.13 → 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/.env.example CHANGED
@@ -45,6 +45,16 @@
45
45
  # Default: https://unifiedmemory.ai/oauth/callback
46
46
  # OAUTH_SUCCESS_URL=https://unifiedmemory.ai/oauth/callback
47
47
 
48
+ # ============================================
49
+ # Frontend URL (CLI Auth Page)
50
+ # ============================================
51
+ # URL of the frontend app that hosts the CLI auth page with org picker
52
+ # During login, the browser opens this URL where users can select an organization
53
+ # before being redirected to Clerk OAuth
54
+ # Default: https://unifiedmemory.ai
55
+ # For local development: http://localhost:3000
56
+ # FRONTEND_URL=https://unifiedmemory.ai
57
+
48
58
  # ============================================
49
59
  # Clerk Client Secret (Optional)
50
60
  # ============================================
package/README.md CHANGED
@@ -106,6 +106,34 @@ Project Configuration:
106
106
  āœ“ Configured: My Project (proj_xxx)
107
107
  ```
108
108
 
109
+ ### `um usage`
110
+ Check your current usage and quota allowances.
111
+
112
+ ```bash
113
+ um usage
114
+ ```
115
+
116
+ **Example output:**
117
+ ```
118
+ šŸ“Š Usage & Quota
119
+
120
+ Account:
121
+ Organization: My Team
122
+
123
+ Monthly Queries:
124
+ 450 / 1,000 (45.0%)
125
+ [ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘ā–‘]
126
+ Remaining: 550 queries
127
+ Resets: 2/1/2026
128
+ ```
129
+
130
+ Context-aware: Shows personal or organization quota based on current context (`um org switch`).
131
+
132
+ **Color indicators:**
133
+ - 🟢 Green: <70% usage
134
+ - 🟔 Yellow: 70-89% usage
135
+ - šŸ”“ Red: ≄90% usage (with upgrade prompt)
136
+
109
137
  ### `um org switch`
110
138
  Switch between your organizations or personal account.
111
139
 
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { getToken, getOrgContext } from '../lib/token-storage.js';
3
+ import { isTokenExpired } from '../lib/token-refresh.js';
4
+
5
+ /**
6
+ * Debug authentication and token issues
7
+ * Displays comprehensive diagnostics about the current token state
8
+ * JWT is the single source of truth for org context
9
+ */
10
+ export async function debugAuth() {
11
+ console.log(chalk.blue('\nšŸ” Token Diagnostics\n'));
12
+
13
+ const tokenData = getToken();
14
+
15
+ if (!tokenData) {
16
+ console.log(chalk.red('āŒ No token found'));
17
+ console.log(chalk.gray(' Run: um login'));
18
+ return;
19
+ }
20
+
21
+ // Check expiration
22
+ const expired = isTokenExpired(tokenData);
23
+ console.log(chalk.yellow('Token Expiration:'));
24
+ console.log(expired ? chalk.red(' āŒ Expired') : chalk.green(' āœ“ Valid'));
25
+ if (tokenData.decoded?.exp) {
26
+ const expDate = new Date(tokenData.decoded.exp * 1000);
27
+ console.log(chalk.gray(` Expires: ${expDate.toLocaleString()}`));
28
+ }
29
+
30
+ // Check org context from JWT claims (single source of truth)
31
+ console.log(chalk.yellow('\nOrganization Context (from JWT):'));
32
+ const orgContext = getOrgContext();
33
+
34
+ if (orgContext) {
35
+ console.log(chalk.green(` āœ“ Organization: ${orgContext.name} (${orgContext.id})`));
36
+ console.log(chalk.gray(` Role: ${orgContext.role || 'member'}`));
37
+ if (orgContext.slug) {
38
+ console.log(chalk.gray(` Slug: ${orgContext.slug}`));
39
+ }
40
+ } else {
41
+ console.log(chalk.cyan(' Personal Account (no org claims in JWT)'));
42
+ }
43
+
44
+ // Check session ID
45
+ console.log(chalk.yellow('\nSession ID:'));
46
+ const hasSid = tokenData.decoded?.sid;
47
+ console.log(hasSid ? chalk.green(' āœ“ Present') : chalk.yellow(' ⚠ Missing'));
48
+ if (hasSid) {
49
+ console.log(chalk.gray(` SID: ${tokenData.decoded.sid}`));
50
+ } else {
51
+ console.log(chalk.gray(' Note: Some org-scoped tokens may not have sid'));
52
+ }
53
+
54
+ // Check user ID
55
+ console.log(chalk.yellow('\nUser ID:'));
56
+ if (tokenData.decoded?.sub) {
57
+ console.log(chalk.green(` āœ“ ${tokenData.decoded.sub}`));
58
+ } else {
59
+ console.log(chalk.red(' āŒ Missing'));
60
+ }
61
+
62
+ // Check refresh token
63
+ console.log(chalk.yellow('\nRefresh Token:'));
64
+ console.log(tokenData.refresh_token ? chalk.green(' āœ“ Present') : chalk.red(' āŒ Missing'));
65
+
66
+ console.log('');
67
+ }
package/commands/init.js CHANGED
@@ -65,8 +65,9 @@ export async function init(options = {}) {
65
65
  }
66
66
 
67
67
  console.log(chalk.green(`āœ“ Logged in as: ${authData.user_id}`));
68
- if (authData.org_id) {
69
- console.log(chalk.green(`āœ“ Organization: ${authData.org_id}`));
68
+ if (authData.org_id && authData.org_id !== authData.user_id) {
69
+ const orgDisplay = authData.org_name || authData.org_id;
70
+ console.log(chalk.green(`āœ“ Organization: ${orgDisplay}`));
70
71
  }
71
72
 
72
73
  // Step 1.5: Check for existing config
@@ -166,14 +167,17 @@ async function ensureAuthenticated(options) {
166
167
  if (stored && stored.decoded) {
167
168
  console.log(chalk.gray('Using saved session'));
168
169
 
169
- // Extract user_id and org_id from token
170
+ // Extract user_id and org_id from JWT claims, with selectedOrgId fallback
170
171
  const userId = stored.decoded.sub;
171
- const orgId = stored.selectedOrg?.id || userId; // Fallback to user_id if no org
172
+ // Use org from JWT o.* claims, fallback to selectedOrgId from login, then userId for personal context
173
+ const orgId = stored.decoded.o?.o_id || stored.selectedOrgId || userId;
174
+ const orgName = stored.decoded.o?.o_name || stored.selectedOrgName || null;
172
175
  const expirationTime = stored.decoded.exp * 1000;
173
176
 
174
177
  return {
175
178
  user_id: userId,
176
179
  org_id: orgId,
180
+ org_name: orgName,
177
181
  access_token: stored.idToken || stored.accessToken,
178
182
  api_url: 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
179
183
  expires_at: expirationTime,
@@ -189,14 +193,17 @@ async function ensureAuthenticated(options) {
189
193
  return null;
190
194
  }
191
195
 
192
- // Extract from saved token
196
+ // Extract from saved token (JWT claims with selectedOrgId fallback)
193
197
  const savedToken = getToken();
194
198
  const userId = savedToken?.decoded?.sub;
195
- const orgId = savedToken?.selectedOrg?.id || userId;
199
+ // Use org from JWT o.* claims, fallback to selectedOrgId from login, then userId for personal context
200
+ const orgId = savedToken?.decoded?.o?.o_id || savedToken?.selectedOrgId || userId;
201
+ const orgName = savedToken?.decoded?.o?.o_name || savedToken?.selectedOrgName || null;
196
202
 
197
203
  return {
198
204
  user_id: userId,
199
205
  org_id: orgId,
206
+ org_name: orgName,
200
207
  access_token: savedToken.idToken || savedToken.accessToken,
201
208
  api_url: 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
202
209
  expires_at: savedToken.decoded?.exp * 1000,
@@ -244,10 +251,12 @@ async function selectOrCreateProject(authData, options) {
244
251
  return null;
245
252
  }
246
253
 
247
- // Update authData with fresh credentials
254
+ // Update authData with fresh credentials (JWT claims with selectedOrgId fallback)
248
255
  const savedToken = getToken();
249
256
  authData.user_id = savedToken.decoded.sub;
250
- authData.org_id = savedToken.selectedOrg?.id || authData.user_id;
257
+ // Use org from JWT o.* claims, fallback to selectedOrgId from login, then userId for personal context
258
+ authData.org_id = savedToken.decoded?.o?.o_id || savedToken?.selectedOrgId || authData.user_id;
259
+ authData.org_name = savedToken.decoded?.o?.o_name || savedToken?.selectedOrgName || null;
251
260
  authData.access_token = savedToken.idToken || savedToken.accessToken;
252
261
  authData.expires_at = savedToken.decoded?.exp * 1000;
253
262
 
package/commands/login.js CHANGED
@@ -3,12 +3,9 @@ import { URL } from 'url';
3
3
  import open from 'open';
4
4
  import chalk from 'chalk';
5
5
  import crypto from 'crypto';
6
- import inquirer from 'inquirer';
7
6
  import { config, validateConfig } from '../lib/config.js';
8
- import { saveToken, updateSelectedOrg } from '../lib/token-storage.js';
9
- import { getUserOrganizations, getOrganizationsFromToken, getOrgScopedToken } from '../lib/clerk-api.js';
7
+ import { saveToken } from '../lib/token-storage.js';
10
8
  import { parseJWT } from '../lib/jwt-utils.js';
11
- import { promptOrganizationSelection, displayOrganizationSelection } from '../lib/org-selection-ui.js';
12
9
 
13
10
  function generateRandomState() {
14
11
  // Use cryptographically secure random bytes for CSRF protection
@@ -38,11 +35,10 @@ export async function login() {
38
35
  const state = generateRandomState();
39
36
  const pkce = generatePKCE();
40
37
 
41
- // Build OAuth2 authorization URL with PKCE
42
- const authUrl = new URL(`https://${config.clerkDomain}/oauth/authorize`);
38
+ // Build URL to frontend CLI auth page (which shows org picker before Clerk OAuth)
39
+ const authUrl = new URL(`${config.frontendUrl}/cli-auth`);
43
40
  authUrl.searchParams.append('client_id', config.clerkClientId);
44
41
  authUrl.searchParams.append('redirect_uri', config.redirectUri);
45
- authUrl.searchParams.append('response_type', 'code');
46
42
  authUrl.searchParams.append('scope', 'openid profile email');
47
43
  authUrl.searchParams.append('state', state);
48
44
  authUrl.searchParams.append('code_challenge', pkce.challenge);
@@ -76,7 +72,23 @@ export async function login() {
76
72
  return;
77
73
  }
78
74
 
79
- if (returnedState !== state) {
75
+ // Parse state to extract CSRF token, org_id, and org_name
76
+ // State format: base64({ csrf: string, org_id: string|null, org_name: string|null })
77
+ let csrfState = returnedState;
78
+ let selectedOrgId = null;
79
+ let selectedOrgName = null;
80
+
81
+ try {
82
+ const stateData = JSON.parse(atob(returnedState));
83
+ csrfState = stateData.csrf;
84
+ selectedOrgId = stateData.org_id;
85
+ selectedOrgName = stateData.org_name;
86
+ } catch (e) {
87
+ // Old format - state is just the CSRF token
88
+ csrfState = returnedState;
89
+ }
90
+
91
+ if (csrfState !== state) {
80
92
  res.writeHead(400, { 'Content-Type': 'text/html' });
81
93
  res.end(`
82
94
  <html>
@@ -242,10 +254,12 @@ export async function login() {
242
254
  `);
243
255
 
244
256
  // Parse JWT for user info - do not log tokens
257
+ // The id_token should already have org claims because the frontend
258
+ // called setActive() before OAuth redirect
245
259
  const tokenToParse = tokenData.id_token || tokenData.access_token;
246
260
  const decoded = parseJWT(tokenToParse);
247
261
 
248
- // Save token (save both access_token and id_token if available)
262
+ // Save token with selected org from state (since JWT doesn't have org claims)
249
263
  saveToken({
250
264
  accessToken: tokenData.access_token,
251
265
  idToken: tokenData.id_token,
@@ -253,7 +267,10 @@ export async function login() {
253
267
  tokenType: tokenData.token_type || 'Bearer',
254
268
  expiresIn: tokenData.expires_in,
255
269
  receivedAt: Date.now(),
256
- decoded: decoded
270
+ decoded: decoded,
271
+ sessionId: decoded?.sid,
272
+ selectedOrgId: selectedOrgId, // From state parameter
273
+ selectedOrgName: selectedOrgName, // From state parameter
257
274
  });
258
275
 
259
276
  console.log(chalk.green('\nāœ… Authentication successful!'));
@@ -269,95 +286,30 @@ export async function login() {
269
286
  }
270
287
  }
271
288
 
272
- // Close server first
273
- server.close(async () => {
289
+ // Close server and display org context
290
+ server.close(() => {
274
291
  console.log(chalk.gray('āœ“ Callback server closed'));
275
292
 
276
- // Prompt for organization selection
277
- try {
278
- const userId = decoded?.sub;
279
- if (userId) {
280
- // First try to get organizations from JWT token
281
- let memberships = getOrganizationsFromToken(decoded);
282
-
283
- // If not in JWT, fetch from Clerk Frontend API
284
- if (memberships.length === 0) {
285
- const sessionToken = tokenData.id_token || tokenData.access_token;
286
- memberships = await getUserOrganizations(userId, sessionToken);
287
- }
288
-
289
- console.log(chalk.blue('\nšŸ” Checking for organizations...'));
290
- const selectedOrg = await promptOrganizationSelection(memberships);
291
- displayOrganizationSelection(selectedOrg);
292
-
293
- if (selectedOrg) {
294
- // Get org-scoped JWT from Clerk
295
- try {
296
- console.log(chalk.cyan('\nšŸ”„ Getting organization-scoped token...'));
297
-
298
- const sessionId = decoded.sid;
299
- if (!sessionId) {
300
- throw new Error('No session ID found in token');
301
- }
302
-
303
- const orgToken = await getOrgScopedToken(
304
- sessionId,
305
- selectedOrg.id,
306
- tokenData.id_token
307
- );
308
-
309
- // Update saved token with org-scoped version
310
- // Preserve originalSid for recovery purposes (org-scoped token won't have sid)
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
- originalSid: decoded.sid // Preserve session ID from original OAuth token
321
- });
322
-
323
- console.log(chalk.green(`\nāœ… Using organization context: ${chalk.bold(selectedOrg.name)}`));
324
- console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
325
- console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
326
- console.log(chalk.gray(' āœ“ Token updated with organization context'));
327
- } catch (error) {
328
- console.error(chalk.yellow('\nāš ļø Failed to get org-scoped token:'), error.message);
329
- console.log(chalk.gray(' Continuing with original token (may have limited org access)'));
330
-
331
- // Save token with selectedOrg AND originalSid for recovery
332
- // This allows token-validation.js to retry getting org-scoped token later
333
- saveToken({
334
- accessToken: tokenData.access_token,
335
- idToken: tokenData.id_token,
336
- refresh_token: tokenData.refresh_token,
337
- tokenType: 'Bearer',
338
- expiresIn: tokenData.expires_in,
339
- receivedAt: Date.now(),
340
- decoded: decoded,
341
- selectedOrg: selectedOrg,
342
- originalSid: decoded.sid // Preserve session ID for recovery
343
- });
344
-
345
- console.log(chalk.green(`\nāœ… Using organization context: ${chalk.bold(selectedOrg.name)}`));
346
- console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
347
- console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
348
- console.log(chalk.yellow(' āš ļø Token lacks org claims - recovery will be attempted on next use'));
349
- }
350
- } else {
351
- console.log(chalk.green('\nāœ… Using personal account context'));
352
- }
353
-
354
- console.log(chalk.gray('\nYou can switch organizations anytime with: um org switch'));
293
+ // Display organization context - prefer JWT claims, fall back to selected org from state
294
+ // Support both flat claims (org_id) and nested claims (o.o_id) from Clerk JWT templates
295
+ const jwtOrgId = decoded?.org_id || decoded?.o?.o_id;
296
+ const orgId = jwtOrgId || selectedOrgId;
297
+ const orgName = decoded?.org_name || decoded?.o?.o_name || selectedOrgName;
298
+ const orgSlug = decoded?.org_slug || decoded?.o?.o_slug;
299
+ const orgRole = decoded?.org_role || decoded?.o?.o_role;
300
+
301
+ if (orgId) {
302
+ console.log(chalk.green(`\nāœ… Organization context: ${chalk.bold(orgName || orgSlug || orgId)}`));
303
+ console.log(chalk.gray(` Organization ID: ${orgId}`));
304
+ if (orgRole) {
305
+ console.log(chalk.gray(` Your role: ${orgRole}`));
355
306
  }
356
- } catch (error) {
357
- console.log(chalk.yellow('\nāš ļø Could not fetch organizations. Continuing with personal account context.'));
358
- console.log(chalk.gray(`Error: ${error.message}`));
307
+ } else {
308
+ console.log(chalk.green('\nāœ… Using personal account context'));
359
309
  }
360
310
 
311
+ console.log(chalk.gray('\nYou can switch organizations anytime with: um org switch'));
312
+
361
313
  resolve(tokenData);
362
314
  });
363
315
  } catch (error) {
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