@lanonasis/cli 3.1.13 → 3.2.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.
@@ -4,12 +4,44 @@ interface UserProfile {
4
4
  role: string;
5
5
  plan: string;
6
6
  }
7
+ interface CLIConfigData {
8
+ version?: string;
9
+ apiUrl?: string;
10
+ token?: string | undefined;
11
+ user?: UserProfile | undefined;
12
+ lastUpdated?: string;
13
+ mcpServerPath?: string;
14
+ mcpServerUrl?: string;
15
+ mcpUseRemote?: boolean;
16
+ mcpPreference?: 'local' | 'remote' | 'auto';
17
+ discoveredServices?: {
18
+ auth_base: string;
19
+ memory_base: string;
20
+ mcp_base?: string;
21
+ mcp_ws_base: string;
22
+ mcp_sse_base?: string;
23
+ project_scope: string;
24
+ };
25
+ lastServiceDiscovery?: string;
26
+ manualEndpointOverrides?: boolean;
27
+ lastManualEndpointUpdate?: string;
28
+ vendorKey?: string | undefined;
29
+ authMethod?: 'jwt' | 'vendor_key' | 'oauth' | undefined;
30
+ tokenExpiry?: number | undefined;
31
+ lastValidated?: string | undefined;
32
+ deviceId?: string;
33
+ authFailureCount?: number;
34
+ lastAuthFailure?: string | undefined;
35
+ [key: string]: unknown;
36
+ }
7
37
  export declare class CLIConfig {
8
38
  private configDir;
9
39
  private configPath;
10
40
  private config;
11
41
  private lockFile;
12
42
  private static readonly CONFIG_VERSION;
43
+ private authCheckCache;
44
+ private readonly AUTH_CACHE_TTL;
13
45
  constructor();
14
46
  init(): Promise<void>;
15
47
  load(): Promise<void>;
@@ -20,9 +52,15 @@ export declare class CLIConfig {
20
52
  private acquireLock;
21
53
  private releaseLock;
22
54
  getApiUrl(): string;
23
- discoverServices(): Promise<void>;
55
+ discoverServices(verbose?: boolean): Promise<void>;
56
+ private handleServiceDiscoveryFailure;
57
+ private categorizeServiceDiscoveryError;
58
+ setManualEndpoints(endpoints: Partial<CLIConfigData['discoveredServices']>): Promise<void>;
59
+ hasManualEndpointOverrides(): boolean;
60
+ clearManualEndpointOverrides(): Promise<void>;
24
61
  getDiscoveredApiUrl(): string;
25
62
  setVendorKey(vendorKey: string): Promise<void>;
63
+ validateVendorKeyFormat(vendorKey: string): string | boolean;
26
64
  private validateVendorKeyWithServer;
27
65
  getVendorKey(): string | undefined;
28
66
  hasVendorKey(): boolean;
@@ -44,6 +82,7 @@ export declare class CLIConfig {
44
82
  getLastAuthFailure(): string | undefined;
45
83
  shouldDelayAuth(): boolean;
46
84
  getAuthDelayMs(): number;
85
+ getDeviceId(): Promise<string>;
47
86
  get<T = unknown>(key: string): T;
48
87
  set(key: string, value: unknown): void;
49
88
  setAndSave(key: string, value: unknown): Promise<void>;
@@ -9,6 +9,8 @@ export class CLIConfig {
9
9
  config = {};
10
10
  lockFile;
11
11
  static CONFIG_VERSION = '1.0.0';
12
+ authCheckCache = null;
13
+ AUTH_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
12
14
  constructor() {
13
15
  this.configDir = path.join(os.homedir(), '.maas');
14
16
  this.configPath = path.join(this.configDir, 'config.json');
@@ -151,13 +153,21 @@ export class CLIConfig {
151
153
  this.config.apiUrl ||
152
154
  'https://api.lanonasis.com/api/v1';
153
155
  }
154
- // Service Discovery Integration
155
- async discoverServices() {
156
+ // Enhanced Service Discovery Integration
157
+ async discoverServices(verbose = false) {
158
+ const discoveryUrl = 'https://mcp.lanonasis.com/.well-known/onasis.json';
156
159
  try {
157
160
  // Use axios instead of fetch for consistency
158
161
  const axios = (await import('axios')).default;
159
- const discoveryUrl = 'https://mcp.lanonasis.com/.well-known/onasis.json';
160
- const response = await axios.get(discoveryUrl);
162
+ if (verbose) {
163
+ console.log(`🔍 Discovering services from ${discoveryUrl}...`);
164
+ }
165
+ const response = await axios.get(discoveryUrl, {
166
+ timeout: 10000,
167
+ headers: {
168
+ 'User-Agent': 'Lanonasis-CLI/3.0.13'
169
+ }
170
+ });
161
171
  // Map discovery response to our config format
162
172
  const discovered = response.data;
163
173
  this.config.discoveredServices = {
@@ -168,34 +178,134 @@ export class CLIConfig {
168
178
  mcp_sse_base: discovered.endpoints?.sse || 'https://mcp.lanonasis.com/api/v1/events',
169
179
  project_scope: 'lanonasis-maas'
170
180
  };
181
+ // Mark discovery as successful
182
+ this.config.lastServiceDiscovery = new Date().toISOString();
171
183
  await this.save();
184
+ if (verbose) {
185
+ console.log('✓ Service discovery completed successfully');
186
+ console.log(` Auth: ${this.config.discoveredServices.auth_base}`);
187
+ console.log(` MCP: ${this.config.discoveredServices.mcp_base}`);
188
+ console.log(` WebSocket: ${this.config.discoveredServices.mcp_ws_base}`);
189
+ }
172
190
  }
173
- catch {
174
- // Service discovery failed, use fallback defaults
175
- if (process.env.CLI_VERBOSE === 'true') {
176
- console.log('Service discovery failed, using fallback defaults');
191
+ catch (error) {
192
+ // Enhanced error handling with user-visible messages
193
+ await this.handleServiceDiscoveryFailure(error, verbose);
194
+ }
195
+ }
196
+ async handleServiceDiscoveryFailure(error, verbose) {
197
+ const errorType = this.categorizeServiceDiscoveryError(error);
198
+ if (verbose || process.env.CLI_VERBOSE === 'true') {
199
+ console.log('⚠️ Service discovery failed, using cached/fallback endpoints');
200
+ switch (errorType) {
201
+ case 'network_error':
202
+ console.log(' Reason: Network connection failed');
203
+ console.log(' This is normal when offline or behind restrictive firewalls');
204
+ break;
205
+ case 'timeout':
206
+ console.log(' Reason: Request timed out');
207
+ console.log(' The discovery service may be temporarily slow');
208
+ break;
209
+ case 'server_error':
210
+ console.log(' Reason: Discovery service returned an error');
211
+ console.log(' The service may be temporarily unavailable');
212
+ break;
213
+ case 'invalid_response':
214
+ console.log(' Reason: Invalid response format from discovery service');
215
+ console.log(' Using known working endpoints instead');
216
+ break;
217
+ default:
218
+ console.log(` Reason: ${error.message || 'Unknown error'}`);
219
+ }
220
+ }
221
+ // Use cached endpoints if available and recent (within 24 hours)
222
+ if (this.config.discoveredServices && this.config.lastServiceDiscovery) {
223
+ const lastDiscovery = new Date(this.config.lastServiceDiscovery);
224
+ const hoursSinceDiscovery = (Date.now() - lastDiscovery.getTime()) / (1000 * 60 * 60);
225
+ if (hoursSinceDiscovery < 24) {
226
+ if (verbose) {
227
+ console.log('✓ Using cached service endpoints (less than 24 hours old)');
228
+ }
229
+ return;
230
+ }
231
+ }
232
+ // Set fallback service endpoints
233
+ this.config.discoveredServices = {
234
+ auth_base: 'https://api.lanonasis.com', // CLI auth goes to central auth system
235
+ memory_base: 'https://api.lanonasis.com/api/v1', // Memory via onasis-core
236
+ mcp_base: 'https://mcp.lanonasis.com/api/v1', // MCP HTTP/REST
237
+ mcp_ws_base: 'wss://mcp.lanonasis.com/ws', // MCP WebSocket
238
+ mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events', // MCP SSE
239
+ project_scope: 'lanonasis-maas' // Correct project scope
240
+ };
241
+ // Mark as fallback (don't set lastServiceDiscovery)
242
+ await this.save();
243
+ if (verbose) {
244
+ console.log('✓ Using fallback service endpoints');
245
+ console.log(' These are the standard production endpoints');
246
+ }
247
+ }
248
+ categorizeServiceDiscoveryError(error) {
249
+ if (error.code) {
250
+ switch (error.code) {
251
+ case 'ECONNREFUSED':
252
+ case 'ENOTFOUND':
253
+ case 'ECONNRESET':
254
+ case 'ENETUNREACH':
255
+ return 'network_error';
256
+ case 'ETIMEDOUT':
257
+ return 'timeout';
177
258
  }
178
- // Set fallback service endpoints to prevent double slash issues
179
- // Use mcp.lanonasis.com for MCP services (proxied to port 3001)
180
- this.config.discoveredServices = {
181
- auth_base: 'https://api.lanonasis.com', // CLI auth goes to central auth system
182
- memory_base: 'https://api.lanonasis.com/api/v1', // Memory via onasis-core
183
- mcp_base: 'https://mcp.lanonasis.com/api/v1', // MCP HTTP/REST
184
- mcp_ws_base: 'wss://mcp.lanonasis.com/ws', // MCP WebSocket
185
- mcp_sse_base: 'https://mcp.lanonasis.com/api/v1/events', // MCP SSE
186
- project_scope: 'lanonasis-maas' // Correct project scope
187
- };
188
- await this.save();
189
259
  }
260
+ if (error.response?.status >= 500) {
261
+ return 'server_error';
262
+ }
263
+ if (error.response?.status === 404) {
264
+ return 'invalid_response';
265
+ }
266
+ const message = error.message?.toLowerCase() || '';
267
+ if (message.includes('timeout')) {
268
+ return 'timeout';
269
+ }
270
+ if (message.includes('network') || message.includes('connection')) {
271
+ return 'network_error';
272
+ }
273
+ return 'unknown';
274
+ }
275
+ // Manual endpoint override functionality
276
+ async setManualEndpoints(endpoints) {
277
+ if (!this.config.discoveredServices) {
278
+ // Initialize with defaults first
279
+ await this.discoverServices();
280
+ }
281
+ // Merge manual overrides with existing endpoints
282
+ this.config.discoveredServices = {
283
+ ...this.config.discoveredServices,
284
+ ...endpoints
285
+ };
286
+ // Mark as manually configured
287
+ this.config.manualEndpointOverrides = true;
288
+ this.config.lastManualEndpointUpdate = new Date().toISOString();
289
+ await this.save();
290
+ }
291
+ hasManualEndpointOverrides() {
292
+ return !!this.config.manualEndpointOverrides;
293
+ }
294
+ async clearManualEndpointOverrides() {
295
+ this.config.manualEndpointOverrides = undefined;
296
+ this.config.lastManualEndpointUpdate = undefined;
297
+ // Rediscover services
298
+ await this.discoverServices();
190
299
  }
191
300
  getDiscoveredApiUrl() {
192
301
  return this.config.discoveredServices?.auth_base || this.getApiUrl();
193
302
  }
194
303
  // Enhanced authentication support
195
304
  async setVendorKey(vendorKey) {
196
- // Validate vendor key format (pk_*.sk_*)
197
- if (!vendorKey.match(/^pk_[a-zA-Z0-9]+\.sk_[a-zA-Z0-9]+$/)) {
198
- throw new Error('Invalid vendor key format. Expected: pk_xxx.sk_xxx');
305
+ // Enhanced format validation with detailed error messages
306
+ const formatValidation = this.validateVendorKeyFormat(vendorKey);
307
+ if (formatValidation !== true) {
308
+ throw new Error(typeof formatValidation === 'string' ? formatValidation : 'Invalid vendor key format');
199
309
  }
200
310
  // Server-side validation
201
311
  await this.validateVendorKeyWithServer(vendorKey);
@@ -205,6 +315,44 @@ export class CLIConfig {
205
315
  await this.resetFailureCount(); // Reset failure count on successful auth
206
316
  await this.save();
207
317
  }
318
+ validateVendorKeyFormat(vendorKey) {
319
+ if (!vendorKey || vendorKey.trim().length === 0) {
320
+ return 'Vendor key is required';
321
+ }
322
+ const trimmed = vendorKey.trim();
323
+ // Check basic format
324
+ if (!trimmed.includes('.')) {
325
+ return 'Invalid vendor key format: Must contain a dot (.) separator. Expected format: pk_xxx.sk_xxx';
326
+ }
327
+ const parts = trimmed.split('.');
328
+ if (parts.length !== 2) {
329
+ return 'Invalid vendor key format: Must have exactly two parts separated by a dot. Expected format: pk_xxx.sk_xxx';
330
+ }
331
+ const [publicPart, secretPart] = parts;
332
+ // Validate public key part
333
+ if (!publicPart.startsWith('pk_')) {
334
+ return 'Invalid vendor key format: First part must start with "pk_". Expected format: pk_xxx.sk_xxx';
335
+ }
336
+ if (publicPart.length < 11) { // pk_ + minimum 8 chars
337
+ return 'Invalid vendor key format: Public key part is too short. Expected format: pk_xxx.sk_xxx (minimum 8 characters after "pk_")';
338
+ }
339
+ const publicKeyContent = publicPart.substring(3); // Remove 'pk_'
340
+ if (!/^[a-zA-Z0-9]+$/.test(publicKeyContent)) {
341
+ return 'Invalid vendor key format: Public key part contains invalid characters. Only letters and numbers are allowed after "pk_"';
342
+ }
343
+ // Validate secret key part
344
+ if (!secretPart.startsWith('sk_')) {
345
+ return 'Invalid vendor key format: Second part must start with "sk_". Expected format: pk_xxx.sk_xxx';
346
+ }
347
+ if (secretPart.length < 19) { // sk_ + minimum 16 chars
348
+ return 'Invalid vendor key format: Secret key part is too short. Expected format: pk_xxx.sk_xxx (minimum 16 characters after "sk_")';
349
+ }
350
+ const secretKeyContent = secretPart.substring(3); // Remove 'sk_'
351
+ if (!/^[a-zA-Z0-9]+$/.test(secretKeyContent)) {
352
+ return 'Invalid vendor key format: Secret key part contains invalid characters. Only letters and numbers are allowed after "sk_"';
353
+ }
354
+ return true;
355
+ }
208
356
  async validateVendorKeyWithServer(vendorKey) {
209
357
  try {
210
358
  // Import axios dynamically to avoid circular dependency
@@ -223,14 +371,45 @@ export class CLIConfig {
223
371
  });
224
372
  }
225
373
  catch (error) {
226
- if (error.response?.status === 401 || error.response?.status === 403) {
227
- throw new Error('Invalid vendor key: Authentication failed with server');
374
+ // Provide specific error messages based on response
375
+ if (error.response?.status === 401) {
376
+ const errorData = error.response.data;
377
+ if (errorData?.error?.includes('expired') || errorData?.message?.includes('expired')) {
378
+ throw new Error('Vendor key has expired. Please generate a new key from your dashboard.');
379
+ }
380
+ else if (errorData?.error?.includes('revoked') || errorData?.message?.includes('revoked')) {
381
+ throw new Error('Vendor key has been revoked. Please generate a new key from your dashboard.');
382
+ }
383
+ else if (errorData?.error?.includes('invalid') || errorData?.message?.includes('invalid')) {
384
+ throw new Error('Vendor key is invalid. Please check the key format and ensure it was copied correctly.');
385
+ }
386
+ else {
387
+ throw new Error('Vendor key authentication failed. The key may be invalid, expired, or revoked.');
388
+ }
389
+ }
390
+ else if (error.response?.status === 403) {
391
+ throw new Error('Vendor key access denied. The key may not have sufficient permissions for this operation.');
228
392
  }
229
- else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
230
- throw new Error('Cannot validate vendor key: Server unreachable');
393
+ else if (error.response?.status === 429) {
394
+ throw new Error('Too many validation attempts. Please wait a moment before trying again.');
395
+ }
396
+ else if (error.response?.status >= 500) {
397
+ throw new Error('Server error during validation. Please try again in a few moments.');
398
+ }
399
+ else if (error.code === 'ECONNREFUSED') {
400
+ throw new Error('Cannot connect to authentication server. Please check your internet connection and try again.');
401
+ }
402
+ else if (error.code === 'ENOTFOUND') {
403
+ throw new Error('Authentication server not found. Please check your internet connection.');
404
+ }
405
+ else if (error.code === 'ETIMEDOUT') {
406
+ throw new Error('Validation request timed out. Please check your internet connection and try again.');
407
+ }
408
+ else if (error.code === 'ECONNRESET') {
409
+ throw new Error('Connection was reset during validation. Please try again.');
231
410
  }
232
411
  else {
233
- throw new Error(`Vendor key validation failed: ${error.message}`);
412
+ throw new Error(`Vendor key validation failed: ${error.message || 'Unknown error'}`);
234
413
  }
235
414
  }
236
415
  }
@@ -280,6 +459,12 @@ export class CLIConfig {
280
459
  const token = this.getToken();
281
460
  if (!token)
282
461
  return false;
462
+ // Check cache first
463
+ if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
464
+ return this.authCheckCache.isValid;
465
+ }
466
+ // Local expiry check first (fast)
467
+ let locallyValid = false;
283
468
  // Handle simple CLI tokens (format: cli_xxx_timestamp)
284
469
  if (token.startsWith('cli_')) {
285
470
  // Extract timestamp from CLI token
@@ -290,21 +475,44 @@ export class CLIConfig {
290
475
  if (!isNaN(timestamp)) {
291
476
  // CLI tokens are valid for 30 days
292
477
  const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000;
293
- return (Date.now() - timestamp) < thirtyDaysInMs;
478
+ locallyValid = (Date.now() - timestamp) < thirtyDaysInMs;
294
479
  }
295
480
  }
296
- // If we can't parse timestamp, assume valid (fallback)
297
- return true;
481
+ else {
482
+ locallyValid = true; // Fallback for old format
483
+ }
298
484
  }
299
- // Handle JWT tokens
300
- try {
301
- const decoded = jwtDecode(token);
302
- const now = Date.now() / 1000;
303
- return typeof decoded.exp === 'number' && decoded.exp > now;
485
+ else {
486
+ // Handle JWT tokens
487
+ try {
488
+ const decoded = jwtDecode(token);
489
+ const now = Date.now() / 1000;
490
+ locallyValid = typeof decoded.exp === 'number' && decoded.exp > now;
491
+ }
492
+ catch {
493
+ locallyValid = false;
494
+ }
304
495
  }
305
- catch {
496
+ // If expired locally, no need to check server
497
+ if (!locallyValid) {
498
+ this.authCheckCache = { isValid: false, timestamp: Date.now() };
306
499
  return false;
307
500
  }
501
+ // Verify with server (security check)
502
+ try {
503
+ const axios = (await import('axios')).default;
504
+ const response = await axios.post('https://api.lanonasis.com/auth/verify', { token }, { timeout: 5000 });
505
+ const isValid = response.data.valid === true;
506
+ this.authCheckCache = { isValid, timestamp: Date.now() };
507
+ return isValid;
508
+ }
509
+ catch (error) {
510
+ // If server check fails, fall back to local validation
511
+ // This allows offline usage but is less secure
512
+ console.warn('⚠️ Unable to verify token with server, using local validation');
513
+ this.authCheckCache = { isValid: locallyValid, timestamp: Date.now() };
514
+ return locallyValid;
515
+ }
308
516
  }
309
517
  async logout() {
310
518
  this.config.token = undefined;
@@ -447,6 +655,14 @@ export class CLIConfig {
447
655
  const delay = Math.min(baseDelay * Math.pow(2, failureCount - 3), maxDelay);
448
656
  return delay;
449
657
  }
658
+ async getDeviceId() {
659
+ if (!this.config.deviceId) {
660
+ // Generate a new device ID
661
+ this.config.deviceId = randomUUID();
662
+ await this.save();
663
+ }
664
+ return this.config.deviceId;
665
+ }
450
666
  // Generic get/set methods for MCP and other dynamic config
451
667
  get(key) {
452
668
  return this.config[key];
@@ -49,15 +49,64 @@ export declare class MCPClient {
49
49
  private isConnected;
50
50
  private sseConnection;
51
51
  private wsConnection;
52
+ private retryAttempts;
53
+ private maxRetries;
54
+ private healthCheckInterval;
55
+ private connectionStartTime;
56
+ private lastHealthCheck;
52
57
  constructor();
53
58
  /**
54
59
  * Initialize the MCP client configuration
55
60
  */
56
61
  init(): Promise<void>;
57
62
  /**
58
- * Connect to MCP server (local or remote)
63
+ * Connect to MCP server with retry logic
59
64
  */
60
65
  connect(options?: MCPConnectionOptions): Promise<boolean>;
66
+ /**
67
+ * Connect to MCP server with retry logic and exponential backoff
68
+ */
69
+ private connectWithRetry;
70
+ /**
71
+ * Handle connection failures with retry logic and specific error messages
72
+ */
73
+ private handleConnectionFailure;
74
+ /**
75
+ * Check if error is authentication-related
76
+ */
77
+ private isAuthenticationError;
78
+ /**
79
+ * Provide authentication-specific guidance
80
+ */
81
+ private provideAuthenticationGuidance;
82
+ /**
83
+ * Provide network troubleshooting guidance
84
+ */
85
+ private provideNetworkTroubleshootingGuidance;
86
+ /**
87
+ * Calculate exponential backoff delay with jitter
88
+ */
89
+ private exponentialBackoff;
90
+ /**
91
+ * Validate authentication credentials before attempting MCP connection
92
+ */
93
+ private validateAuthBeforeConnect;
94
+ /**
95
+ * Validate vendor key format
96
+ */
97
+ private validateVendorKeyFormat;
98
+ /**
99
+ * Validate and refresh token if needed
100
+ */
101
+ private validateAndRefreshToken;
102
+ /**
103
+ * Refresh token if needed
104
+ */
105
+ private refreshTokenIfNeeded;
106
+ /**
107
+ * Validate token with server
108
+ */
109
+ private validateTokenWithServer;
61
110
  /**
62
111
  * Initialize SSE connection for real-time updates
63
112
  */
@@ -70,6 +119,34 @@ export declare class MCPClient {
70
119
  * Send a message over the WebSocket connection
71
120
  */
72
121
  private sendWebSocketMessage;
122
+ /**
123
+ * Start health monitoring for the connection
124
+ */
125
+ private startHealthMonitoring;
126
+ /**
127
+ * Stop health monitoring
128
+ */
129
+ private stopHealthMonitoring;
130
+ /**
131
+ * Perform a health check on the current connection
132
+ */
133
+ private performHealthCheck;
134
+ /**
135
+ * Check WebSocket connection health
136
+ */
137
+ private checkWebSocketHealth;
138
+ /**
139
+ * Check remote connection health
140
+ */
141
+ private checkRemoteHealth;
142
+ /**
143
+ * Check local connection health
144
+ */
145
+ private checkLocalHealth;
146
+ /**
147
+ * Handle health check failure by attempting reconnection
148
+ */
149
+ private handleHealthCheckFailure;
73
150
  /**
74
151
  * Disconnect from MCP server
75
152
  */
@@ -94,12 +171,16 @@ export declare class MCPClient {
94
171
  */
95
172
  isConnectedToServer(): boolean;
96
173
  /**
97
- * Get connection status details
174
+ * Get connection status details with health information
98
175
  */
99
176
  getConnectionStatus(): {
100
177
  connected: boolean;
101
178
  mode: string;
102
179
  server?: string;
180
+ latency?: number;
181
+ lastHealthCheck?: Date;
182
+ connectionUptime?: number;
183
+ failureCount: number;
103
184
  };
104
185
  }
105
186
  export declare function getMCPClient(): MCPClient;