@lanonasis/cli 3.9.0 → 3.9.1

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,19 @@
1
1
  # Changelog - @lanonasis/cli
2
2
 
3
+ ## [3.9.1] - 2026-02-01
4
+
5
+ ### 🔐 Authentication Fixes
6
+
7
+ - **OAuth Scope Clarification**: OAuth login now clearly states it enables MCP integration only
8
+ - **Improved Error Messages**: 401 errors for OAuth users include specific guidance for direct API access
9
+ - **Removed Misleading Storage**: OAuth tokens are no longer incorrectly stored as vendor keys
10
+ - **Documentation Updates**: README and in-CLI help clarify authentication method differences
11
+
12
+ ### 🐛 Bug Fixes
13
+
14
+ - Fixed confusing error message when OAuth users try to use direct CLI commands
15
+ - Removed `.lanonasis/mcp-config.json` from version control
16
+
3
17
  ## [3.9.0] - 2026-02-01
4
18
 
5
19
  ### 🎨 CLI UX Revolution
@@ -26,6 +40,21 @@
26
40
  - **User Preferences**: Captures and persists input mode, editor choice, and behavior preferences
27
41
  - **Troubleshooting Guidance**: Context-aware help when issues are detected
28
42
 
43
+ ### 🔐 Authentication Clarifications
44
+
45
+ #### OAuth vs Direct API Access
46
+ - **Clear Scope Documentation**: OAuth2 login now explicitly states it enables MCP integration
47
+ - **Improved Error Messages**: 401 errors for OAuth users include specific guidance for direct API access
48
+ - **Authentication Method Guidance**: CLI provides clear instructions for:
49
+ - **OAuth**: Use for MCP integration and real-time features
50
+ - **Vendor Key**: Obtain from dashboard for direct API access (`lanonasis auth login --vendor`)
51
+ - **Credentials**: Use username/password for direct API access (`lanonasis auth login --credentials`)
52
+
53
+ #### Secure Storage Fallback
54
+ - **Keytar Optional**: When keytar (native secure storage) is unavailable, CLI gracefully falls back to encrypted file storage
55
+ - **Cross-Platform**: Encrypted storage works consistently across all platforms
56
+ - **No Data Loss**: Credentials are preserved in `~/.lanonasis/api-key.enc` with AES-256-GCM encryption
57
+
29
58
  ### 🐛 Critical Bug Fixes (PR #93)
30
59
 
31
60
  #### 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:
@@ -702,23 +702,22 @@ async function handleOAuthFlow(config) {
702
702
  }
703
703
  const tokens = await exchangeCodeForTokens(code, pkce.verifier, authBase, redirectUri);
704
704
  spinner.succeed('Access tokens received');
705
- // Store OAuth tokens - these are already valid auth-gateway tokens from /oauth/token
706
- // No need for additional exchange since /oauth/token returns auth-gateway's own tokens
705
+ // Store OAuth tokens - these are auth-gateway tokens from /oauth/token
706
+ // Note: OAuth tokens are valid for MCP services but not for direct API access
707
707
  await config.setToken(tokens.access_token);
708
708
  await config.set('refresh_token', tokens.refresh_token);
709
709
  await config.set('token_expires_at', Date.now() + (tokens.expires_in * 1000));
710
710
  await config.set('authMethod', 'oauth');
711
- // The OAuth access token from auth-gateway works as the API token for all services
712
- // Store it as the vendor key equivalent for MCP and API access
713
- spinner.text = 'Configuring unified access...';
714
- spinner.start();
715
- // Use the OAuth access token directly - it's already an auth-gateway token
716
- await config.setVendorKey(tokens.access_token);
717
- spinner.succeed('Unified authentication configured');
711
+ spinner.succeed('OAuth tokens stored');
718
712
  console.log();
719
713
  console.log(chalk.green('✓ OAuth2 authentication successful'));
720
- console.log(colors.info('You can now use all Lanonasis services'));
721
- console.log(chalk.gray('✓ MCP, API, and CLI access configured'));
714
+ console.log(colors.info('You can now use MCP integration features'));
715
+ console.log();
716
+ console.log(chalk.yellow('Note: ') + chalk.gray('OAuth login enables MCP integration.'));
717
+ console.log(chalk.gray('For direct CLI memory commands, use:'));
718
+ console.log(chalk.cyan(' lanonasis auth login --vendor') + chalk.gray(' (get a vendor key from dashboard)'));
719
+ console.log(chalk.gray(' OR'));
720
+ console.log(chalk.cyan(' lanonasis auth login --credentials') + chalk.gray(' (use username/password)'));
722
721
  process.exit(0);
723
722
  }
724
723
  catch (error) {
@@ -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') {
package/dist/utils/api.js CHANGED
@@ -60,7 +60,17 @@ export class APIClient {
60
60
  const { status, data } = error.response;
61
61
  if (status === 401) {
62
62
  console.error(chalk.red('✖ Authentication failed'));
63
- console.log(chalk.yellow('Please run:'), chalk.white('memory login'));
63
+ // Check if user is using OAuth - OAuth tokens only work with MCP, not direct API
64
+ const authMethod = this.config.get('authMethod');
65
+ if (authMethod === 'oauth') {
66
+ console.log(chalk.yellow('\nNote: OAuth tokens are for MCP integration only.'));
67
+ console.log(chalk.gray('For direct API access, you have two options:'));
68
+ console.log(chalk.gray(' 1. Get a vendor key from the dashboard: ') + chalk.cyan('lanonasis auth login --vendor'));
69
+ console.log(chalk.gray(' 2. Login with username/password: ') + chalk.cyan('lanonasis auth login --credentials'));
70
+ }
71
+ else {
72
+ console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
73
+ }
64
74
  process.exit(1);
65
75
  }
66
76
  if (status === 403) {
@@ -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.1",
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",