@lanonasis/cli 3.9.0 → 3.9.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog - @lanonasis/cli
2
2
 
3
+ ## [3.9.2] - 2026-02-02
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - **Auth Method Override**: Fixed vendor key authentication not overriding previous OAuth `authMethod`
8
+ - When users explicitly authenticate with vendor key after OAuth, the `authMethod` is now correctly set to `vendor_key`
9
+ - This fixes "Authenticated: No" status after successful vendor key authentication
10
+ - Reverted changes from 3.9.1 that incorrectly removed vendor key storage from OAuth flow
11
+
12
+ ## [3.9.1] - 2026-02-01
13
+
14
+ ### 🔐 Authentication Fixes
15
+
16
+ - **OAuth Scope Clarification**: OAuth login now clearly states it enables MCP integration only
17
+ - **Improved Error Messages**: 401 errors for OAuth users include specific guidance for direct API access
18
+ - **Removed Misleading Storage**: OAuth tokens are no longer incorrectly stored as vendor keys
19
+ - **Documentation Updates**: README and in-CLI help clarify authentication method differences
20
+
21
+ ### 🐛 Bug Fixes
22
+
23
+ - Fixed confusing error message when OAuth users try to use direct CLI commands
24
+ - Removed `.lanonasis/mcp-config.json` from version control
25
+
3
26
  ## [3.9.0] - 2026-02-01
4
27
 
5
28
  ### 🎨 CLI UX Revolution
@@ -26,6 +49,21 @@
26
49
  - **User Preferences**: Captures and persists input mode, editor choice, and behavior preferences
27
50
  - **Troubleshooting Guidance**: Context-aware help when issues are detected
28
51
 
52
+ ### 🔐 Authentication Clarifications
53
+
54
+ #### OAuth vs Direct API Access
55
+ - **Clear Scope Documentation**: OAuth2 login now explicitly states it enables MCP integration
56
+ - **Improved Error Messages**: 401 errors for OAuth users include specific guidance for direct API access
57
+ - **Authentication Method Guidance**: CLI provides clear instructions for:
58
+ - **OAuth**: Use for MCP integration and real-time features
59
+ - **Vendor Key**: Obtain from dashboard for direct API access (`lanonasis auth login --vendor`)
60
+ - **Credentials**: Use username/password for direct API access (`lanonasis auth login --credentials`)
61
+
62
+ #### Secure Storage Fallback
63
+ - **Keytar Optional**: When keytar (native secure storage) is unavailable, CLI gracefully falls back to encrypted file storage
64
+ - **Cross-Platform**: Encrypted storage works consistently across all platforms
65
+ - **No Data Loss**: Credentials are preserved in `~/.lanonasis/api-key.enc` with AES-256-GCM encryption
66
+
29
67
  ### 🐛 Critical Bug Fixes (PR #93)
30
68
 
31
69
  #### P1: Connection Verification False Positive
package/README.md CHANGED
@@ -156,12 +156,14 @@ onasis login --vendor-key <your-vendor-key>
156
156
 
157
157
  ### 2. OAuth Browser Authentication
158
158
 
159
- Secure browser-based authentication:
159
+ Secure browser-based authentication for MCP integration:
160
160
 
161
161
  ```bash
162
162
  onasis login --oauth
163
163
  ```
164
164
 
165
+ > **Note**: OAuth authentication enables MCP integration features (real-time updates, WebSocket connections). For direct CLI memory commands (`memory list`, `memory create`, etc.), use vendor key or credentials authentication.
166
+
165
167
  ### 3. Interactive Credentials
166
168
 
167
169
  Traditional username/password authentication:
@@ -606,6 +606,10 @@ async function handleVendorKeyAuth(vendorKey, config) {
606
606
  const spinner = ora('Validating vendor key...').start();
607
607
  try {
608
608
  await config.setVendorKey(vendorKey);
609
+ // Explicitly set authMethod to vendor_key when user does explicit vendor key auth
610
+ // This overrides any previous OAuth authMethod
611
+ await config.set('authMethod', 'vendor_key');
612
+ await config.save();
609
613
  // Test the vendor key with a health check
610
614
  await apiClient.get('/health');
611
615
  spinner.succeed('Vendor key authentication successful');
@@ -212,12 +212,38 @@ export function mcpCommands(program) {
212
212
  let healthLabel = chalk.gray('Unknown');
213
213
  let healthDetails;
214
214
  let isServiceReachable = false;
215
+ let resolvedHealthUrl;
215
216
  try {
216
217
  const axios = (await import('axios')).default;
217
- // Derive MCP health URL from discovered REST base (e.g. https://mcp.lanonasis.com/api/v1 -> https://mcp.lanonasis.com/health)
218
- const restUrl = config.getMCPRestUrl();
219
- const rootBase = restUrl.replace(/\/api\/v1$/, '');
220
- const healthUrl = `${rootBase}/health`;
218
+ const normalizeMcpHealthUrl = (inputUrl) => {
219
+ const parsed = new URL(inputUrl);
220
+ if (parsed.protocol === 'wss:') {
221
+ parsed.protocol = 'https:';
222
+ }
223
+ else if (parsed.protocol === 'ws:') {
224
+ parsed.protocol = 'http:';
225
+ }
226
+ parsed.pathname = '/health';
227
+ parsed.search = '';
228
+ parsed.hash = '';
229
+ return parsed.toString();
230
+ };
231
+ // Prefer MCP host health based on active mode:
232
+ // - websocket: use configured websocket host (wss -> https)
233
+ // - remote: use configured MCP REST host
234
+ // - local/default: fall back to discovered MCP REST host
235
+ let healthProbeBase;
236
+ if (status.mode === 'websocket') {
237
+ healthProbeBase = config.get('mcpWebSocketUrl') ?? config.getMCPServerUrl();
238
+ }
239
+ else if (status.mode === 'remote') {
240
+ healthProbeBase = config.get('mcpServerUrl') ?? config.getMCPRestUrl();
241
+ }
242
+ else {
243
+ healthProbeBase = config.getMCPRestUrl();
244
+ }
245
+ const healthUrl = normalizeMcpHealthUrl(healthProbeBase);
246
+ resolvedHealthUrl = healthUrl;
221
247
  const token = config.getToken();
222
248
  const vendorKey = await config.getVendorKeyAsync();
223
249
  const headers = {};
@@ -234,7 +260,8 @@ export function mcpCommands(program) {
234
260
  timeout: 5000
235
261
  });
236
262
  const overallStatus = String(response.data?.status ?? '').toLowerCase();
237
- const ok = response.status === 200 && (!overallStatus || overallStatus === 'healthy');
263
+ const okStatuses = new Set(['healthy', 'ok', 'up']);
264
+ const ok = response.status === 200 && (!overallStatus || okStatuses.has(overallStatus));
238
265
  if (ok) {
239
266
  healthLabel = chalk.green('Healthy');
240
267
  isServiceReachable = true;
@@ -285,6 +312,9 @@ export function mcpCommands(program) {
285
312
  if (healthDetails && process.env.CLI_VERBOSE === 'true') {
286
313
  console.log(chalk.gray(`Health details: ${healthDetails}`));
287
314
  }
315
+ if (resolvedHealthUrl && process.env.CLI_VERBOSE === 'true') {
316
+ console.log(chalk.gray(`Health probe URL: ${resolvedHealthUrl}`));
317
+ }
288
318
  // Show features when service is reachable
289
319
  if (isServiceReachable) {
290
320
  if (status.mode === 'remote') {
@@ -191,6 +191,11 @@ export declare class MCPClient {
191
191
  * Check if connected to MCP server
192
192
  */
193
193
  isConnectedToServer(): boolean;
194
+ /**
195
+ * Determine whether tool operations should use the remote REST bridge.
196
+ * WebSocket mode uses the same bridge for tool list/call operations.
197
+ */
198
+ private shouldUseRemoteToolBridge;
194
199
  /**
195
200
  * Get connection status details with health information
196
201
  */
@@ -68,6 +68,7 @@ export class MCPClient {
68
68
  // Save the successful connection mode as preference
69
69
  this.config.set('mcpConnectionMode', mode);
70
70
  this.config.set('mcpPreference', mode);
71
+ this.config.set('mcpUseRemote', mode === 'remote' || mode === 'websocket');
71
72
  // Save the specific URL that worked
72
73
  if (url) {
73
74
  if (mode === 'websocket') {
@@ -525,7 +526,12 @@ export class MCPClient {
525
526
  this.wsConnection.on('message', (data) => {
526
527
  try {
527
528
  const message = JSON.parse(data.toString());
528
- console.log(chalk.blue('📡 MCP message:'), message.id, message.method || 'response');
529
+ const messageId = message.id ?? 'event';
530
+ const messageType = message.method
531
+ || (message.error ? 'error' : undefined)
532
+ || (message.result ? 'result' : undefined)
533
+ || 'response';
534
+ console.log(chalk.blue('📡 MCP message:'), messageId, messageType);
529
535
  }
530
536
  catch (error) {
531
537
  console.error('Failed to parse WebSocket message:', error);
@@ -740,7 +746,7 @@ export class MCPClient {
740
746
  if (!this.isConnected) {
741
747
  throw new Error('Not connected to MCP server. Run "lanonasis mcp connect" first.');
742
748
  }
743
- const useRemote = this.config.get('mcpUseRemote') ?? false;
749
+ const useRemote = this.shouldUseRemoteToolBridge();
744
750
  if (useRemote) {
745
751
  // Remote MCP calls are translated to REST API calls
746
752
  return await this.callRemoteTool(toolName, args);
@@ -854,7 +860,7 @@ export class MCPClient {
854
860
  if (!this.isConnected) {
855
861
  throw new Error('Not connected to MCP server');
856
862
  }
857
- const useRemote = this.config.get('mcpUseRemote') ?? false;
863
+ const useRemote = this.shouldUseRemoteToolBridge();
858
864
  if (useRemote) {
859
865
  // Return hardcoded list for remote mode
860
866
  return [
@@ -883,6 +889,16 @@ export class MCPClient {
883
889
  isConnectedToServer() {
884
890
  return this.isConnected;
885
891
  }
892
+ /**
893
+ * Determine whether tool operations should use the remote REST bridge.
894
+ * WebSocket mode uses the same bridge for tool list/call operations.
895
+ */
896
+ shouldUseRemoteToolBridge() {
897
+ if (this.activeConnectionMode === 'remote' || this.activeConnectionMode === 'websocket') {
898
+ return true;
899
+ }
900
+ return this.config.get('mcpUseRemote') ?? false;
901
+ }
886
902
  /**
887
903
  * Get connection status details with health information
888
904
  */
@@ -202,6 +202,10 @@ export class ConnectionManagerImpl {
202
202
  LOG_LEVEL: this.config.logLevel,
203
203
  },
204
204
  });
205
+ if (serverProcess.pid === undefined) {
206
+ reject(new Error('Failed to start local MCP server: process ID was not assigned'));
207
+ return;
208
+ }
205
209
  const serverInstance = {
206
210
  pid: serverProcess.pid,
207
211
  port: this.config.serverPort || 3000,
@@ -291,7 +295,7 @@ export class ConnectionManagerImpl {
291
295
  this.serverProcess.kill('SIGKILL');
292
296
  }
293
297
  }, 5000);
294
- this.serverProcess.on('exit', () => {
298
+ this.serverProcess.once('exit', () => {
295
299
  clearTimeout(forceKillTimeout);
296
300
  this.serverProcess = null;
297
301
  if (this.connectionStatus.serverInstance) {
@@ -47,10 +47,17 @@ export class TextInputHandlerImpl {
47
47
  status: 'active',
48
48
  };
49
49
  return new Promise((resolve, reject) => {
50
+ let handleKeypress = null;
51
+ const cleanup = () => {
52
+ if (handleKeypress) {
53
+ process.stdin.removeListener('data', handleKeypress);
54
+ }
55
+ this.disableRawMode();
56
+ };
50
57
  try {
51
58
  this.enableRawMode();
52
- this.displayInputPrompt('');
53
- const handleKeypress = (chunk) => {
59
+ this.displayInputPrompt(this.getCurrentContent());
60
+ handleKeypress = (chunk) => {
54
61
  const key = this.parseKeyEvent(chunk);
55
62
  if (this.handleSpecialKeys(key)) {
56
63
  return;
@@ -61,10 +68,6 @@ export class TextInputHandlerImpl {
61
68
  this.displayInputPrompt(this.getCurrentContent());
62
69
  }
63
70
  };
64
- const cleanup = () => {
65
- process.stdin.removeListener('data', handleKeypress);
66
- this.disableRawMode();
67
- };
68
71
  // Set up completion handlers
69
72
  const complete = (result) => {
70
73
  cleanup();
@@ -86,7 +89,7 @@ export class TextInputHandlerImpl {
86
89
  process.stdin.on('data', handleKeypress);
87
90
  }
88
91
  catch (error) {
89
- this.disableRawMode();
92
+ cleanup();
90
93
  reject(error);
91
94
  }
92
95
  });
@@ -322,15 +325,17 @@ export class TextInputHandlerImpl {
322
325
  }
323
326
  break;
324
327
  case 'right':
325
- const currentLineLength = content[cursorPosition.line]?.length || 0;
326
- if (cursorPosition.column < currentLineLength) {
327
- cursorPosition.column++;
328
- }
329
- else if (cursorPosition.line < content.length - 1) {
330
- cursorPosition.line++;
331
- cursorPosition.column = 0;
328
+ {
329
+ const currentLineLength = content[cursorPosition.line]?.length || 0;
330
+ if (cursorPosition.column < currentLineLength) {
331
+ cursorPosition.column++;
332
+ }
333
+ else if (cursorPosition.line < content.length - 1) {
334
+ cursorPosition.line++;
335
+ cursorPosition.column = 0;
336
+ }
337
+ break;
332
338
  }
333
- break;
334
339
  }
335
340
  }
336
341
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.0",
3
+ "version": "3.9.2",
4
4
  "description": "Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security",
5
5
  "keywords": [
6
6
  "lanonasis",