@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 CHANGED
@@ -26,14 +26,25 @@ lanonasis --version # or onasis --version
26
26
  onasis guide
27
27
 
28
28
  # Quick manual setup
29
- onasis init # Initialize configuration
30
- onasis login --vendor-key pk_xxx.sk_xxx # Authenticate with vendor key
31
- onasis health # Verify system 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 pk_xxxxx.sk_xxxxx
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": "pk_xxxxx.sk_xxxxx"
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 pk_xxx.sk_xxx
377
+ onasis login --vendor-key <your-vendor-key>
367
378
  ```
368
379
 
369
380
  #### Connection Issues
@@ -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
- await config.set('authMethod', 'oauth2');
682
- console.log();
683
- console.log(chalk.green('✓ OAuth2 authentication successful'));
684
- console.log(colors.info('You can now use Lanonasis services'));
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
- // Store token and user info
770
- await config.setToken(response.token);
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.organization_id) {
776
- console.log(`Organization: ${response.user.organization_id}`);
841
+ if (response.user.role) {
842
+ console.log(`Role: ${response.user.role}`);
777
843
  }
778
- console.log(`Plan: ${response.user.plan || 'free'}`);
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 allMemories = [...new Set([...ownMemories, ...sharedMemories])];
68
- return allMemories;
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
- for (const [name, success] of results) {
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
- for (const interval of this.healthCheckIntervals.values()) {
330
+ this.healthCheckIntervals.forEach(interval => {
331
331
  clearInterval(interval);
332
- }
332
+ });
333
333
  this.healthCheckIntervals.clear();
334
334
  // Disconnect all clients
335
- for (const [name, client] of this.clients) {
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
- for (const connection of this.connectionPool.values()) {
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
- for (const [clientId, connection] of this.connectionPool.entries()) {
956
+ this.connectionPool.forEach((connection, clientId) => {
957
957
  if (connection.lastActivity.getTime() < tenMinutesAgo) {
958
958
  staleConnections.push(clientId);
959
959
  }
960
- }
961
- for (const clientId of staleConnections) {
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
- for (const [transport, failure] of this.transportFailures.entries()) {
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
- for (const [name, transport] of this.transports) {
414
+ this.transports.forEach((transport, name) => {
415
415
  statuses[name] = transport.isConnected();
416
- }
416
+ });
417
417
  return statuses;
418
418
  }
419
419
  /**
@@ -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
- for (const candidate of candidates) {
55
- if (candidate && existsSync(candidate)) {
56
- return candidate;
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('/api/v1/memory', data);
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('/api/v1/memory', { params });
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(`/api/v1/memory/${id}`);
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(`/api/v1/memory/${id}`, data);
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(`/api/v1/memory/${id}`);
120
+ await this.client.delete(`/memory/${id}`);
121
121
  }
122
122
  async searchMemories(query, options = {}) {
123
- const response = await this.client.post('/api/v1/memory/search', {
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('/api/v1/memory/stats');
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('/api/v1/memory/bulk/delete', {
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('/api/v1/topics', data);
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('/api/v1/topics');
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(`/api/v1/topics/${id}`);
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(`/api/v1/topics/${id}`, data);
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(`/api/v1/topics/${id}`);
157
+ await this.client.delete(`/topics/${id}`);
158
158
  }
159
159
  // Health check
160
160
  async getHealth() {
161
- const response = await this.client.get('/api/v1/health');
161
+ const response = await this.client.get('/health');
162
162
  return response.data;
163
163
  }
164
164
  // Generic HTTP methods
@@ -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;
@@ -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://mcp.lanonasis.com/api/v1';
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
- // Skip service discovery in test environment
177
- if (process.env.NODE_ENV === 'test' || process.env.SKIP_SERVICE_DISCOVERY === 'true') {
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
- const discoveryUrl = 'https://mcp.lanonasis.com/.well-known/onasis.json';
191
- try {
192
- // Use axios instead of fetch for consistency
193
- const axios = (await import('axios')).default;
194
- if (verbose) {
195
- console.log(`🔍 Discovering services from ${discoveryUrl}...`);
196
- }
197
- const response = await axios.get(discoveryUrl, {
198
- timeout: 10000,
199
- maxRedirects: 5,
200
- proxy: false, // Bypass proxy to avoid redirect loops
201
- headers: {
202
- 'User-Agent': 'Lanonasis-CLI/3.0.13'
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
- // Map discovery response to our config format
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
- // Enhanced error handling with user-visible messages
235
- await this.handleServiceDiscoveryFailure(error, verbose);
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
- ...this.config.discoveredServices,
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
- const normalizedBase = authBase.replace(/\/$/, '');
459
- // Try multiple validation endpoints
460
- const validationEndpoints = [
461
- `${normalizedBase}/api/v1/auth/validate`,
462
- `${normalizedBase}/api/v1/auth/validate-vendor-key`,
463
- `${normalizedBase}/v1/auth/validate`
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 (error.response?.status === 401) {
501
- const errorData = error.response.data;
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 (error.response?.status === 403) {
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 (error.response?.status === 429) {
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 (error.response?.status >= 500) {
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 (error.code === 'ECONNREFUSED') {
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 (error.code === 'ENOTFOUND') {
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 (error.code === 'ETIMEDOUT') {
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 (error.code === 'ECONNRESET') {
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: ${error.message || 'Unknown error'}`);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.7.2",
3
+ "version": "3.7.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",