@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.
@@ -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
@@ -22,7 +22,10 @@ export const config = {
22
22
  port: parseInt(process.env.PORT || '3333', 10),
23
23
 
24
24
  // OAuth success page configuration (optional website link for account management)
25
- oauthSuccessUrl: process.env.OAUTH_SUCCESS_URL || 'https://unifiedmemory.ai/oauth/callback'
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'
26
29
  };
27
30
 
28
31
  // Validation function - validates configuration values
@@ -51,5 +54,12 @@ export function validateConfig() {
51
54
  throw new Error(`OAUTH_SUCCESS_URL must be a valid URL (got: ${config.oauthSuccessUrl})`);
52
55
  }
53
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
+
54
64
  return true;
55
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) {
package/lib/mcp-server.js CHANGED
@@ -11,28 +11,32 @@ import path from 'path';
11
11
  import os from 'os';
12
12
  import { loadAndRefreshToken } from './token-validation.js';
13
13
  import { fetchRemoteMCPTools, callRemoteMCPTool } from './mcp-proxy.js';
14
+ import { config } from './config.js';
15
+
16
+ /**
17
+ * Usage cache to avoid spamming quota API
18
+ * Map: orgId -> { data: QuotaResponse, timestamp: number }
19
+ */
20
+ const usageCache = new Map();
21
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
22
+ const UPGRADE_THRESHOLD = 90; // Show prompt at 90%+
14
23
 
15
24
  /**
16
25
  * Start the MCP server on stdio transport
17
26
  */
18
27
  export async function startMCPServer() {
19
28
  try {
20
- // 1. Load and validate authentication
21
- const authData = await loadAndValidateAuth();
29
+ // 1. Validate authentication at startup for immediate failure
30
+ await loadAndValidateAuth();
22
31
 
23
- // 2. Load project context from current directory
32
+ // 2. Load project context from current directory (this doesn't change)
24
33
  const projectContext = loadProjectContext();
25
34
 
26
- // 3. Build auth headers
27
- const authHeaders = buildAuthHeaders(authData, projectContext);
35
+ // Note: We don't cache authData here anymore. Instead, we reload it
36
+ // on each request to ensure we always have the current token state,
37
+ // even after token refreshes.
28
38
 
29
- // 4. Extract auth context for parameter injection
30
- const authContext = {
31
- decoded: authData.decoded,
32
- selectedOrg: authData.selectedOrg
33
- };
34
-
35
- // 5. Create MCP server
39
+ // 3. Create MCP server
36
40
  const server = new Server(
37
41
  {
38
42
  name: "unifiedmemory",
@@ -46,22 +50,25 @@ export async function startMCPServer() {
46
50
  }
47
51
  );
48
52
 
49
- // 6. Register handlers
53
+ // 4. Register handlers - reload auth on each request
50
54
  server.setRequestHandler(ListToolsRequestSchema, async () => {
55
+ const { authHeaders } = await loadFreshAuth(projectContext);
51
56
  return await handleListTools(authHeaders, projectContext);
52
57
  });
53
58
 
54
59
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
60
+ const { authHeaders, authContext } = await loadFreshAuth(projectContext);
55
61
  return await handleCallTool(request, authHeaders, authContext, projectContext);
56
62
  });
57
63
 
58
64
  // Register resource handlers for authentication context
59
65
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
66
+ // Reload auth for fresh context
67
+ const authData = await loadAndRefreshToken();
60
68
  const resources = [];
61
69
 
62
- // Add org_id resource
63
- const orgId = authData.selectedOrg?.id || authData.decoded?.sub;
64
- if (orgId) {
70
+ // Add org_id resource only if JWT has org context
71
+ if (authData.decoded?.o?.o_id) {
65
72
  resources.push({
66
73
  uri: "um://context/org_id",
67
74
  name: "Organization ID",
@@ -95,14 +102,17 @@ export async function startMCPServer() {
95
102
  });
96
103
 
97
104
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
105
+ // Reload auth for fresh context
106
+ const authData = await loadAndRefreshToken();
98
107
  const { uri } = request.params;
99
108
  console.error(`→ Reading resource: ${uri}`);
100
109
 
101
110
  switch(uri) {
102
111
  case "um://context/org_id": {
103
- const orgId = authData.selectedOrg?.id || authData.decoded?.sub || "";
112
+ // Only return org_id if JWT has org claims
113
+ const orgId = authData.decoded?.o?.o_id;
104
114
  if (!orgId) {
105
- throw new Error("Organization context not available");
115
+ throw new Error("Organization context not available (using personal account)");
106
116
  }
107
117
  return {
108
118
  contents: [{
@@ -193,6 +203,7 @@ function loadProjectContext() {
193
203
 
194
204
  /**
195
205
  * Build authentication headers for gateway requests
206
+ * Checks JWT claims first, falls back to selectedOrgId from login
196
207
  * @param {Object} authData - Token data
197
208
  * @param {Object|null} projectContext - Project config
198
209
  * @returns {Object} - Headers object
@@ -202,21 +213,22 @@ function buildAuthHeaders(authData, projectContext) {
202
213
  'Authorization': `Bearer ${authData.idToken || authData.accessToken}`,
203
214
  };
204
215
 
205
- // Add org context
206
- if (authData.selectedOrg) {
207
- headers['X-Org-Id'] = authData.selectedOrg.id;
208
- console.error(`āœ“ Organization: ${authData.selectedOrg.name} (${authData.selectedOrg.id})`);
209
- } else if (authData.decoded?.sub) {
210
- // Fallback to user ID for personal account
211
- headers['X-Org-Id'] = authData.decoded.sub;
212
- console.error(`āœ“ Using personal account (${authData.decoded.sub})`);
213
- }
214
-
215
- // Add user ID from JWT
216
+ // Add user ID from JWT (always present)
216
217
  if (authData.decoded?.sub) {
217
218
  headers['X-User-Id'] = authData.decoded.sub;
218
219
  }
219
220
 
221
+ // Add X-Org-Id - check JWT claims first, fall back to selectedOrgId
222
+ const orgId = authData.decoded?.o?.o_id || authData.selectedOrgId;
223
+ const orgName = authData.decoded?.o?.o_name || authData.selectedOrgName;
224
+
225
+ if (orgId) {
226
+ headers['X-Org-Id'] = orgId;
227
+ console.error(`āœ“ Organization: ${orgName || orgId}`);
228
+ } else {
229
+ console.error(`āœ“ Using personal account context`);
230
+ }
231
+
220
232
  // Add project context if available
221
233
  if (projectContext) {
222
234
  headers['X-Project-Id'] = projectContext.project_id;
@@ -226,6 +238,93 @@ function buildAuthHeaders(authData, projectContext) {
226
238
  return headers;
227
239
  }
228
240
 
241
+ /**
242
+ * Load fresh authentication data and build headers
243
+ * Called on each MCP request to ensure current token state
244
+ * Checks JWT claims first, falls back to selectedOrgId from login
245
+ * @param {Object|null} projectContext - Project config
246
+ * @returns {Promise<{authHeaders: Object, authContext: Object}>}
247
+ */
248
+ async function loadFreshAuth(projectContext) {
249
+ const authData = await loadAndRefreshToken();
250
+ const authHeaders = buildAuthHeaders(authData, projectContext);
251
+
252
+ // Build auth context - check JWT claims first, fall back to selectedOrgId
253
+ const jwtOrgId = authData.decoded?.o?.o_id;
254
+ const orgId = jwtOrgId || authData.selectedOrgId;
255
+ const orgName = authData.decoded?.o?.o_name || authData.selectedOrgName;
256
+ const orgRole = authData.decoded?.o?.o_role;
257
+
258
+ const authContext = {
259
+ decoded: authData.decoded,
260
+ orgContext: orgId ? {
261
+ id: orgId,
262
+ name: orgName,
263
+ role: orgRole,
264
+ } : null
265
+ };
266
+
267
+ return { authHeaders, authContext };
268
+ }
269
+
270
+ /**
271
+ * Check quota usage and return upgrade prompt if needed
272
+ * Uses caching to avoid API spam (5-min TTL)
273
+ */
274
+ async function checkAndGetUpgradePrompt(authHeaders, authContext) {
275
+ try {
276
+ // Use org from JWT claims, or userId for personal context
277
+ const orgId = authContext?.orgContext?.id || authContext?.decoded?.sub;
278
+ if (!orgId) return null;
279
+
280
+ // Check cache
281
+ const cached = usageCache.get(orgId);
282
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
283
+ return formatUpgradePrompt(cached.data, authContext);
284
+ }
285
+
286
+ // Fetch fresh data
287
+ const response = await fetch(`${config.apiEndpoint}/v1/quota/usage`, {
288
+ headers: authHeaders
289
+ });
290
+
291
+ if (!response.ok) {
292
+ console.error(`āš ļø Quota check failed: ${response.status}`);
293
+ return null; // Fail silently, don't break MCP
294
+ }
295
+
296
+ const data = await response.json();
297
+
298
+ // Update cache
299
+ usageCache.set(orgId, { data, timestamp: Date.now() });
300
+
301
+ return formatUpgradePrompt(data, authContext);
302
+
303
+ } catch (error) {
304
+ console.error(`āš ļø Quota check error: ${error.message}`);
305
+ return null; // Fail silently
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Format upgrade prompt if usage >= threshold
311
+ */
312
+ function formatUpgradePrompt(quotaData, authContext) {
313
+ const { percentage_used, used, total } = quotaData.usage;
314
+
315
+ if (percentage_used < UPGRADE_THRESHOLD) return null;
316
+
317
+ const isOrg = quotaData.quota_type === 'organization';
318
+ const contextName = isOrg
319
+ ? authContext?.orgContext?.name
320
+ : 'your account';
321
+
322
+ return {
323
+ type: "text",
324
+ text: `\n---\nāš ļø Quota Alert: ${contextName} is at ${percentage_used.toFixed(1)}% capacity (${used.toLocaleString()}/${total.toLocaleString()} queries).\nUpgrade your plan at https://unifiedmemory.ai/pricing to unlock more capacity.\n---`
325
+ };
326
+ }
327
+
229
328
  /**
230
329
  * Handle tools/list request
231
330
  * @param {Object} authHeaders - Auth headers
@@ -260,12 +359,18 @@ async function handleCallTool(request, authHeaders, authContext, projectContext)
260
359
  const result = await callRemoteMCPTool(name, args, authHeaders, authContext, projectContext);
261
360
  console.error(`āœ“ Tool executed successfully: ${name}`);
262
361
 
362
+ // Check if upgrade prompt needed (cached, 5-min TTL)
363
+ const upgradePrompt = await checkAndGetUpgradePrompt(authHeaders, authContext);
364
+
263
365
  return {
264
- content: result.content || [
265
- {
266
- type: "text",
267
- text: JSON.stringify(result, null, 2),
268
- },
366
+ content: [
367
+ ...(result.content || [
368
+ {
369
+ type: "text",
370
+ text: JSON.stringify(result, null, 2),
371
+ },
372
+ ]),
373
+ ...(upgradePrompt ? [upgradePrompt] : [])
269
374
  ],
270
375
  };
271
376
  } catch (error) {
@@ -1,7 +1,6 @@
1
- import { getToken, saveToken } from './token-storage.js';
1
+ import { saveToken } from './token-storage.js';
2
2
  import { config } from './config.js';
3
3
  import { parseJWT } from './jwt-utils.js';
4
- import { getOrgScopedToken } from './clerk-api.js';
5
4
 
6
5
  /**
7
6
  * Check if token has expired
@@ -23,6 +22,7 @@ export function isTokenExpired(tokenData) {
23
22
 
24
23
  /**
25
24
  * Refresh access token using refresh token
25
+ * Simple refresh - Clerk preserves org context if it was set
26
26
  * @param {Object} tokenData - Current token data
27
27
  * @returns {Promise<Object>} - New token data
28
28
  */
@@ -74,36 +74,22 @@ export async function refreshAccessToken(tokenData) {
74
74
 
75
75
  // Parse refreshed JWT
76
76
  const refreshedToken = newTokenData.id_token || newTokenData.access_token;
77
- let finalIdToken = newTokenData.id_token;
78
- let decoded = parseJWT(refreshedToken);
77
+ const decoded = parseJWT(refreshedToken);
79
78
 
80
- // If we have org context, get org-scoped token to ensure JWT has org claims
81
- if (tokenData.selectedOrg?.id && decoded?.sid) {
82
- try {
83
- const orgToken = await getOrgScopedToken(
84
- decoded.sid,
85
- tokenData.selectedOrg.id,
86
- refreshedToken
87
- );
88
- finalIdToken = orgToken.jwt;
89
- decoded = parseJWT(orgToken.jwt);
90
- } catch (error) {
91
- // Log warning but continue with base token
92
- // The subsequent API call may fail, prompting re-login
93
- console.error(`āš ļø Could not refresh org-scoped token: ${error.message}`);
94
- }
95
- }
96
-
97
- // Build updated token object, preserving selectedOrg
79
+ // Build updated token object
80
+ // Note: Clerk preserves org context through refresh automatically
81
+ // We also preserve selectedOrgId/selectedOrgName from login state
98
82
  const updatedToken = {
99
83
  accessToken: newTokenData.access_token,
100
- idToken: finalIdToken,
84
+ idToken: newTokenData.id_token,
101
85
  tokenType: newTokenData.token_type || 'Bearer',
102
86
  expiresIn: newTokenData.expires_in,
103
87
  receivedAt: Date.now(),
104
88
  decoded: decoded,
105
- selectedOrg: tokenData.selectedOrg, // Preserve organization context
106
89
  refresh_token: newTokenData.refresh_token || tokenData.refresh_token,
90
+ sessionId: tokenData.sessionId, // Preserve sessionId through refresh
91
+ selectedOrgId: tokenData.selectedOrgId, // Preserve org selection through refresh
92
+ selectedOrgName: tokenData.selectedOrgName, // Preserve org name through refresh
107
93
  };
108
94
 
109
95
  // Save to storage