@lanonasis/cli 3.8.0 → 3.9.0

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/README.md +65 -2
  3. package/dist/commands/auth.js +1 -1
  4. package/dist/commands/config.js +3 -2
  5. package/dist/commands/init.js +12 -0
  6. package/dist/commands/mcp.js +50 -3
  7. package/dist/commands/memory.js +49 -23
  8. package/dist/index.js +20 -0
  9. package/dist/mcp/access-control.js +2 -2
  10. package/dist/mcp/schemas/tool-schemas.d.ts +4 -4
  11. package/dist/mcp/server/lanonasis-server.js +26 -3
  12. package/dist/utils/api.js +10 -10
  13. package/dist/utils/config.js +40 -6
  14. package/dist/utils/mcp-client.d.ts +2 -0
  15. package/dist/utils/mcp-client.js +33 -15
  16. package/dist/ux/implementations/ConnectionManagerImpl.d.ts +72 -0
  17. package/dist/ux/implementations/ConnectionManagerImpl.js +352 -0
  18. package/dist/ux/implementations/OnboardingFlowImpl.d.ts +72 -0
  19. package/dist/ux/implementations/OnboardingFlowImpl.js +415 -0
  20. package/dist/ux/implementations/TextInputHandlerImpl.d.ts +74 -0
  21. package/dist/ux/implementations/TextInputHandlerImpl.js +342 -0
  22. package/dist/ux/implementations/index.d.ts +11 -0
  23. package/dist/ux/implementations/index.js +11 -0
  24. package/dist/ux/index.d.ts +15 -0
  25. package/dist/ux/index.js +22 -0
  26. package/dist/ux/interfaces/ConnectionManager.d.ts +112 -0
  27. package/dist/ux/interfaces/ConnectionManager.js +7 -0
  28. package/dist/ux/interfaces/OnboardingFlow.d.ts +103 -0
  29. package/dist/ux/interfaces/OnboardingFlow.js +7 -0
  30. package/dist/ux/interfaces/TextInputHandler.d.ts +87 -0
  31. package/dist/ux/interfaces/TextInputHandler.js +7 -0
  32. package/dist/ux/interfaces/index.d.ts +10 -0
  33. package/dist/ux/interfaces/index.js +8 -0
  34. package/package.json +34 -4
@@ -201,14 +201,14 @@ export declare const SystemConfigSchema: z.ZodObject<{
201
201
  scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
202
202
  }, "strip", z.ZodTypeAny, {
203
203
  value?: any;
204
+ key?: string;
204
205
  action?: "get" | "set" | "reset";
205
206
  scope?: "user" | "global";
206
- key?: string;
207
207
  }, {
208
208
  value?: any;
209
+ key?: string;
209
210
  action?: "get" | "set" | "reset";
210
211
  scope?: "user" | "global";
211
- key?: string;
212
212
  }>;
213
213
  export declare const BulkOperationSchema: z.ZodObject<{
214
214
  operation: z.ZodEnum<["create", "update", "delete"]>;
@@ -579,14 +579,14 @@ export declare const MCPSchemas: {
579
579
  scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
580
580
  }, "strip", z.ZodTypeAny, {
581
581
  value?: any;
582
+ key?: string;
582
583
  action?: "get" | "set" | "reset";
583
584
  scope?: "user" | "global";
584
- key?: string;
585
585
  }, {
586
586
  value?: any;
587
+ key?: string;
587
588
  action?: "get" | "set" | "reset";
588
589
  scope?: "user" | "global";
589
- key?: string;
590
590
  }>;
591
591
  };
592
592
  operations: {
@@ -407,7 +407,15 @@ export class LanonasisMCPServer {
407
407
  text: `Authentication Error: ${error instanceof Error ? error.message : 'Authentication failed'}`
408
408
  }
409
409
  ],
410
- isError: true
410
+ isError: true,
411
+ task: {
412
+ taskId: `${clientId}-${Date.now()}`,
413
+ status: 'failed',
414
+ ttl: null,
415
+ createdAt: new Date().toISOString(),
416
+ lastUpdatedAt: new Date().toISOString(),
417
+ statusMessage: 'Authentication failed'
418
+ }
411
419
  };
412
420
  }
413
421
  this.updateConnectionActivity(clientId);
@@ -419,7 +427,14 @@ export class LanonasisMCPServer {
419
427
  type: 'text',
420
428
  text: JSON.stringify(result, null, 2)
421
429
  }
422
- ]
430
+ ],
431
+ task: {
432
+ taskId: `${clientId}-${Date.now()}`,
433
+ status: 'completed',
434
+ ttl: null,
435
+ createdAt: new Date().toISOString(),
436
+ lastUpdatedAt: new Date().toISOString()
437
+ }
423
438
  };
424
439
  }
425
440
  catch (error) {
@@ -430,7 +445,15 @@ export class LanonasisMCPServer {
430
445
  text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
431
446
  }
432
447
  ],
433
- isError: true
448
+ isError: true,
449
+ task: {
450
+ taskId: `${clientId}-${Date.now()}`,
451
+ status: 'failed',
452
+ ttl: null,
453
+ createdAt: new Date().toISOString(),
454
+ lastUpdatedAt: new Date().toISOString(),
455
+ statusMessage: error instanceof Error ? error.message : 'Unknown error'
456
+ }
434
457
  };
435
458
  }
436
459
  });
package/dist/utils/api.js CHANGED
@@ -99,40 +99,40 @@ export class APIClient {
99
99
  });
100
100
  return response.data;
101
101
  }
102
- // Memory operations - aligned with existing schema
103
- // All memory endpoints use /api/v1/memory path
102
+ // Memory operations - aligned with REST API canonical endpoints
103
+ // All memory endpoints use /api/v1/memories path (plural, per REST conventions)
104
104
  async createMemory(data) {
105
- const response = await this.client.post('/api/v1/memory', data);
105
+ const response = await this.client.post('/api/v1/memories', data);
106
106
  return response.data;
107
107
  }
108
108
  async getMemories(params = {}) {
109
- const response = await this.client.get('/api/v1/memory', { params });
109
+ const response = await this.client.get('/api/v1/memories', { params });
110
110
  return response.data;
111
111
  }
112
112
  async getMemory(id) {
113
- const response = await this.client.get(`/api/v1/memory/${id}`);
113
+ const response = await this.client.get(`/api/v1/memories/${id}`);
114
114
  return response.data;
115
115
  }
116
116
  async updateMemory(id, data) {
117
- const response = await this.client.put(`/api/v1/memory/${id}`, data);
117
+ const response = await this.client.put(`/api/v1/memories/${id}`, data);
118
118
  return response.data;
119
119
  }
120
120
  async deleteMemory(id) {
121
- await this.client.delete(`/api/v1/memory/${id}`);
121
+ await this.client.delete(`/api/v1/memories/${id}`);
122
122
  }
123
123
  async searchMemories(query, options = {}) {
124
- const response = await this.client.post('/api/v1/memory/search', {
124
+ const response = await this.client.post('/api/v1/memories/search', {
125
125
  query,
126
126
  ...options
127
127
  });
128
128
  return response.data;
129
129
  }
130
130
  async getMemoryStats() {
131
- const response = await this.client.get('/api/v1/memory/stats');
131
+ const response = await this.client.get('/api/v1/memories/stats');
132
132
  return response.data;
133
133
  }
134
134
  async bulkDeleteMemories(memoryIds) {
135
- const response = await this.client.post('/api/v1/memory/bulk/delete', {
135
+ const response = await this.client.post('/api/v1/memories/bulk/delete', {
136
136
  memory_ids: memoryIds
137
137
  });
138
138
  return response.data;
@@ -499,7 +499,11 @@ export class CLIConfig {
499
499
  }
500
500
  // Store a reference marker in config (not the actual key)
501
501
  this.config.vendorKey = 'stored_in_api_key_storage';
502
- this.config.authMethod = 'vendor_key';
502
+ // Only set authMethod to 'vendor_key' if not already set to OAuth
503
+ // This prevents overwriting OAuth auth method when storing the token for MCP access
504
+ if (!this.config.authMethod || !['oauth', 'oauth2', 'jwt'].includes(this.config.authMethod)) {
505
+ this.config.authMethod = 'vendor_key';
506
+ }
503
507
  this.config.lastValidated = new Date().toISOString();
504
508
  await this.resetFailureCount(); // Reset failure count on successful auth
505
509
  await this.save();
@@ -692,11 +696,41 @@ export class CLIConfig {
692
696
  this.authCheckCache = { isValid: true, timestamp: Date.now() };
693
697
  return true;
694
698
  }
695
- // For vendor keys, we trust that they were validated during setVendorKey()
696
- // and rely on the lastValidated timestamp. For additional security,
697
- // the server should revoke keys that are invalid.
698
- this.authCheckCache = { isValid: true, timestamp: Date.now() };
699
- return true;
699
+ // Vendor key not recently validated - verify with server
700
+ try {
701
+ await this.discoverServices();
702
+ const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
703
+ // Ping auth health with vendor key to verify it's still valid
704
+ await this.pingAuthHealth(axios, authBase, {
705
+ 'X-API-Key': vendorKey,
706
+ 'X-Auth-Method': 'vendor_key',
707
+ 'X-Project-Scope': 'lanonasis-maas'
708
+ }, { timeout: 5000, proxy: false });
709
+ // Update last validated timestamp on success
710
+ this.config.lastValidated = new Date().toISOString();
711
+ await this.save().catch(() => { }); // Don't fail auth check if save fails
712
+ this.authCheckCache = { isValid: true, timestamp: Date.now() };
713
+ return true;
714
+ }
715
+ catch (error) {
716
+ // Server validation failed - check for grace period (7 days offline)
717
+ const gracePeriod = 7 * 24 * 60 * 60 * 1000;
718
+ const withinGracePeriod = lastValidated &&
719
+ (Date.now() - new Date(lastValidated).getTime()) < gracePeriod;
720
+ if (withinGracePeriod) {
721
+ if (process.env.CLI_VERBOSE === 'true') {
722
+ console.warn('⚠️ Unable to validate vendor key with server, using cached validation');
723
+ }
724
+ this.authCheckCache = { isValid: true, timestamp: Date.now() };
725
+ return true;
726
+ }
727
+ // Grace period expired - require server validation
728
+ if (process.env.CLI_VERBOSE === 'true') {
729
+ console.warn('⚠️ Vendor key validation failed and grace period expired');
730
+ }
731
+ this.authCheckCache = { isValid: false, timestamp: Date.now() };
732
+ return false;
733
+ }
700
734
  }
701
735
  // Handle token-based authentication
702
736
  const token = this.getToken();
@@ -53,6 +53,8 @@ export declare class MCPClient {
53
53
  private retryAttempts;
54
54
  private maxRetries;
55
55
  private healthCheckInterval;
56
+ private healthCheckTimeout;
57
+ private wsReconnectTimeout;
56
58
  private connectionStartTime;
57
59
  private lastHealthCheck;
58
60
  private activeConnectionMode;
@@ -14,6 +14,8 @@ export class MCPClient {
14
14
  retryAttempts = 0;
15
15
  maxRetries = 3;
16
16
  healthCheckInterval = null;
17
+ healthCheckTimeout = null;
18
+ wsReconnectTimeout = null;
17
19
  connectionStartTime = 0;
18
20
  lastHealthCheck = null;
19
21
  activeConnectionMode = 'local'; // Track actual connection mode
@@ -152,6 +154,8 @@ export class MCPClient {
152
154
  else {
153
155
  console.log(chalk.yellow(`Retry ${this.retryAttempts}/${this.maxRetries}: Connecting to remote MCP server...`));
154
156
  }
157
+ // Verify remote health before establishing SSE
158
+ await this.checkRemoteHealth(serverUrl);
155
159
  // Initialize SSE connection for real-time updates
156
160
  await this.initializeSSE(serverUrl);
157
161
  this.isConnected = true;
@@ -174,7 +178,7 @@ export class MCPClient {
174
178
  // Check if the server file exists
175
179
  if (!fs.existsSync(serverPath)) {
176
180
  console.log(chalk.yellow(`⚠️ Local MCP server not found at ${serverPath}`));
177
- console.log(chalk.cyan('💡 For remote use WebSocket: lanonasis mcp connect --mode websocket --url wss://mcp.lanonasis.com/ws'));
181
+ console.log(chalk.cyan('💡 For remote connection, use: lanonasis mcp connect --mode websocket --url wss://mcp.lanonasis.com/ws'));
178
182
  throw new Error(`MCP server not found at ${serverPath}`);
179
183
  }
180
184
  if (this.retryAttempts === 0) {
@@ -248,8 +252,8 @@ export class MCPClient {
248
252
  return false;
249
253
  }
250
254
  this.retryAttempts++;
251
- if (this.retryAttempts >= this.maxRetries) {
252
- console.error(chalk.red(`Failed to connect after ${this.maxRetries} attempts`));
255
+ if (this.retryAttempts > this.maxRetries) {
256
+ console.error(chalk.red(`Failed to connect after ${this.maxRetries + 1} attempts`));
253
257
  this.provideNetworkTroubleshootingGuidance(error);
254
258
  this.isConnected = false;
255
259
  return false;
@@ -356,7 +360,7 @@ export class MCPClient {
356
360
  */
357
361
  async validateAuthBeforeConnect() {
358
362
  const token = this.config.get('token');
359
- const vendorKey = this.config.get('vendorKey');
363
+ const vendorKey = await this.config.getVendorKeyAsync();
360
364
  // Check if we have any authentication credentials
361
365
  if (!token && !vendorKey) {
362
366
  throw new Error('AUTHENTICATION_REQUIRED: No authentication credentials found. Run "lanonasis auth login" first.');
@@ -456,7 +460,7 @@ export class MCPClient {
456
460
  // Use the proper SSE endpoint from config
457
461
  const sseUrl = this.config.getMCPSSEUrl() ?? `${serverUrl}/events`;
458
462
  const token = this.config.get('token');
459
- const vendorKey = this.config.get('vendorKey');
463
+ const vendorKey = await this.config.getVendorKeyAsync();
460
464
  const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
461
465
  if (authKey) {
462
466
  // EventSource doesn't support headers directly, append token to URL
@@ -480,7 +484,7 @@ export class MCPClient {
480
484
  */
481
485
  async initializeWebSocket(wsUrl) {
482
486
  const token = this.config.get('token');
483
- const vendorKey = this.config.get('vendorKey');
487
+ const vendorKey = await this.config.getVendorKeyAsync();
484
488
  const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
485
489
  if (!authKey) {
486
490
  throw new Error('API key required for WebSocket mode. Set LANONASIS_API_KEY or login first.');
@@ -534,8 +538,11 @@ export class MCPClient {
534
538
  this.wsConnection.on('close', (code, reason) => {
535
539
  console.log(chalk.yellow(`WebSocket connection closed (${code}): ${reason}`));
536
540
  // Auto-reconnect after delay
537
- setTimeout(() => {
538
- if (this.isConnected) {
541
+ if (this.wsReconnectTimeout) {
542
+ clearTimeout(this.wsReconnectTimeout);
543
+ }
544
+ this.wsReconnectTimeout = setTimeout(() => {
545
+ if (this.isConnected && process.env.NODE_ENV !== 'test') {
539
546
  console.log(chalk.blue('🔄 Attempting to reconnect to WebSocket...'));
540
547
  this.initializeWebSocket(wsUrl).catch(err => {
541
548
  console.error('Failed to reconnect:', err);
@@ -569,7 +576,8 @@ export class MCPClient {
569
576
  await this.performHealthCheck();
570
577
  }, 30000);
571
578
  // Perform initial health check
572
- setTimeout(() => this.performHealthCheck(), 5000);
579
+ const initialDelay = process.env.NODE_ENV === 'test' ? 50 : 5000;
580
+ this.healthCheckTimeout = setTimeout(() => this.performHealthCheck(), initialDelay);
573
581
  }
574
582
  /**
575
583
  * Stop health monitoring
@@ -579,6 +587,10 @@ export class MCPClient {
579
587
  clearInterval(this.healthCheckInterval);
580
588
  this.healthCheckInterval = null;
581
589
  }
590
+ if (this.healthCheckTimeout) {
591
+ clearTimeout(this.healthCheckTimeout);
592
+ this.healthCheckTimeout = null;
593
+ }
582
594
  }
583
595
  /**
584
596
  * Perform a health check on the current connection
@@ -625,10 +637,10 @@ export class MCPClient {
625
637
  /**
626
638
  * Check remote connection health
627
639
  */
628
- async checkRemoteHealth() {
629
- const apiUrl = this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
640
+ async checkRemoteHealth(serverUrl) {
641
+ const apiUrl = serverUrl ?? this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
630
642
  const token = this.config.get('token');
631
- const vendorKey = this.config.get('vendorKey');
643
+ const vendorKey = await this.config.getVendorKeyAsync();
632
644
  const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
633
645
  if (!authKey) {
634
646
  throw new Error('No authentication token available');
@@ -700,11 +712,14 @@ export class MCPClient {
700
712
  */
701
713
  async disconnect() {
702
714
  this.stopHealthMonitoring();
715
+ this.isConnected = false;
703
716
  if (this.client) {
704
717
  await this.client.close();
705
718
  this.client = null;
706
719
  }
707
720
  if (this.sseConnection) {
721
+ this.sseConnection.onmessage = null;
722
+ this.sseConnection.onerror = null;
708
723
  this.sseConnection.close();
709
724
  this.sseConnection = null;
710
725
  }
@@ -712,7 +727,10 @@ export class MCPClient {
712
727
  this.wsConnection.close();
713
728
  this.wsConnection = null;
714
729
  }
715
- this.isConnected = false;
730
+ if (this.wsReconnectTimeout) {
731
+ clearTimeout(this.wsReconnectTimeout);
732
+ this.wsReconnectTimeout = null;
733
+ }
716
734
  this.activeConnectionMode = 'websocket'; // Reset to default
717
735
  }
718
736
  /**
@@ -755,7 +773,7 @@ export class MCPClient {
755
773
  async callRemoteTool(toolName, args) {
756
774
  const apiUrl = this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
757
775
  const token = this.config.get('token');
758
- const vendorKey = this.config.get('vendorKey');
776
+ const vendorKey = await this.config.getVendorKeyAsync();
759
777
  const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
760
778
  if (!authKey) {
761
779
  throw new Error('Authentication required. Run "lanonasis auth login" first.');
@@ -897,7 +915,7 @@ export class MCPClient {
897
915
  break;
898
916
  }
899
917
  const connectionUptime = this.connectionStartTime > 0
900
- ? Date.now() - this.connectionStartTime
918
+ ? Math.max(Date.now() - this.connectionStartTime, this.isConnected ? 1 : 0)
901
919
  : undefined;
902
920
  return {
903
921
  connected: this.isConnected,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Connection Manager Implementation
3
+ *
4
+ * Manages MCP server discovery, configuration, and connection lifecycle
5
+ * Implementation of the ConnectionManager interface.
6
+ */
7
+ import { ConnectionManager, ConnectionResult, ConfigResult, ServerInstance, ConnectionStatus, MCPConfig } from '../interfaces/ConnectionManager.js';
8
+ /**
9
+ * ConnectionManagerImpl manages MCP server discovery, configuration, and connection lifecycle
10
+ *
11
+ * This implementation automatically detects the embedded MCP server location within the CLI package,
12
+ * generates configuration files with correct server paths, and manages server processes.
13
+ */
14
+ export declare class ConnectionManagerImpl implements ConnectionManager {
15
+ private config;
16
+ private connectionStatus;
17
+ private serverProcess;
18
+ private configPath;
19
+ constructor(configPath?: string);
20
+ /**
21
+ * Initialize the connection manager by loading persisted configuration
22
+ */
23
+ init(): Promise<void>;
24
+ /**
25
+ * Connect to the local embedded MCP server
26
+ */
27
+ connectLocal(): Promise<ConnectionResult>;
28
+ /**
29
+ * Automatically configure the local MCP server with correct paths
30
+ */
31
+ autoConfigureLocalServer(): Promise<ConfigResult>;
32
+ /**
33
+ * Detect the embedded MCP server path within the CLI package
34
+ */
35
+ detectServerPath(): Promise<string | null>;
36
+ /**
37
+ * Start the local MCP server process
38
+ */
39
+ startLocalServer(): Promise<ServerInstance>;
40
+ /**
41
+ * Verify that the MCP server connection is working
42
+ */
43
+ verifyConnection(serverPath: string): Promise<boolean>;
44
+ /**
45
+ * Get the current connection status
46
+ */
47
+ getConnectionStatus(): ConnectionStatus;
48
+ /**
49
+ * Stop the local MCP server if running
50
+ */
51
+ stopLocalServer(): Promise<void>;
52
+ /**
53
+ * Get the current MCP configuration
54
+ */
55
+ getConfig(): MCPConfig;
56
+ /**
57
+ * Update the MCP configuration
58
+ */
59
+ updateConfig(config: Partial<MCPConfig>): Promise<void>;
60
+ /**
61
+ * Check if the server is currently running
62
+ */
63
+ private isServerRunning;
64
+ /**
65
+ * Save the current configuration to disk
66
+ */
67
+ private saveConfig;
68
+ /**
69
+ * Load configuration from disk
70
+ */
71
+ private loadConfig;
72
+ }