@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/.env.example +18 -0
- package/README.md +28 -0
- package/commands/debug.js +67 -0
- package/commands/init.js +17 -8
- package/commands/login.js +138 -101
- package/commands/org.js +25 -177
- package/commands/record.js +14 -11
- package/commands/usage.js +163 -0
- package/index.js +37 -6
- package/lib/clerk-api.js +9 -12
- package/lib/config.js +21 -1
- package/lib/mcp-proxy.js +9 -10
- package/lib/mcp-server.js +139 -34
- package/lib/token-refresh.js +10 -24
- package/lib/token-storage.js +33 -27
- package/lib/token-validation.js +2 -85
- package/package.json +1 -1
- package/tests/unit/mcp-proxy.test.js +9 -6
- package/tests/unit/token-refresh.test.js +18 -44
- package/tests/unit/token-storage.test.js +1 -2
- package/tests/unit/token-validation.test.js +24 -241
package/commands/org.js
CHANGED
|
@@ -1,115 +1,30 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
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
|
-
|
|
14
|
-
const tokenData = await loadAndRefreshToken();
|
|
9
|
+
console.log(chalk.blue('\nš Opening browser to select organization...\n'));
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
const
|
|
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
|
|
20
|
+
const orgContext = getOrgContext();
|
|
106
21
|
|
|
107
22
|
console.log(chalk.blue('\nš Current Organization Context\n'));
|
|
108
23
|
|
|
109
|
-
if (
|
|
110
|
-
console.log(chalk.green(` ${
|
|
111
|
-
console.log(chalk.gray(` ID: ${
|
|
112
|
-
console.log(chalk.gray(` 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
|
|
123
|
-
*
|
|
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
|
|
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.
|
|
44
|
+
console.log(chalk.blue('\nš§ Organization Token Check\n'));
|
|
156
45
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
}
|
package/commands/record.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 (
|
|
98
|
-
console.log(chalk.green(` ${
|
|
99
|
-
console.log(chalk.gray(` ID: ${
|
|
100
|
-
console.log(chalk.gray(` 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
injected.
|
|
114
|
-
|
|
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) {
|