@lanonasis/cli 3.7.2 → 3.7.4
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/README.md +18 -7
- package/dist/commands/auth.js +79 -10
- package/dist/mcp/access-control.js +10 -2
- package/dist/mcp/client/enhanced-client.js +6 -6
- package/dist/mcp/schemas/tool-schemas.d.ts +4 -4
- package/dist/mcp/server/lanonasis-server.js +8 -7
- package/dist/mcp/transports/transport-manager.js +2 -2
- package/dist/mcp-server.js +7 -3
- package/dist/utils/api.js +14 -14
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +99 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,14 +26,25 @@ lanonasis --version # or onasis --version
|
|
|
26
26
|
onasis guide
|
|
27
27
|
|
|
28
28
|
# Quick manual setup
|
|
29
|
-
onasis init
|
|
30
|
-
onasis login --vendor-key
|
|
31
|
-
onasis health
|
|
29
|
+
onasis init # Initialize configuration
|
|
30
|
+
onasis login --vendor-key <your-vendor-key> # Authenticate with vendor key
|
|
31
|
+
onasis health # Verify system health
|
|
32
32
|
|
|
33
33
|
# Create your first memory
|
|
34
34
|
onasis memory create --title "Welcome" --content "My first memory"
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## 🤖 Claude Desktop Integration
|
|
38
|
+
|
|
39
|
+
For instant Claude Desktop MCP integration with OAuth2 authentication, see our [Claude Desktop Setup Guide](./CLAUDE_DESKTOP_SETUP.md).
|
|
40
|
+
|
|
41
|
+
**Quick Links**:
|
|
42
|
+
- **Authorization Endpoint**: `https://auth.lanonasis.com/oauth/authorize`
|
|
43
|
+
- **Client ID**: `claude-desktop`
|
|
44
|
+
- **Scopes**: `mcp:full memories:read memories:write`
|
|
45
|
+
|
|
46
|
+
The guide includes complete OAuth2 configuration, available MCP tools, and troubleshooting steps.
|
|
47
|
+
|
|
37
48
|
## 🎯 Command Aliases
|
|
38
49
|
|
|
39
50
|
The CLI supports multiple command aliases for different use cases:
|
|
@@ -74,10 +85,10 @@ onasis login --vendor-key pk_xxxxx.sk_xxxxx // ✅ Automatically hashed
|
|
|
74
85
|
|
|
75
86
|
### 1. Vendor Key Authentication (Recommended)
|
|
76
87
|
|
|
77
|
-
Best for API integrations and automation:
|
|
88
|
+
Best for API integrations and automation. Copy the vendor key value exactly as shown in your LanOnasis dashboard (keys may vary in format):
|
|
78
89
|
|
|
79
90
|
```bash
|
|
80
|
-
onasis login --vendor-key
|
|
91
|
+
onasis login --vendor-key <your-vendor-key>
|
|
81
92
|
```
|
|
82
93
|
|
|
83
94
|
### 2. OAuth Browser Authentication
|
|
@@ -277,7 +288,7 @@ Location: `~/.maas/config.json`
|
|
|
277
288
|
"apiUrl": "https://api.lanonasis.com/api/v1",
|
|
278
289
|
"defaultOutputFormat": "table",
|
|
279
290
|
"mcpPreference": "auto",
|
|
280
|
-
"vendorKey": "
|
|
291
|
+
"vendorKey": "<your-vendor-key>"
|
|
281
292
|
}
|
|
282
293
|
```
|
|
283
294
|
|
|
@@ -363,7 +374,7 @@ onasis auth status
|
|
|
363
374
|
|
|
364
375
|
# Re-authenticate
|
|
365
376
|
onasis auth logout
|
|
366
|
-
onasis login --vendor-key
|
|
377
|
+
onasis login --vendor-key <your-vendor-key>
|
|
367
378
|
```
|
|
368
379
|
|
|
369
380
|
#### Connection Issues
|
package/dist/commands/auth.js
CHANGED
|
@@ -305,6 +305,40 @@ async function refreshOAuth2Token(config) {
|
|
|
305
305
|
return false;
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Exchange Supabase JWT token for auth-gateway API key
|
|
310
|
+
* This enables CLI to work with MCP WebSocket and all services seamlessly
|
|
311
|
+
*/
|
|
312
|
+
async function exchangeSupabaseTokenForApiKey(supabaseToken, config) {
|
|
313
|
+
try {
|
|
314
|
+
const discoveredServices = config.get('discoveredServices');
|
|
315
|
+
const authBase = discoveredServices?.auth_base || 'https://auth.lanonasis.com';
|
|
316
|
+
if (process.env.CLI_VERBOSE === 'true') {
|
|
317
|
+
console.log(chalk.dim(` Exchanging token at: ${authBase}/v1/auth/token/exchange`));
|
|
318
|
+
}
|
|
319
|
+
const response = await axios.post(`${authBase}/v1/auth/token/exchange`, {
|
|
320
|
+
project_scope: 'lanonasis-maas',
|
|
321
|
+
platform: 'cli'
|
|
322
|
+
}, {
|
|
323
|
+
headers: {
|
|
324
|
+
'Authorization': `Bearer ${supabaseToken}`,
|
|
325
|
+
'Content-Type': 'application/json',
|
|
326
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
access_token: response.data.access_token,
|
|
331
|
+
user: response.data.user
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
console.error(chalk.yellow('⚠️ Token exchange failed:', error.message));
|
|
336
|
+
if (process.env.CLI_VERBOSE === 'true' && error.response) {
|
|
337
|
+
console.error(chalk.dim(' Response:', JSON.stringify(error.response.data, null, 2)));
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
308
342
|
export async function diagnoseCommand() {
|
|
309
343
|
const config = new CLIConfig();
|
|
310
344
|
await config.init();
|
|
@@ -674,14 +708,33 @@ async function handleOAuthFlow(config) {
|
|
|
674
708
|
}
|
|
675
709
|
const tokens = await exchangeCodeForTokens(code, pkce.verifier, authBase, redirectUri);
|
|
676
710
|
spinner.succeed('Access tokens received');
|
|
677
|
-
// Store tokens
|
|
711
|
+
// Store OAuth tokens
|
|
678
712
|
await config.setToken(tokens.access_token);
|
|
679
713
|
await config.set('refresh_token', tokens.refresh_token);
|
|
680
714
|
await config.set('token_expires_at', Date.now() + (tokens.expires_in * 1000));
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
715
|
+
// Exchange for unified API key
|
|
716
|
+
spinner.text = 'Configuring unified access...';
|
|
717
|
+
spinner.start();
|
|
718
|
+
const exchangeResult = await exchangeSupabaseTokenForApiKey(tokens.access_token, config);
|
|
719
|
+
if (exchangeResult) {
|
|
720
|
+
// Store the auth-gateway API key for MCP and other services
|
|
721
|
+
await config.setVendorKey(exchangeResult.access_token);
|
|
722
|
+
await config.set('authMethod', 'oauth2');
|
|
723
|
+
spinner.succeed('Unified authentication configured');
|
|
724
|
+
console.log();
|
|
725
|
+
console.log(chalk.green('✓ OAuth2 authentication successful'));
|
|
726
|
+
console.log(colors.info('You can now use all Lanonasis services'));
|
|
727
|
+
console.log(chalk.gray('✓ MCP, API, and CLI access configured'));
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
// Fallback
|
|
731
|
+
await config.set('authMethod', 'oauth2');
|
|
732
|
+
spinner.warn('Token exchange failed, OAuth token stored');
|
|
733
|
+
console.log();
|
|
734
|
+
console.log(chalk.green('✓ OAuth2 authentication successful'));
|
|
735
|
+
console.log(colors.info('You can now use Lanonasis services'));
|
|
736
|
+
console.log(chalk.yellow('⚠️ Some services may require re-authentication'));
|
|
737
|
+
}
|
|
685
738
|
process.exit(0);
|
|
686
739
|
}
|
|
687
740
|
catch (error) {
|
|
@@ -766,16 +819,32 @@ async function handleCredentialsFlow(options, config) {
|
|
|
766
819
|
const spinner = ora('Authenticating...').start();
|
|
767
820
|
try {
|
|
768
821
|
const response = await apiClient.login(email, password);
|
|
769
|
-
|
|
770
|
-
|
|
822
|
+
if (process.env.CLI_VERBOSE === 'true') {
|
|
823
|
+
console.log(chalk.dim(' Login response:'), JSON.stringify(response, null, 2));
|
|
824
|
+
}
|
|
825
|
+
// The auth-gateway login endpoint already returns the correct token format
|
|
826
|
+
// No need to exchange - this token works with all services (MCP, API, CLI)
|
|
827
|
+
const authToken = response.token || response.access_token;
|
|
828
|
+
if (!authToken) {
|
|
829
|
+
throw new Error('No token received from login response');
|
|
830
|
+
}
|
|
831
|
+
if (process.env.CLI_VERBOSE === 'true') {
|
|
832
|
+
console.log(chalk.dim(` JWT received (length: ${authToken.length})`));
|
|
833
|
+
}
|
|
834
|
+
// Store JWT token for API authentication
|
|
835
|
+
await config.setToken(authToken);
|
|
836
|
+
await config.set('authMethod', 'jwt');
|
|
771
837
|
spinner.succeed('Login successful');
|
|
772
838
|
console.log();
|
|
773
839
|
console.log(chalk.green('✓ Authenticated successfully'));
|
|
774
840
|
console.log(`Welcome, ${response.user.email}!`);
|
|
775
|
-
if (response.user.
|
|
776
|
-
console.log(`
|
|
841
|
+
if (response.user.role) {
|
|
842
|
+
console.log(`Role: ${response.user.role}`);
|
|
777
843
|
}
|
|
778
|
-
console.log(
|
|
844
|
+
console.log(chalk.gray('✓ API access configured'));
|
|
845
|
+
console.log();
|
|
846
|
+
console.log(chalk.dim('Note: MCP WebSocket commands require a vendor key.'));
|
|
847
|
+
console.log(chalk.dim('Run'), chalk.white('onasis auth vendor-key <key>'), chalk.dim('to configure MCP access.'));
|
|
779
848
|
}
|
|
780
849
|
catch (error) {
|
|
781
850
|
spinner.fail('Login failed');
|
|
@@ -64,8 +64,16 @@ export class MemoryAccessControl {
|
|
|
64
64
|
// Get shared memories based on permissions
|
|
65
65
|
const sharedMemories = await this.getSharedMemories(userId, appId);
|
|
66
66
|
// Combine and deduplicate
|
|
67
|
-
const
|
|
68
|
-
|
|
67
|
+
const combined = [...ownMemories, ...sharedMemories];
|
|
68
|
+
const deduped = [];
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
combined.forEach(memoryId => {
|
|
71
|
+
if (!seen.has(memoryId)) {
|
|
72
|
+
seen.add(memoryId);
|
|
73
|
+
deduped.push(memoryId);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return deduped;
|
|
69
77
|
}
|
|
70
78
|
catch (error) {
|
|
71
79
|
logger.error('Failed to get accessible memories', { error, userId, appId });
|
|
@@ -45,11 +45,11 @@ export class EnhancedMCPClient extends EventEmitter {
|
|
|
45
45
|
}));
|
|
46
46
|
await Promise.allSettled(connectionPromises);
|
|
47
47
|
// Start health monitoring for connected servers
|
|
48
|
-
|
|
48
|
+
results.forEach((success, name) => {
|
|
49
49
|
if (success) {
|
|
50
50
|
this.startHealthMonitoring(name);
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
});
|
|
53
53
|
return results;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
@@ -327,12 +327,12 @@ export class EnhancedMCPClient extends EventEmitter {
|
|
|
327
327
|
*/
|
|
328
328
|
async disconnectAll() {
|
|
329
329
|
// Stop all health monitoring
|
|
330
|
-
|
|
330
|
+
this.healthCheckIntervals.forEach(interval => {
|
|
331
331
|
clearInterval(interval);
|
|
332
|
-
}
|
|
332
|
+
});
|
|
333
333
|
this.healthCheckIntervals.clear();
|
|
334
334
|
// Disconnect all clients
|
|
335
|
-
|
|
335
|
+
await Promise.all(Array.from(this.clients.entries()).map(async ([name, client]) => {
|
|
336
336
|
try {
|
|
337
337
|
await client.close();
|
|
338
338
|
console.log(chalk.gray(`Disconnected from ${name}`));
|
|
@@ -340,7 +340,7 @@ export class EnhancedMCPClient extends EventEmitter {
|
|
|
340
340
|
catch {
|
|
341
341
|
console.log(chalk.yellow(`Warning: Error disconnecting from ${name}`));
|
|
342
342
|
}
|
|
343
|
-
}
|
|
343
|
+
}));
|
|
344
344
|
this.clients.clear();
|
|
345
345
|
this.transports.clear();
|
|
346
346
|
this.connectionStatus.clear();
|
|
@@ -202,13 +202,13 @@ export declare const SystemConfigSchema: z.ZodObject<{
|
|
|
202
202
|
}, "strip", z.ZodTypeAny, {
|
|
203
203
|
value?: any;
|
|
204
204
|
action?: "get" | "set" | "reset";
|
|
205
|
-
key?: string;
|
|
206
205
|
scope?: "user" | "global";
|
|
206
|
+
key?: string;
|
|
207
207
|
}, {
|
|
208
208
|
value?: any;
|
|
209
209
|
action?: "get" | "set" | "reset";
|
|
210
|
-
key?: string;
|
|
211
210
|
scope?: "user" | "global";
|
|
211
|
+
key?: string;
|
|
212
212
|
}>;
|
|
213
213
|
export declare const BulkOperationSchema: z.ZodObject<{
|
|
214
214
|
operation: z.ZodEnum<["create", "update", "delete"]>;
|
|
@@ -580,13 +580,13 @@ export declare const MCPSchemas: {
|
|
|
580
580
|
}, "strip", z.ZodTypeAny, {
|
|
581
581
|
value?: any;
|
|
582
582
|
action?: "get" | "set" | "reset";
|
|
583
|
-
key?: string;
|
|
584
583
|
scope?: "user" | "global";
|
|
584
|
+
key?: string;
|
|
585
585
|
}, {
|
|
586
586
|
value?: any;
|
|
587
587
|
action?: "get" | "set" | "reset";
|
|
588
|
-
key?: string;
|
|
589
588
|
scope?: "user" | "global";
|
|
589
|
+
key?: string;
|
|
590
590
|
}>;
|
|
591
591
|
};
|
|
592
592
|
operations: {
|
|
@@ -916,7 +916,7 @@ Please choose an option (1-4):`
|
|
|
916
916
|
connectionsByTransport: {}
|
|
917
917
|
};
|
|
918
918
|
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
|
|
919
|
-
|
|
919
|
+
this.connectionPool.forEach(connection => {
|
|
920
920
|
// Count as active if there was activity in the last 5 minutes
|
|
921
921
|
if (connection.lastActivity.getTime() > fiveMinutesAgo) {
|
|
922
922
|
stats.activeConnections++;
|
|
@@ -926,7 +926,7 @@ Please choose an option (1-4):`
|
|
|
926
926
|
}
|
|
927
927
|
stats.connectionsByTransport[connection.transport] =
|
|
928
928
|
(stats.connectionsByTransport[connection.transport] || 0) + 1;
|
|
929
|
-
}
|
|
929
|
+
});
|
|
930
930
|
return stats;
|
|
931
931
|
}
|
|
932
932
|
/**
|
|
@@ -953,12 +953,13 @@ Please choose an option (1-4):`
|
|
|
953
953
|
cleanupStaleConnections() {
|
|
954
954
|
const tenMinutesAgo = Date.now() - (10 * 60 * 1000);
|
|
955
955
|
const staleConnections = [];
|
|
956
|
-
|
|
956
|
+
this.connectionPool.forEach((connection, clientId) => {
|
|
957
957
|
if (connection.lastActivity.getTime() < tenMinutesAgo) {
|
|
958
958
|
staleConnections.push(clientId);
|
|
959
959
|
}
|
|
960
|
-
}
|
|
961
|
-
for (
|
|
960
|
+
});
|
|
961
|
+
for (let i = 0; i < staleConnections.length; i++) {
|
|
962
|
+
const clientId = staleConnections[i];
|
|
962
963
|
this.removeConnection(clientId);
|
|
963
964
|
if (this.options.verbose) {
|
|
964
965
|
console.log(chalk.yellow(`🧹 Cleaned up stale connection: ${clientId}`));
|
|
@@ -1385,12 +1386,12 @@ Please choose an option (1-4):`
|
|
|
1385
1386
|
*/
|
|
1386
1387
|
getTransportStatus() {
|
|
1387
1388
|
const failures = {};
|
|
1388
|
-
|
|
1389
|
+
this.transportFailures.forEach((failure, transport) => {
|
|
1389
1390
|
failures[transport] = {
|
|
1390
1391
|
count: failure.count,
|
|
1391
1392
|
lastFailure: failure.lastFailure.toISOString()
|
|
1392
1393
|
};
|
|
1393
|
-
}
|
|
1394
|
+
});
|
|
1394
1395
|
return {
|
|
1395
1396
|
supportedTransports: this.supportedTransports,
|
|
1396
1397
|
preferredTransport: this.options.preferredTransport || 'stdio',
|
|
@@ -411,9 +411,9 @@ export class MCPTransportManager {
|
|
|
411
411
|
*/
|
|
412
412
|
getStatuses() {
|
|
413
413
|
const statuses = {};
|
|
414
|
-
|
|
414
|
+
this.transports.forEach((transport, name) => {
|
|
415
415
|
statuses[name] = transport.isConnected();
|
|
416
|
-
}
|
|
416
|
+
});
|
|
417
417
|
return statuses;
|
|
418
418
|
}
|
|
419
419
|
/**
|
package/dist/mcp-server.js
CHANGED
|
@@ -51,10 +51,14 @@ export class CLIMCPServer {
|
|
|
51
51
|
}
|
|
52
52
|
candidates.add(join(process.cwd(), 'mcp-server/dist/cli-aligned-mcp-server.js'));
|
|
53
53
|
candidates.add(join(__dirname, '../../../mcp-server/dist/cli-aligned-mcp-server.js'));
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
let resolvedPath = null;
|
|
55
|
+
candidates.forEach(candidate => {
|
|
56
|
+
if (!resolvedPath && candidate && existsSync(candidate)) {
|
|
57
|
+
resolvedPath = candidate;
|
|
57
58
|
}
|
|
59
|
+
});
|
|
60
|
+
if (resolvedPath) {
|
|
61
|
+
return resolvedPath;
|
|
58
62
|
}
|
|
59
63
|
throw new Error('Unable to locate the CLI-aligned MCP server. Set MCP_SERVER_PATH or install @lanonasis/mcp-server.');
|
|
60
64
|
}
|
package/dist/utils/api.js
CHANGED
|
@@ -101,64 +101,64 @@ export class APIClient {
|
|
|
101
101
|
}
|
|
102
102
|
// Memory operations - aligned with existing schema
|
|
103
103
|
async createMemory(data) {
|
|
104
|
-
const response = await this.client.post('/
|
|
104
|
+
const response = await this.client.post('/memory', data);
|
|
105
105
|
return response.data;
|
|
106
106
|
}
|
|
107
107
|
async getMemories(params = {}) {
|
|
108
|
-
const response = await this.client.get('/
|
|
108
|
+
const response = await this.client.get('/memory', { params });
|
|
109
109
|
return response.data;
|
|
110
110
|
}
|
|
111
111
|
async getMemory(id) {
|
|
112
|
-
const response = await this.client.get(`/
|
|
112
|
+
const response = await this.client.get(`/memory/${id}`);
|
|
113
113
|
return response.data;
|
|
114
114
|
}
|
|
115
115
|
async updateMemory(id, data) {
|
|
116
|
-
const response = await this.client.put(`/
|
|
116
|
+
const response = await this.client.put(`/memory/${id}`, data);
|
|
117
117
|
return response.data;
|
|
118
118
|
}
|
|
119
119
|
async deleteMemory(id) {
|
|
120
|
-
await this.client.delete(`/
|
|
120
|
+
await this.client.delete(`/memory/${id}`);
|
|
121
121
|
}
|
|
122
122
|
async searchMemories(query, options = {}) {
|
|
123
|
-
const response = await this.client.post('/
|
|
123
|
+
const response = await this.client.post('/memory/search', {
|
|
124
124
|
query,
|
|
125
125
|
...options
|
|
126
126
|
});
|
|
127
127
|
return response.data;
|
|
128
128
|
}
|
|
129
129
|
async getMemoryStats() {
|
|
130
|
-
const response = await this.client.get('/
|
|
130
|
+
const response = await this.client.get('/memory/stats');
|
|
131
131
|
return response.data;
|
|
132
132
|
}
|
|
133
133
|
async bulkDeleteMemories(memoryIds) {
|
|
134
|
-
const response = await this.client.post('/
|
|
134
|
+
const response = await this.client.post('/memory/bulk/delete', {
|
|
135
135
|
memory_ids: memoryIds
|
|
136
136
|
});
|
|
137
137
|
return response.data;
|
|
138
138
|
}
|
|
139
139
|
// Topic operations - working with existing memory_topics table
|
|
140
140
|
async createTopic(data) {
|
|
141
|
-
const response = await this.client.post('/
|
|
141
|
+
const response = await this.client.post('/topics', data);
|
|
142
142
|
return response.data;
|
|
143
143
|
}
|
|
144
144
|
async getTopics() {
|
|
145
|
-
const response = await this.client.get('/
|
|
145
|
+
const response = await this.client.get('/topics');
|
|
146
146
|
return response.data;
|
|
147
147
|
}
|
|
148
148
|
async getTopic(id) {
|
|
149
|
-
const response = await this.client.get(`/
|
|
149
|
+
const response = await this.client.get(`/topics/${id}`);
|
|
150
150
|
return response.data;
|
|
151
151
|
}
|
|
152
152
|
async updateTopic(id, data) {
|
|
153
|
-
const response = await this.client.put(`/
|
|
153
|
+
const response = await this.client.put(`/topics/${id}`, data);
|
|
154
154
|
return response.data;
|
|
155
155
|
}
|
|
156
156
|
async deleteTopic(id) {
|
|
157
|
-
await this.client.delete(`/
|
|
157
|
+
await this.client.delete(`/topics/${id}`);
|
|
158
158
|
}
|
|
159
159
|
// Health check
|
|
160
160
|
async getHealth() {
|
|
161
|
-
const response = await this.client.get('/
|
|
161
|
+
const response = await this.client.get('/health');
|
|
162
162
|
return response.data;
|
|
163
163
|
}
|
|
164
164
|
// Generic HTTP methods
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -61,7 +61,9 @@ export declare class CLIConfig {
|
|
|
61
61
|
private acquireLock;
|
|
62
62
|
private releaseLock;
|
|
63
63
|
getApiUrl(): string;
|
|
64
|
+
getApiUrlsWithFallbacks(): string[];
|
|
64
65
|
discoverServices(verbose?: boolean): Promise<void>;
|
|
66
|
+
private normalizeServiceError;
|
|
65
67
|
private handleServiceDiscoveryFailure;
|
|
66
68
|
private categorizeServiceDiscoveryError;
|
|
67
69
|
private resolveFallbackEndpoints;
|
package/dist/utils/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as os from 'os';
|
|
|
4
4
|
import { jwtDecode } from 'jwt-decode';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { ApiKeyStorage } from '@lanonasis/oauth-client';
|
|
7
|
+
import axios from 'axios';
|
|
7
8
|
export class CLIConfig {
|
|
8
9
|
configDir;
|
|
9
10
|
configPath;
|
|
@@ -169,12 +170,23 @@ export class CLIConfig {
|
|
|
169
170
|
getApiUrl() {
|
|
170
171
|
return process.env.MEMORY_API_URL ||
|
|
171
172
|
this.config.apiUrl ||
|
|
172
|
-
'https://
|
|
173
|
+
'https://api.lanonasis.com';
|
|
174
|
+
}
|
|
175
|
+
// Get API URLs with fallbacks - try multiple endpoints
|
|
176
|
+
getApiUrlsWithFallbacks() {
|
|
177
|
+
const primary = this.getApiUrl();
|
|
178
|
+
const fallbacks = [
|
|
179
|
+
'https://api.lanonasis.com',
|
|
180
|
+
'https://mcp.lanonasis.com'
|
|
181
|
+
];
|
|
182
|
+
// Remove duplicates and return primary first
|
|
183
|
+
return [primary, ...fallbacks.filter(url => url !== primary)];
|
|
173
184
|
}
|
|
174
185
|
// Enhanced Service Discovery Integration
|
|
175
186
|
async discoverServices(verbose = false) {
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
const isTestEnvironment = process.env.NODE_ENV === 'test';
|
|
188
|
+
const forceDiscovery = process.env.FORCE_SERVICE_DISCOVERY === 'true';
|
|
189
|
+
if ((isTestEnvironment && !forceDiscovery) || process.env.SKIP_SERVICE_DISCOVERY === 'true') {
|
|
178
190
|
if (!this.config.discoveredServices) {
|
|
179
191
|
this.config.discoveredServices = {
|
|
180
192
|
auth_base: 'https://auth.lanonasis.com',
|
|
@@ -187,26 +199,47 @@ export class CLIConfig {
|
|
|
187
199
|
}
|
|
188
200
|
return;
|
|
189
201
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
// Try multiple discovery URLs with fallbacks
|
|
203
|
+
const discoveryUrls = [
|
|
204
|
+
'https://api.lanonasis.com/.well-known/onasis.json',
|
|
205
|
+
'https://mcp.lanonasis.com/.well-known/onasis.json'
|
|
206
|
+
];
|
|
207
|
+
let response = null;
|
|
208
|
+
let lastError = null;
|
|
209
|
+
// Use axios instead of fetch for consistency
|
|
210
|
+
const axios = (await import('axios')).default;
|
|
211
|
+
for (const discoveryUrl of discoveryUrls) {
|
|
212
|
+
try {
|
|
213
|
+
if (verbose) {
|
|
214
|
+
console.log(`🔍 Discovering services from ${discoveryUrl}...`);
|
|
203
215
|
}
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
response = await axios.get(discoveryUrl, {
|
|
217
|
+
timeout: 10000,
|
|
218
|
+
maxRedirects: 5,
|
|
219
|
+
proxy: false, // Bypass proxy to avoid redirect loops
|
|
220
|
+
headers: {
|
|
221
|
+
'User-Agent': 'Lanonasis-CLI/3.0.13'
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
if (verbose) {
|
|
225
|
+
console.log(`✓ Successfully discovered services from ${discoveryUrl}`);
|
|
226
|
+
}
|
|
227
|
+
break; // Success, exit loop
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
lastError = err;
|
|
231
|
+
if (verbose) {
|
|
232
|
+
console.log(`⚠️ Failed to discover from ${discoveryUrl}, trying next...`);
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!response) {
|
|
238
|
+
throw lastError || new Error('All service discovery URLs failed');
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
206
241
|
const discovered = response.data;
|
|
207
|
-
// Extract auth base, but filter out localhost URLs
|
|
208
242
|
let authBase = discovered.auth?.base || discovered.auth?.login?.replace('/auth/login', '') || '';
|
|
209
|
-
// Override localhost with production auth endpoint
|
|
210
243
|
if (authBase.includes('localhost') || authBase.includes('127.0.0.1')) {
|
|
211
244
|
authBase = 'https://auth.lanonasis.com';
|
|
212
245
|
}
|
|
@@ -220,7 +253,6 @@ export class CLIConfig {
|
|
|
220
253
|
project_scope: 'lanonasis-maas'
|
|
221
254
|
};
|
|
222
255
|
this.config.apiUrl = memoryBase;
|
|
223
|
-
// Mark discovery as successful
|
|
224
256
|
this.config.lastServiceDiscovery = new Date().toISOString();
|
|
225
257
|
await this.save();
|
|
226
258
|
if (verbose) {
|
|
@@ -231,9 +263,23 @@ export class CLIConfig {
|
|
|
231
263
|
}
|
|
232
264
|
}
|
|
233
265
|
catch (error) {
|
|
234
|
-
|
|
235
|
-
await this.handleServiceDiscoveryFailure(
|
|
266
|
+
const normalizedError = this.normalizeServiceError(error);
|
|
267
|
+
await this.handleServiceDiscoveryFailure(normalizedError, verbose);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
normalizeServiceError(error) {
|
|
271
|
+
if (error && typeof error === 'object') {
|
|
272
|
+
if (error instanceof Error) {
|
|
273
|
+
return {
|
|
274
|
+
...error,
|
|
275
|
+
message: error.message
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return error;
|
|
236
279
|
}
|
|
280
|
+
return {
|
|
281
|
+
message: typeof error === 'string' ? error : JSON.stringify(error)
|
|
282
|
+
};
|
|
237
283
|
}
|
|
238
284
|
async handleServiceDiscoveryFailure(error, verbose) {
|
|
239
285
|
const errorType = this.categorizeServiceDiscoveryError(error);
|
|
@@ -260,7 +306,6 @@ export class CLIConfig {
|
|
|
260
306
|
console.log(` Reason: ${error.message || 'Unknown error'}`);
|
|
261
307
|
}
|
|
262
308
|
}
|
|
263
|
-
// Use cached endpoints if available and recent (within 24 hours)
|
|
264
309
|
if (this.config.discoveredServices && this.config.lastServiceDiscovery) {
|
|
265
310
|
const lastDiscovery = new Date(this.config.lastServiceDiscovery);
|
|
266
311
|
const hoursSinceDiscovery = (Date.now() - lastDiscovery.getTime()) / (1000 * 60 * 60);
|
|
@@ -277,7 +322,6 @@ export class CLIConfig {
|
|
|
277
322
|
project_scope: 'lanonasis-maas'
|
|
278
323
|
};
|
|
279
324
|
this.config.apiUrl = fallback.endpoints.memory_base;
|
|
280
|
-
// Mark as fallback (don't set lastServiceDiscovery)
|
|
281
325
|
await this.save();
|
|
282
326
|
this.logFallbackUsage(fallback.source, this.config.discoveredServices);
|
|
283
327
|
if (verbose) {
|
|
@@ -297,7 +341,7 @@ export class CLIConfig {
|
|
|
297
341
|
return 'timeout';
|
|
298
342
|
}
|
|
299
343
|
}
|
|
300
|
-
if (error.response?.status >= 500) {
|
|
344
|
+
if ((error.response?.status ?? 0) >= 500) {
|
|
301
345
|
return 'server_error';
|
|
302
346
|
}
|
|
303
347
|
if (error.response?.status === 404) {
|
|
@@ -387,9 +431,17 @@ export class CLIConfig {
|
|
|
387
431
|
// Initialize with defaults first
|
|
388
432
|
await this.discoverServices();
|
|
389
433
|
}
|
|
434
|
+
const currentServices = this.config.discoveredServices ?? {
|
|
435
|
+
auth_base: 'https://auth.lanonasis.com',
|
|
436
|
+
memory_base: 'https://mcp.lanonasis.com/api/v1',
|
|
437
|
+
mcp_base: 'https://mcp.lanonasis.com/api/v1',
|
|
438
|
+
mcp_ws_base: 'wss://mcp.lanonasis.com/ws',
|
|
439
|
+
mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events',
|
|
440
|
+
project_scope: 'lanonasis-maas'
|
|
441
|
+
};
|
|
390
442
|
// Merge manual overrides with existing endpoints
|
|
391
443
|
this.config.discoveredServices = {
|
|
392
|
-
...
|
|
444
|
+
...currentServices,
|
|
393
445
|
...endpoints
|
|
394
446
|
};
|
|
395
447
|
// Mark as manually configured
|
|
@@ -449,56 +501,26 @@ export class CLIConfig {
|
|
|
449
501
|
return true;
|
|
450
502
|
}
|
|
451
503
|
async validateVendorKeyWithServer(vendorKey) {
|
|
504
|
+
if (process.env.SKIP_SERVER_VALIDATION === 'true') {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
452
507
|
try {
|
|
453
508
|
// Import axios dynamically to avoid circular dependency
|
|
454
|
-
const axios = (await import('axios')).default;
|
|
455
509
|
// Ensure service discovery is done
|
|
456
510
|
await this.discoverServices();
|
|
457
511
|
const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
];
|
|
465
|
-
let lastError;
|
|
466
|
-
let validated = false;
|
|
467
|
-
for (const endpoint of validationEndpoints) {
|
|
468
|
-
try {
|
|
469
|
-
const response = await axios.post(endpoint, { key: vendorKey }, {
|
|
470
|
-
headers: {
|
|
471
|
-
'X-API-Key': vendorKey,
|
|
472
|
-
'X-Auth-Method': 'vendor_key',
|
|
473
|
-
'X-Project-Scope': 'lanonasis-maas',
|
|
474
|
-
'Content-Type': 'application/json'
|
|
475
|
-
},
|
|
476
|
-
timeout: 10000,
|
|
477
|
-
proxy: false
|
|
478
|
-
});
|
|
479
|
-
// Check if response indicates validation success
|
|
480
|
-
if (response.data && (response.data.valid === true || response.data.success === true)) {
|
|
481
|
-
validated = true;
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch (error) {
|
|
486
|
-
lastError = error;
|
|
487
|
-
// Continue to next endpoint if this one fails
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
if (!validated && lastError) {
|
|
492
|
-
throw lastError;
|
|
493
|
-
}
|
|
494
|
-
else if (!validated) {
|
|
495
|
-
throw new Error('Vendor key validation failed: Unable to validate key with server');
|
|
496
|
-
}
|
|
512
|
+
// Use pingAuthHealth for validation (simpler and more reliable)
|
|
513
|
+
await this.pingAuthHealth(axios, authBase, {
|
|
514
|
+
'X-API-Key': vendorKey,
|
|
515
|
+
'X-Auth-Method': 'vendor_key',
|
|
516
|
+
'X-Project-Scope': 'lanonasis-maas'
|
|
517
|
+
}, { timeout: 10000, proxy: false });
|
|
497
518
|
}
|
|
498
519
|
catch (error) {
|
|
520
|
+
const normalizedError = this.normalizeServiceError(error);
|
|
499
521
|
// Provide specific error messages based on response
|
|
500
|
-
if (
|
|
501
|
-
const errorData =
|
|
522
|
+
if (normalizedError.response?.status === 401) {
|
|
523
|
+
const errorData = normalizedError.response.data;
|
|
502
524
|
if (errorData?.error?.includes('expired') || errorData?.message?.includes('expired')) {
|
|
503
525
|
throw new Error('Vendor key validation failed: Key has expired. Please generate a new key from your dashboard.');
|
|
504
526
|
}
|
|
@@ -512,29 +534,29 @@ export class CLIConfig {
|
|
|
512
534
|
throw new Error('Vendor key validation failed: Authentication failed. The key may be invalid, expired, or revoked.');
|
|
513
535
|
}
|
|
514
536
|
}
|
|
515
|
-
else if (
|
|
537
|
+
else if (normalizedError.response?.status === 403) {
|
|
516
538
|
throw new Error('Vendor key access denied. The key may not have sufficient permissions for this operation.');
|
|
517
539
|
}
|
|
518
|
-
else if (
|
|
540
|
+
else if (normalizedError.response?.status === 429) {
|
|
519
541
|
throw new Error('Too many validation attempts. Please wait a moment before trying again.');
|
|
520
542
|
}
|
|
521
|
-
else if (
|
|
543
|
+
else if ((normalizedError.response?.status ?? 0) >= 500) {
|
|
522
544
|
throw new Error('Server error during validation. Please try again in a few moments.');
|
|
523
545
|
}
|
|
524
|
-
else if (
|
|
546
|
+
else if (normalizedError.code === 'ECONNREFUSED') {
|
|
525
547
|
throw new Error('Cannot connect to authentication server. Please check your internet connection and try again.');
|
|
526
548
|
}
|
|
527
|
-
else if (
|
|
549
|
+
else if (normalizedError.code === 'ENOTFOUND') {
|
|
528
550
|
throw new Error('Authentication server not found. Please check your internet connection.');
|
|
529
551
|
}
|
|
530
|
-
else if (
|
|
552
|
+
else if (normalizedError.code === 'ETIMEDOUT') {
|
|
531
553
|
throw new Error('Validation request timed out. Please check your internet connection and try again.');
|
|
532
554
|
}
|
|
533
|
-
else if (
|
|
555
|
+
else if (normalizedError.code === 'ECONNRESET') {
|
|
534
556
|
throw new Error('Connection was reset during validation. Please try again.');
|
|
535
557
|
}
|
|
536
558
|
else {
|
|
537
|
-
throw new Error(`Vendor key validation failed: ${
|
|
559
|
+
throw new Error(`Vendor key validation failed: ${normalizedError.message || 'Unknown error'}`);
|
|
538
560
|
}
|
|
539
561
|
}
|
|
540
562
|
}
|
|
@@ -697,7 +719,6 @@ export class CLIConfig {
|
|
|
697
719
|
// If not locally valid, attempt server verification before failing
|
|
698
720
|
if (!locallyValid) {
|
|
699
721
|
try {
|
|
700
|
-
const axios = (await import('axios')).default;
|
|
701
722
|
const endpoints = [
|
|
702
723
|
'http://localhost:4000/v1/auth/verify-token',
|
|
703
724
|
'https://auth.lanonasis.com/v1/auth/verify-token'
|
|
@@ -734,7 +755,6 @@ export class CLIConfig {
|
|
|
734
755
|
}
|
|
735
756
|
// Verify with server (security check) for tokens that haven't been validated recently
|
|
736
757
|
try {
|
|
737
|
-
const axios = (await import('axios')).default;
|
|
738
758
|
// Try auth-gateway first (port 4000), then fall back to Netlify function
|
|
739
759
|
const endpoints = [
|
|
740
760
|
'http://localhost:4000/v1/auth/verify-token',
|
|
@@ -846,7 +866,6 @@ export class CLIConfig {
|
|
|
846
866
|
return false;
|
|
847
867
|
}
|
|
848
868
|
// Import axios dynamically to avoid circular dependency
|
|
849
|
-
const axios = (await import('axios')).default;
|
|
850
869
|
// Ensure service discovery is done
|
|
851
870
|
await this.discoverServices();
|
|
852
871
|
const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
|
|
@@ -891,7 +910,6 @@ export class CLIConfig {
|
|
|
891
910
|
// Refresh if token expires within 5 minutes
|
|
892
911
|
if (exp > 0 && (exp - now) < 300) {
|
|
893
912
|
// Import axios dynamically
|
|
894
|
-
const axios = (await import('axios')).default;
|
|
895
913
|
await this.discoverServices();
|
|
896
914
|
const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
|
|
897
915
|
// Attempt token refresh
|