@lanonasis/cli 3.9.8 → 3.9.9

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.
@@ -162,7 +162,7 @@ apiKeysCommand
162
162
  validate: (input) => input.length > 0 || 'Value is required'
163
163
  },
164
164
  {
165
- type: 'list',
165
+ type: 'select',
166
166
  name: 'keyType',
167
167
  message: 'Key type:',
168
168
  when: !keyData.keyType,
@@ -177,21 +177,21 @@ apiKeysCommand
177
177
  ]
178
178
  },
179
179
  {
180
- type: 'list',
180
+ type: 'select',
181
181
  name: 'environment',
182
182
  message: 'Environment:',
183
183
  choices: ['development', 'staging', 'production'],
184
184
  default: 'development'
185
185
  },
186
186
  {
187
- type: 'list',
187
+ type: 'select',
188
188
  name: 'projectId',
189
189
  message: 'Select project:',
190
190
  when: !keyData.projectId && projects.length > 0,
191
191
  choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id }))
192
192
  },
193
193
  {
194
- type: 'list',
194
+ type: 'select',
195
195
  name: 'accessLevel',
196
196
  message: 'Access level:',
197
197
  choices: ['public', 'authenticated', 'team', 'admin', 'enterprise'],
@@ -543,7 +543,7 @@ mcpCommand
543
543
  default: false
544
544
  },
545
545
  {
546
- type: 'list',
546
+ type: 'select',
547
547
  name: 'riskLevel',
548
548
  message: 'Risk level:',
549
549
  choices: ['low', 'medium', 'high', 'critical'],
@@ -647,7 +647,7 @@ mcpCommand
647
647
  const tools = await apiClient.get('/api-keys/mcp/tools');
648
648
  const answers = await inquirer.prompt([
649
649
  {
650
- type: 'list',
650
+ type: 'select',
651
651
  name: 'toolId',
652
652
  message: 'Select MCP tool:',
653
653
  when: !requestData.toolId && tools.length > 0,
@@ -672,7 +672,7 @@ mcpCommand
672
672
  validate: (input) => input.length > 0 || 'At least one key name is required'
673
673
  },
674
674
  {
675
- type: 'list',
675
+ type: 'select',
676
676
  name: 'environment',
677
677
  message: 'Environment:',
678
678
  when: !requestData.environment,
@@ -569,7 +569,7 @@ export async function loginCommand(options) {
569
569
  // Show authentication options
570
570
  const authChoice = await inquirer.prompt([
571
571
  {
572
- type: 'list',
572
+ type: 'select',
573
573
  name: 'method',
574
574
  message: 'Choose authentication method:',
575
575
  choices: [
@@ -821,7 +821,7 @@ async function handleCredentialsFlow(options, config) {
821
821
  }
822
822
  // Store JWT token for API authentication
823
823
  await config.setToken(authToken);
824
- await config.set('authMethod', 'jwt');
824
+ await config.setAndSave('authMethod', 'jwt');
825
825
  spinner.succeed('Login successful');
826
826
  console.log();
827
827
  console.log(chalk.green('✓ Authenticated successfully'));
@@ -188,7 +188,7 @@ export class UserGuidanceSystem {
188
188
  default: 'https://api.lanonasis.com/api/v1'
189
189
  },
190
190
  {
191
- type: 'list',
191
+ type: 'select',
192
192
  name: 'outputFormat',
193
193
  message: 'Preferred output format:',
194
194
  choices: ['table', 'json', 'yaml', 'csv'],
@@ -206,7 +206,7 @@ export class UserGuidanceSystem {
206
206
  console.log();
207
207
  const { authMethod } = await inquirer.prompt([
208
208
  {
209
- type: 'list',
209
+ type: 'select',
210
210
  name: 'authMethod',
211
211
  message: 'Choose authentication method:',
212
212
  choices: [
@@ -335,7 +335,7 @@ export class UserGuidanceSystem {
335
335
  console.log();
336
336
  const { shell } = await inquirer.prompt([
337
337
  {
338
- type: 'list',
338
+ type: 'select',
339
339
  name: 'shell',
340
340
  message: 'Which shell do you use?',
341
341
  choices: [
@@ -4,9 +4,42 @@ import { table } from 'table';
4
4
  import { getMCPClient } from '../utils/mcp-client.js';
5
5
  import { EnhancedMCPClient } from '../mcp/client/enhanced-client.js';
6
6
  import { CLIConfig } from '../utils/config.js';
7
+ import { apiClient } from '../utils/api.js';
7
8
  import WebSocket from 'ws';
8
9
  import { dirname, join } from 'path';
9
10
  import { createConnectionManager } from '../ux/index.js';
11
+ const tokenizeQuery = (input) => input
12
+ .toLowerCase()
13
+ .split(/[^a-z0-9]+/g)
14
+ .map((token) => token.trim())
15
+ .filter((token) => token.length >= 2);
16
+ const lexicalScore = (query, memory) => {
17
+ const tokens = tokenizeQuery(query);
18
+ if (tokens.length === 0)
19
+ return 0;
20
+ const haystack = `${memory.title || ''} ${memory.content || ''} ${(memory.tags || []).join(' ')}`.toLowerCase();
21
+ const hits = tokens.filter((token) => haystack.includes(token)).length;
22
+ if (hits === 0)
23
+ return 0;
24
+ const ratio = hits / tokens.length;
25
+ return Math.max(0.35, Math.min(0.69, Number((ratio * 0.65).toFixed(3))));
26
+ };
27
+ const fallbackMemorySearch = async (query, limit) => {
28
+ const candidateLimit = Math.min(Math.max(limit * 8, 50), 200);
29
+ const memoriesResult = await apiClient.getMemories({ page: 1, limit: candidateLimit });
30
+ const candidates = (memoriesResult.memories || memoriesResult.data || []);
31
+ return candidates
32
+ .map((memory) => ({
33
+ id: memory.id,
34
+ title: memory.title,
35
+ memory_type: memory.memory_type,
36
+ similarity_score: lexicalScore(query, memory),
37
+ content: memory.content || ''
38
+ }))
39
+ .filter((memory) => memory.similarity_score > 0)
40
+ .sort((a, b) => b.similarity_score - a.similarity_score)
41
+ .slice(0, limit);
42
+ };
10
43
  /**
11
44
  * Register MCP-related CLI commands (mcp and mcp-server) on a Commander program.
12
45
  *
@@ -460,25 +493,54 @@ export function mcpCommands(program) {
460
493
  });
461
494
  memory.command('search')
462
495
  .description('Search memories via MCP')
463
- .argument('<query>', 'Search query')
496
+ .argument('<query...>', 'Search query')
464
497
  .option('-l, --limit <number>', 'Maximum results', '10')
465
498
  .option('-t, --threshold <number>', 'Similarity threshold (0-1)', '0.55')
466
- .action(async (query, options) => {
499
+ .action(async (queryParts, options) => {
500
+ const query = Array.isArray(queryParts) ? queryParts.join(' ').trim() : String(queryParts || '').trim();
501
+ if (!query) {
502
+ console.error(chalk.red('Search query is required'));
503
+ process.exit(1);
504
+ }
467
505
  const spinner = ora('Searching memories via MCP...').start();
506
+ const client = getMCPClient();
468
507
  try {
469
- const client = getMCPClient();
470
508
  if (!client.isConnectedToServer()) {
471
509
  spinner.info('Not connected. Attempting auto-connect...');
472
510
  const config = new CLIConfig();
473
511
  const useRemote = !!config.get('token');
474
512
  await client.connect({ useRemote });
475
513
  }
476
- const results = await client.callTool('memory_search_memories', {
477
- query,
478
- limit: parseInt(options.limit),
479
- threshold: parseFloat(options.threshold)
480
- });
481
- spinner.succeed(`Found ${results.length} memories`);
514
+ const limit = parseInt(options.limit);
515
+ const threshold = parseFloat(options.threshold);
516
+ let results = [];
517
+ let usedLexicalFallback = false;
518
+ try {
519
+ const rawResult = await client.callTool('memory_search_memories', {
520
+ query,
521
+ limit,
522
+ threshold
523
+ });
524
+ results = Array.isArray(rawResult)
525
+ ? rawResult
526
+ : Array.isArray(rawResult?.results)
527
+ ? rawResult.results
528
+ : Array.isArray(rawResult?.result)
529
+ ? rawResult.result
530
+ : [];
531
+ }
532
+ catch (error) {
533
+ const message = error instanceof Error ? error.message : String(error);
534
+ if (/vector dimensions|hybrid search failed|memory search failed/i.test(message)) {
535
+ spinner.info('Semantic search unavailable in MCP path, using lexical fallback...');
536
+ results = await fallbackMemorySearch(query, limit);
537
+ usedLexicalFallback = true;
538
+ }
539
+ else {
540
+ throw error;
541
+ }
542
+ }
543
+ spinner.succeed(`Found ${results.length} memories${usedLexicalFallback ? ' (lexical fallback)' : ''}`);
482
544
  if (results.length === 0) {
483
545
  console.log(chalk.yellow('\nNo memories found matching your query'));
484
546
  return;
@@ -496,6 +558,11 @@ export function mcpCommands(program) {
496
558
  spinner.fail(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
497
559
  process.exit(1);
498
560
  }
561
+ finally {
562
+ if (client.isConnectedToServer()) {
563
+ await client.disconnect().catch(() => { });
564
+ }
565
+ }
499
566
  });
500
567
  // Configure MCP preferences
501
568
  mcp.command('config')
@@ -209,7 +209,10 @@ const createIntelligenceTransport = async () => {
209
209
  const authToken = config.getToken();
210
210
  const apiKey = await config.getVendorKeyAsync();
211
211
  const apiUrl = `${config.getApiUrl().replace(/\/$/, '')}/api/v1`;
212
- if (authToken) {
212
+ const authMethod = config.getAuthMethod();
213
+ // When vendor_key is explicitly set, skip JWT even if a token exists in storage.
214
+ // JWT validates against auth-gateway's DB; vendor keys validate against mcp-core's DB.
215
+ if (authToken && authMethod !== 'vendor_key') {
213
216
  return {
214
217
  mode: 'sdk',
215
218
  client: new MemoryIntelligenceClient({
@@ -221,7 +224,7 @@ const createIntelligenceTransport = async () => {
221
224
  };
222
225
  }
223
226
  if (apiKey) {
224
- if (apiKey.startsWith('lano_')) {
227
+ if (apiKey.startsWith('lano_') || apiKey.startsWith('lms_')) {
225
228
  return {
226
229
  mode: 'sdk',
227
230
  client: new MemoryIntelligenceClient({
@@ -232,7 +235,7 @@ const createIntelligenceTransport = async () => {
232
235
  }),
233
236
  };
234
237
  }
235
- // Legacy non-lano key path: use CLI API client auth middleware directly.
238
+ // Legacy key path: use CLI API client auth middleware directly.
236
239
  return { mode: 'api' };
237
240
  }
238
241
  throw new Error('Authentication required. Run "lanonasis auth login" first.');
@@ -324,7 +327,7 @@ export function memoryCommands(program) {
324
327
  validate: (input) => input.length > 0 || 'Title is required',
325
328
  },
326
329
  {
327
- type: 'list',
330
+ type: 'select',
328
331
  name: 'type',
329
332
  message: 'Memory type:',
330
333
  choices: [...MEMORY_TYPE_CHOICES],
@@ -670,13 +673,18 @@ export function memoryCommands(program) {
670
673
  program
671
674
  .command('search')
672
675
  .description('Search memories using semantic search')
673
- .argument('<query>', 'search query')
676
+ .argument('<query...>', 'search query')
674
677
  .option('-l, --limit <limit>', 'number of results', '20')
675
678
  .option('--threshold <threshold>', 'similarity threshold (0-1)', '0.55')
676
679
  .option('--type <types>', 'filter by memory types (comma-separated)')
677
680
  .option('--tags <tags>', 'filter by tags (comma-separated)')
678
- .action(async (query, options) => {
681
+ .action(async (queryParts, options) => {
679
682
  try {
683
+ const query = Array.isArray(queryParts) ? queryParts.join(' ').trim() : String(queryParts || '').trim();
684
+ if (!query) {
685
+ console.error(chalk.red('✖ Search query is required'));
686
+ process.exit(1);
687
+ }
680
688
  const spinner = ora(`Searching for "${query}"...`).start();
681
689
  const requestedThreshold = clampThreshold(parseFloat(options.threshold || '0.55'));
682
690
  const searchOptions = {
@@ -694,20 +702,27 @@ export function memoryCommands(program) {
694
702
  let results = [];
695
703
  let thresholdUsed = requestedThreshold;
696
704
  let searchStrategy = 'semantic';
705
+ let semanticSearchError = null;
697
706
  for (const threshold of thresholdPlan) {
698
- const attempt = await apiClient.searchMemories(query, {
699
- ...searchOptions,
700
- threshold,
701
- });
702
- const attemptResults = (attempt.results || attempt.data || []);
703
- result = attempt;
704
- if (attemptResults.length > 0) {
705
- results = attemptResults;
706
- thresholdUsed = threshold;
707
- const attemptStrategy = attempt.search_strategy;
708
- searchStrategy = typeof attemptStrategy === 'string'
709
- ? attemptStrategy
710
- : 'semantic';
707
+ try {
708
+ const attempt = await apiClient.searchMemories(query, {
709
+ ...searchOptions,
710
+ threshold,
711
+ });
712
+ const attemptResults = (attempt.results || attempt.data || []);
713
+ result = attempt;
714
+ if (attemptResults.length > 0) {
715
+ results = attemptResults;
716
+ thresholdUsed = threshold;
717
+ const attemptStrategy = attempt.search_strategy;
718
+ searchStrategy = typeof attemptStrategy === 'string'
719
+ ? attemptStrategy
720
+ : 'semantic';
721
+ break;
722
+ }
723
+ }
724
+ catch (error) {
725
+ semanticSearchError = error instanceof Error ? error.message : 'Unknown semantic search error';
711
726
  break;
712
727
  }
713
728
  }
@@ -721,11 +736,19 @@ export function memoryCommands(program) {
721
736
  spinner.stop();
722
737
  if (results.length === 0) {
723
738
  console.log(chalk.yellow('No memories found matching your search'));
739
+ if (semanticSearchError) {
740
+ console.log(chalk.gray(`Semantic search error: ${semanticSearchError}`));
741
+ }
724
742
  console.log(chalk.gray(`Tried thresholds: ${thresholdPlan.map((t) => t.toFixed(2)).join(', ')}`));
725
743
  return;
726
744
  }
727
- console.log(chalk.blue.bold(`\n🔍 Search Results (${result.total_results || results.length} found)`));
728
- console.log(chalk.gray(`Query: "${query}" | Search time: ${result.search_time_ms || 0}ms`));
745
+ if (semanticSearchError && searchStrategy === 'cli_lexical_fallback') {
746
+ console.log(chalk.yellow(`⚠ Semantic search unavailable, using lexical fallback (${semanticSearchError})`));
747
+ }
748
+ const totalResults = typeof result?.total_results === 'number' ? result.total_results : results.length;
749
+ const searchTimeMs = typeof result?.search_time_ms === 'number' ? result.search_time_ms : 0;
750
+ console.log(chalk.blue.bold(`\n🔍 Search Results (${totalResults} found)`));
751
+ console.log(chalk.gray(`Query: "${query}" | Search time: ${searchTimeMs}ms`));
729
752
  if (Math.abs(thresholdUsed - requestedThreshold) > 0.0001) {
730
753
  console.log(chalk.gray(`No matches at ${requestedThreshold.toFixed(2)}; used adaptive threshold ${thresholdUsed.toFixed(2)}`));
731
754
  }
@@ -820,7 +843,7 @@ export function memoryCommands(program) {
820
843
  default: currentMemory.title,
821
844
  },
822
845
  {
823
- type: 'list',
846
+ type: 'select',
824
847
  name: 'type',
825
848
  message: 'Memory type:',
826
849
  choices: [...MEMORY_TYPE_CHOICES],
@@ -280,7 +280,7 @@ export class InteractiveMemoryCreator {
280
280
  console.log(chalk.cyan(`📎 I noticed this looks like ${suggestions.contentType}. Would you like to:`));
281
281
  const { topicChoice } = await inquirer.prompt([
282
282
  {
283
- type: 'list',
283
+ type: 'select',
284
284
  name: 'topicChoice',
285
285
  message: 'Select topic:',
286
286
  choices: [
@@ -336,7 +336,7 @@ export class InteractiveMemoryCreator {
336
336
  // Memory type selection
337
337
  const { memoryType } = await inquirer.prompt([
338
338
  {
339
- type: 'list',
339
+ type: 'select',
340
340
  name: 'memoryType',
341
341
  message: 'Memory Type:',
342
342
  choices: [
@@ -358,7 +358,7 @@ export class InteractiveMemoryCreator {
358
358
  // Confirm save
359
359
  const { action } = await inquirer.prompt([
360
360
  {
361
- type: 'list',
361
+ type: 'select',
362
362
  name: 'action',
363
363
  message: 'Ready to save?',
364
364
  choices: [
@@ -459,7 +459,7 @@ export class InteractiveSearch {
459
459
  // Result actions
460
460
  const { action } = await inquirer.prompt([
461
461
  {
462
- type: 'list',
462
+ type: 'select',
463
463
  name: 'action',
464
464
  message: 'Actions:',
465
465
  choices: [
package/dist/index.js CHANGED
@@ -91,7 +91,14 @@ program
91
91
  actionCommand.parent?.name?.() === 'mcp-server';
92
92
  const isConfigFlow = actionCommand.name() === 'config' ||
93
93
  actionCommand.parent?.name?.() === 'config';
94
- if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
94
+ // Memory, topic, org, and key commands use the direct REST API — skip MCP auto-connect
95
+ const isDirectApiFlow = actionCommand.name() === 'memory' ||
96
+ actionCommand.parent?.name?.() === 'memory' ||
97
+ actionCommand.name() === 'topic' ||
98
+ actionCommand.parent?.name?.() === 'topic' ||
99
+ actionCommand.name() === 'org' ||
100
+ actionCommand.parent?.name?.() === 'org';
101
+ if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !isDirectApiFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
95
102
  try {
96
103
  const client = getMCPClient();
97
104
  if (!client.isConnectedToServer()) {
File without changes
@@ -30,6 +30,7 @@ export interface MemoryEntry {
30
30
  memory_type: MemoryType;
31
31
  tags: string[];
32
32
  topic_id?: string | null;
33
+ topic_key?: string | null;
33
34
  user_id: string;
34
35
  organization_id: string;
35
36
  metadata?: Record<string, unknown>;
@@ -44,6 +45,7 @@ export interface CreateMemoryRequest {
44
45
  memory_type?: MemoryType;
45
46
  tags?: string[];
46
47
  topic_id?: string;
48
+ topic_key?: string;
47
49
  metadata?: Record<string, unknown>;
48
50
  continuity_key?: string;
49
51
  idempotency_key?: string;
@@ -55,6 +57,7 @@ export interface UpdateMemoryRequest {
55
57
  memory_type?: MemoryType;
56
58
  tags?: string[];
57
59
  topic_id?: string | null;
60
+ topic_key?: string;
58
61
  metadata?: Record<string, unknown>;
59
62
  continuity_key?: string;
60
63
  idempotency_key?: string;
@@ -67,6 +70,8 @@ export interface GetMemoriesParams {
67
70
  memory_type?: MemoryType;
68
71
  tags?: string[] | string;
69
72
  topic_id?: string;
73
+ topic_key?: string;
74
+ include_deleted?: boolean;
70
75
  user_id?: string;
71
76
  sort?: 'created_at' | 'updated_at' | 'last_accessed' | 'access_count' | 'title';
72
77
  order?: 'asc' | 'desc';
@@ -78,8 +83,11 @@ export interface SearchMemoryRequest {
78
83
  memory_types?: MemoryType[];
79
84
  tags?: string[];
80
85
  topic_id?: string;
86
+ topic_key?: string;
81
87
  limit?: number;
82
88
  threshold?: number;
89
+ include_deleted?: boolean;
90
+ response_mode?: 'full' | 'compact' | 'timeline';
83
91
  }
84
92
  export interface MemorySearchResult extends MemoryEntry {
85
93
  similarity_score: number;
@@ -180,6 +188,10 @@ export declare class APIClient {
180
188
  private normalizeMemoryEntry;
181
189
  private shouldUseLegacyMemoryRpcFallback;
182
190
  private shouldRetryViaApiGateway;
191
+ private shouldRetryViaSupabaseMemoryFunctions;
192
+ private shouldUsePostListFallback;
193
+ private getSupabaseFunctionsBaseUrl;
194
+ private mapMemoryApiRouteToSupabaseFunctions;
183
195
  private normalizeMcpPathToApi;
184
196
  constructor();
185
197
  login(email: string, password: string): Promise<AuthResponse>;
package/dist/utils/api.js CHANGED
@@ -52,6 +52,135 @@ export class APIClient {
52
52
  return false;
53
53
  return code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED';
54
54
  }
55
+ shouldRetryViaSupabaseMemoryFunctions(error) {
56
+ const status = Number(error?.response?.status || 0);
57
+ if (status !== 401 && status !== 404)
58
+ return false;
59
+ const cfg = (error?.config || {});
60
+ if (cfg.__retriedViaSupabaseMemoryFunctions || cfg.__useSupabaseMemoryFunctions)
61
+ return false;
62
+ const baseURL = String(cfg.baseURL || '');
63
+ if (baseURL.includes('supabase.co'))
64
+ return false;
65
+ const requestUrl = String(cfg.url || '');
66
+ const normalizedRequestUrl = requestUrl.startsWith('/memory')
67
+ ? this.normalizeMcpPathToApi(requestUrl)
68
+ : requestUrl;
69
+ if (!normalizedRequestUrl.startsWith('/api/v1/memories'))
70
+ return false;
71
+ const errorData = error?.response?.data;
72
+ const responseText = typeof errorData === 'string'
73
+ ? errorData
74
+ : `${errorData?.message || ''} ${errorData?.error || ''}`;
75
+ if (status === 401) {
76
+ const indicatesRouteShapeDrift = /invalid jwt|missing authorization header|authentication required|token is not active or has expired/i
77
+ .test(responseText);
78
+ if (!indicatesRouteShapeDrift)
79
+ return false;
80
+ }
81
+ if (status === 404) {
82
+ const isGetByIdRequest = /^\/api\/v1\/memories\/[^/?#]+$/.test(normalizedRequestUrl);
83
+ const indicatesMissingMcpGetRoute = /cannot get \/api\/v1\/memory\/|cannot get \/memory\/|route[_ -]?not[_ -]?found/i
84
+ .test(responseText.toLowerCase());
85
+ if (!isGetByIdRequest || !indicatesMissingMcpGetRoute)
86
+ return false;
87
+ }
88
+ const authMethod = String(this.config.get('authMethod') || '');
89
+ const token = this.config.getToken();
90
+ const hasOpaqueToken = Boolean(token) && token.split('.').length !== 3;
91
+ const hasVendorKey = this.config.hasVendorKey();
92
+ return hasVendorKey || hasOpaqueToken || authMethod === 'oauth' || authMethod === 'oauth2';
93
+ }
94
+ shouldUsePostListFallback(error) {
95
+ const status = Number(error?.response?.status || 0);
96
+ if (status === 405)
97
+ return true;
98
+ if (status !== 401)
99
+ return false;
100
+ const message = String(error?.response?.data?.message || error?.response?.data?.error || '');
101
+ return /missing authorization header|authentication required/i.test(message);
102
+ }
103
+ getSupabaseFunctionsBaseUrl() {
104
+ const discoveredServices = this.config.get('discoveredServices');
105
+ const candidates = [
106
+ process.env.LANONASIS_SUPABASE_URL,
107
+ process.env.SUPABASE_URL,
108
+ discoveredServices?.memory_base
109
+ ];
110
+ for (const candidate of candidates) {
111
+ if (typeof candidate === 'string'
112
+ && candidate.includes('supabase.co')
113
+ && !candidate.includes('your-project.supabase.co')
114
+ && !candidate.includes('<project-ref>.supabase.co')) {
115
+ return candidate.replace(/\/$/, '');
116
+ }
117
+ }
118
+ return 'https://lanonasis.supabase.co';
119
+ }
120
+ mapMemoryApiRouteToSupabaseFunctions(config, token, vendorKey) {
121
+ const method = String(config.method || 'get').toLowerCase();
122
+ const rawUrl = String(config.url || '');
123
+ const url = rawUrl.startsWith('/memory')
124
+ ? this.normalizeMcpPathToApi(rawUrl)
125
+ : rawUrl;
126
+ const mapped = config;
127
+ mapped.baseURL = this.getSupabaseFunctionsBaseUrl();
128
+ mapped.headers = mapped.headers || {};
129
+ if (token) {
130
+ mapped.headers.Authorization = `Bearer ${token}`;
131
+ delete mapped.headers['X-API-Key'];
132
+ }
133
+ else if (vendorKey) {
134
+ mapped.headers['X-API-Key'] = vendorKey;
135
+ }
136
+ // Supabase functions do not need X-Auth-Method and can reject unexpected values.
137
+ delete mapped.headers['X-Auth-Method'];
138
+ mapped.headers['X-Project-Scope'] = 'lanonasis-maas';
139
+ if (method === 'get' && url === '/api/v1/memories') {
140
+ mapped.url = '/functions/v1/memory-list';
141
+ return mapped;
142
+ }
143
+ if (method === 'post' && url === '/api/v1/memories') {
144
+ mapped.url = '/functions/v1/memory-create';
145
+ return mapped;
146
+ }
147
+ if (method === 'post' && url === '/api/v1/memories/search') {
148
+ mapped.url = '/functions/v1/memory-search';
149
+ return mapped;
150
+ }
151
+ if (method === 'get' && url === '/api/v1/memories/stats') {
152
+ mapped.url = '/functions/v1/memory-stats';
153
+ return mapped;
154
+ }
155
+ if (method === 'post' && url === '/api/v1/memories/bulk/delete') {
156
+ mapped.url = '/functions/v1/memory-bulk-delete';
157
+ return mapped;
158
+ }
159
+ const idMatch = url.match(/^\/api\/v1\/memories\/([^/?#]+)$/);
160
+ if (idMatch) {
161
+ const id = decodeURIComponent(idMatch[1] || '');
162
+ if (method === 'get') {
163
+ mapped.url = '/functions/v1/memory-get';
164
+ mapped.params = { ...(config.params || {}), id };
165
+ return mapped;
166
+ }
167
+ if (method === 'put' || method === 'patch') {
168
+ mapped.method = 'post';
169
+ mapped.url = '/functions/v1/memory-update';
170
+ const body = (config.data && typeof config.data === 'object')
171
+ ? config.data
172
+ : {};
173
+ mapped.data = { id, ...body };
174
+ return mapped;
175
+ }
176
+ if (method === 'delete') {
177
+ mapped.url = '/functions/v1/memory-delete';
178
+ mapped.params = { ...(config.params || {}), id };
179
+ return mapped;
180
+ }
181
+ }
182
+ return mapped;
183
+ }
55
184
  normalizeMcpPathToApi(url) {
56
185
  // MCP HTTP compatibility path -> API gateway REST paths
57
186
  if (url === '/memory') {
@@ -80,33 +209,50 @@ export class APIClient {
80
209
  const authMethod = this.config.get('authMethod');
81
210
  const vendorKey = await this.config.getVendorKeyAsync();
82
211
  const token = this.config.getToken();
83
- const isMemoryEndpoint = typeof config.url === 'string' && config.url.startsWith('/api/v1/memories');
212
+ const useSupabaseMemoryFunctions = config.__useSupabaseMemoryFunctions === true;
213
+ const normalizedMemoryUrl = typeof config.url === 'string'
214
+ ? this.normalizeMcpPathToApi(config.url)
215
+ : '';
216
+ const isMemoryEndpoint = normalizedMemoryUrl.startsWith('/api/v1/memories');
84
217
  const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true'
85
218
  || process.env.CLI_FORCE_API === 'true'
86
219
  || process.env.ONASIS_FORCE_API === 'true';
87
220
  const forceApiFromConfig = this.config.get('forceApi') === true
88
221
  || this.config.get('connectionTransport') === 'api';
89
- // Memory CRUD/search endpoints should always use the API gateway path.
222
+ if (useSupabaseMemoryFunctions && isMemoryEndpoint) {
223
+ const remapped = this.mapMemoryApiRouteToSupabaseFunctions(config, token || undefined, vendorKey || undefined);
224
+ if (process.env.CLI_VERBOSE === 'true') {
225
+ const requestId = randomUUID();
226
+ remapped.headers['X-Request-ID'] = requestId;
227
+ remapped.headers['X-Transport-Mode'] = 'supabase-functions-fallback';
228
+ console.log(chalk.dim(`→ ${String(remapped.method || 'get').toUpperCase()} ${remapped.url} [${requestId}]`));
229
+ console.log(chalk.dim(` transport=supabase-functions-fallback baseURL=${remapped.baseURL}`));
230
+ }
231
+ return remapped;
232
+ }
90
233
  const forceDirectApiRetry = config
91
234
  .__forceDirectApiGatewayRetry === true;
92
- const forceDirectApi = forceApiFromEnv || forceApiFromConfig || isMemoryEndpoint || forceDirectApiRetry;
235
+ // NOTE: isMemoryEndpoint is intentionally NOT in forceDirectApi.
236
+ // api.lanonasis.com is the vendor AI proxy, NOT the memory service.
237
+ // Memory operations must go to mcp.lanonasis.com.
238
+ const forceDirectApi = forceApiFromEnv || forceApiFromConfig || forceDirectApiRetry;
93
239
  const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
94
240
  const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
95
241
  // Determine the correct API base URL:
96
242
  // - Auth endpoints -> auth.lanonasis.com
97
- // - JWT auth (no vendor key) -> mcp.lanonasis.com (supports JWT tokens)
98
- // - Vendor key auth -> api.lanonasis.com (requires vendor key)
243
+ // - Memory/MCP operations (JWT or vendor key) -> mcp.lanonasis.com (the memory service)
244
+ // - Other direct API calls -> api.lanonasis.com (vendor AI proxy)
99
245
  let apiBaseUrl;
100
- const useMcpServer = !forceDirectApi && prefersTokenAuth && !useVendorKeyAuth;
246
+ const useMcpServer = !forceDirectApi && !isAuthEndpoint && (prefersTokenAuth || useVendorKeyAuth || isMemoryEndpoint);
101
247
  if (isAuthEndpoint) {
102
248
  apiBaseUrl = discoveredServices?.auth_base || 'https://auth.lanonasis.com';
103
249
  }
104
250
  else if (forceDirectApi) {
105
- // Force direct REST API mode to bypass MCP routing for troubleshooting.
251
+ // Explicit force: direct to api.lanonasis.com for troubleshooting.
106
252
  apiBaseUrl = this.config.getApiUrl();
107
253
  }
108
254
  else if (useMcpServer) {
109
- // JWT/OAuth tokens work with mcp.lanonasis.com
255
+ // Memory service lives at mcp.lanonasis.com — accepts JWT, OAuth, and vendor keys.
110
256
  apiBaseUrl = 'https://mcp.lanonasis.com/api/v1';
111
257
  }
112
258
  else {
@@ -123,9 +269,7 @@ export class APIClient {
123
269
  config.headers['X-Project-Scope'] = 'lanonasis-maas';
124
270
  }
125
271
  // Enhanced Authentication Support
126
- // Even in forced direct-API mode, prefer bearer token auth when available.
127
- // This avoids accidentally sending an OAuth access token as X-API-Key (we store it
128
- // in secure storage for MCP/WebSocket usage), which can cause 401s.
272
+ // In forced direct-API mode, prefer bearer token auth when available.
129
273
  const preferVendorKeyInDirectApiMode = forceDirectApi && Boolean(vendorKey) && !prefersTokenAuth;
130
274
  if (preferVendorKeyInDirectApiMode) {
131
275
  // Vendor key authentication (validated server-side)
@@ -169,6 +313,14 @@ export class APIClient {
169
313
  }
170
314
  return response;
171
315
  }, (error) => {
316
+ if (this.shouldRetryViaSupabaseMemoryFunctions(error)) {
317
+ const retryConfig = {
318
+ ...error.config,
319
+ __retriedViaSupabaseMemoryFunctions: true,
320
+ __useSupabaseMemoryFunctions: true
321
+ };
322
+ return this.client.request(retryConfig);
323
+ }
172
324
  if (this.shouldRetryViaApiGateway(error)) {
173
325
  const retryConfig = {
174
326
  ...error.config,
@@ -247,7 +399,7 @@ export class APIClient {
247
399
  }
248
400
  catch (error) {
249
401
  // Backward-compatible fallback: newer API contracts may reject GET list.
250
- if (error?.response?.status === 405) {
402
+ if (this.shouldUsePostListFallback(error)) {
251
403
  const limit = Number(params.limit || 20);
252
404
  const page = Number(params.page || 1);
253
405
  const offset = Number(params.offset ?? Math.max(0, (page - 1) * limit));
@@ -26,7 +26,7 @@ interface CLIConfigData {
26
26
  manualEndpointOverrides?: boolean;
27
27
  lastManualEndpointUpdate?: string;
28
28
  vendorKey?: string | undefined;
29
- authMethod?: 'jwt' | 'vendor_key' | 'oauth' | undefined;
29
+ authMethod?: 'jwt' | 'vendor_key' | 'oauth' | 'oauth2' | undefined;
30
30
  tokenExpiry?: number | undefined;
31
31
  lastValidated?: string | undefined;
32
32
  deviceId?: string;
@@ -84,10 +84,14 @@ export declare class CLIConfig {
84
84
  setManualEndpoints(endpoints: Partial<CLIConfigData['discoveredServices']>): Promise<void>;
85
85
  hasManualEndpointOverrides(): boolean;
86
86
  /**
87
- * Clears the in-memory auth cache and removes the `lastValidated` timestamp.
88
- * Called after a definitive 401 from the memory API so that the next
89
- * `isAuthenticated()` call performs a fresh server verification rather than
90
- * returning a stale cached result.
87
+ * Clears the in-memory auth cache so that the next `isAuthenticated()` call
88
+ * performs a fresh server verification rather than returning a stale cached result.
89
+ *
90
+ * NOTE: `lastValidated` is intentionally NOT deleted here. Each auth path
91
+ * (vendor_key, token) already correctly rejects 401 responses without relying
92
+ * on `lastValidated`. Deleting it would destroy the offline grace period
93
+ * (7-day for vendor keys, 24-hour for JWT tokens), causing auth failures
94
+ * on transient network errors even when credentials are valid.
91
95
  */
92
96
  invalidateAuthCache(): Promise<void>;
93
97
  clearManualEndpointOverrides(): Promise<void>;
@@ -111,6 +115,7 @@ export declare class CLIConfig {
111
115
  setApiUrl(url: string): Promise<void>;
112
116
  setToken(token: string): Promise<void>;
113
117
  getToken(): string | undefined;
118
+ getAuthMethod(): string | undefined;
114
119
  getCurrentUser(): Promise<UserProfile | undefined>;
115
120
  isAuthenticated(): Promise<boolean>;
116
121
  logout(): Promise<void>;
@@ -670,15 +670,17 @@ export class CLIConfig {
670
670
  return !!this.config.manualEndpointOverrides;
671
671
  }
672
672
  /**
673
- * Clears the in-memory auth cache and removes the `lastValidated` timestamp.
674
- * Called after a definitive 401 from the memory API so that the next
675
- * `isAuthenticated()` call performs a fresh server verification rather than
676
- * returning a stale cached result.
673
+ * Clears the in-memory auth cache so that the next `isAuthenticated()` call
674
+ * performs a fresh server verification rather than returning a stale cached result.
675
+ *
676
+ * NOTE: `lastValidated` is intentionally NOT deleted here. Each auth path
677
+ * (vendor_key, token) already correctly rejects 401 responses without relying
678
+ * on `lastValidated`. Deleting it would destroy the offline grace period
679
+ * (7-day for vendor keys, 24-hour for JWT tokens), causing auth failures
680
+ * on transient network errors even when credentials are valid.
677
681
  */
678
682
  async invalidateAuthCache() {
679
683
  this.authCheckCache = null;
680
- delete this.config.lastValidated;
681
- await this.save().catch(() => { });
682
684
  }
683
685
  async clearManualEndpointOverrides() {
684
686
  delete this.config.manualEndpointOverrides;
@@ -894,6 +896,9 @@ export class CLIConfig {
894
896
  getToken() {
895
897
  return this.config.token;
896
898
  }
899
+ getAuthMethod() {
900
+ return this.config.authMethod;
901
+ }
897
902
  async getCurrentUser() {
898
903
  return this.config.user;
899
904
  }
@@ -962,15 +967,20 @@ export class CLIConfig {
962
967
  const token = this.getToken();
963
968
  if (!token)
964
969
  return false;
965
- // OAuth tokens are often opaque (not JWT). Prefer local expiry metadata when present.
966
- if (this.config.authMethod === 'oauth') {
970
+ // OAuth tokens are often opaque (not JWT). Use local expiry metadata as a quick
971
+ // pre-check, but do not treat it as authoritative for a "true" result. We still
972
+ // run server verification on cache misses to avoid status/API drift.
973
+ let oauthTokenLocallyValid;
974
+ if (this.config.authMethod === 'oauth' || this.config.authMethod === 'oauth2') {
967
975
  const tokenExpiresAt = this.get('token_expires_at');
968
976
  if (typeof tokenExpiresAt === 'number') {
969
- const isValid = Date.now() < tokenExpiresAt;
970
- this.authCheckCache = { isValid, timestamp: Date.now() };
971
- return isValid;
977
+ oauthTokenLocallyValid = Date.now() < tokenExpiresAt;
978
+ if (!oauthTokenLocallyValid) {
979
+ this.authCheckCache = { isValid: false, timestamp: Date.now() };
980
+ return false;
981
+ }
972
982
  }
973
- // Fall through to legacy validation when we don't have expiry metadata.
983
+ // Fall through to server validation path.
974
984
  }
975
985
  // Check cache first
976
986
  if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
@@ -978,9 +988,11 @@ export class CLIConfig {
978
988
  }
979
989
  // Local expiry check first (fast)
980
990
  let locallyValid = false;
981
- // Handle simple CLI tokens (format: cli_xxx_timestamp)
982
- if (token.startsWith('cli_')) {
983
- // Extract timestamp from CLI token
991
+ if (typeof oauthTokenLocallyValid === 'boolean') {
992
+ locallyValid = oauthTokenLocallyValid;
993
+ }
994
+ else if (token.startsWith('cli_')) {
995
+ // Handle simple CLI tokens (format: cli_xxx_timestamp)
984
996
  const parts = token.split('_');
985
997
  if (parts.length >= 3) {
986
998
  const lastPart = parts[parts.length - 1];
@@ -1033,17 +1045,7 @@ export class CLIConfig {
1033
1045
  this.authCheckCache = { isValid: false, timestamp: Date.now() };
1034
1046
  return false;
1035
1047
  }
1036
- // Token is locally valid - check if we need server validation
1037
- // Skip server validation if we have a recent lastValidated timestamp (within 24 hours)
1038
- const lastValidated = this.config.lastValidated;
1039
- const skipServerValidation = lastValidated &&
1040
- (Date.now() - new Date(lastValidated).getTime()) < (24 * 60 * 60 * 1000); // 24 hours
1041
- if (skipServerValidation) {
1042
- // Trust the local validation if it was recently validated
1043
- this.authCheckCache = { isValid: locallyValid, timestamp: Date.now() };
1044
- return locallyValid;
1045
- }
1046
- // Verify with server (security check) for tokens that haven't been validated recently
1048
+ // Token is locally valid - verify with server on cache miss for consistency
1047
1049
  try {
1048
1050
  // Try auth-gateway first (port 4000), then fall back to Netlify function
1049
1051
  const endpoints = [
@@ -1059,16 +1061,25 @@ export class CLIConfig {
1059
1061
  if (response.data.valid === true) {
1060
1062
  break;
1061
1063
  }
1062
- // Server explicitly said invalid - this is an auth error, not network error
1064
+ // Explicit auth rejection should always invalidate local auth state.
1063
1065
  if (response.status === 401 || response.status === 403 || response.data.valid === false) {
1064
1066
  authError = true;
1065
1067
  }
1068
+ else {
1069
+ // Non-auth failures (like 404/5xx) should behave like transient verification failures.
1070
+ networkError = true;
1071
+ }
1066
1072
  }
1067
1073
  catch (error) {
1068
- // Check if this is a network error (no response) vs auth error (got response)
1074
+ // Check if this is a network/transient error vs explicit auth rejection.
1069
1075
  if (error.response) {
1070
- // Got a response, likely 401/403
1071
- authError = true;
1076
+ const status = error.response.status;
1077
+ if (status === 401 || status === 403) {
1078
+ authError = true;
1079
+ }
1080
+ else {
1081
+ networkError = true;
1082
+ }
1072
1083
  }
1073
1084
  else {
1074
1085
  // Network error (ECONNREFUSED, ETIMEDOUT, etc.)
@@ -1196,6 +1207,12 @@ export class CLIConfig {
1196
1207
  return;
1197
1208
  }
1198
1209
  try {
1210
+ // Vendor-key sessions should never attempt token refresh.
1211
+ // Some environments retain stale token/refresh_token fields from older logins,
1212
+ // which can otherwise trip dead refresh routes during normal memory commands.
1213
+ if (String(this.config.authMethod || '').toLowerCase() === 'vendor_key') {
1214
+ return;
1215
+ }
1199
1216
  // OAuth token refresh (opaque tokens + refresh_token + token_expires_at)
1200
1217
  if (this.config.authMethod === 'oauth') {
1201
1218
  const refreshToken = this.get('refresh_token');
@@ -1376,8 +1393,25 @@ export class CLIConfig {
1376
1393
  'wss://mcp.lanonasis.com/ws';
1377
1394
  }
1378
1395
  getMCPRestUrl() {
1379
- return this.config.discoveredServices?.mcp_base ||
1380
- 'https://mcp.lanonasis.com/api/v1';
1396
+ const configured = this.config.mcpServerUrl;
1397
+ if (typeof configured === 'string' && configured.trim().length > 0) {
1398
+ return configured.trim();
1399
+ }
1400
+ const discoveredMcpBase = this.config.discoveredServices?.mcp_base;
1401
+ if (typeof discoveredMcpBase === 'string' && discoveredMcpBase.trim().length > 0) {
1402
+ const normalizedMcpBase = discoveredMcpBase.trim().replace(/\/$/, '');
1403
+ const normalizedMemoryBase = (this.config.discoveredServices?.memory_base || '')
1404
+ .toString()
1405
+ .trim()
1406
+ .replace(/\/$/, '');
1407
+ // Guard against service-discovery payloads that map MCP REST to the memory API host.
1408
+ const pointsToMemoryBase = normalizedMemoryBase.length > 0 &&
1409
+ normalizedMcpBase.replace(/\/api\/v1$/, '') === normalizedMemoryBase;
1410
+ if (!pointsToMemoryBase) {
1411
+ return normalizedMcpBase;
1412
+ }
1413
+ }
1414
+ return 'https://mcp.lanonasis.com/api/v1';
1381
1415
  }
1382
1416
  getMCPSSEUrl() {
1383
1417
  return this.config.discoveredServices?.mcp_sse_base ||
@@ -112,6 +112,8 @@ export declare class MCPClient {
112
112
  * Calculate exponential backoff delay with jitter
113
113
  */
114
114
  private exponentialBackoff;
115
+ private resolveAuthCredential;
116
+ private buildAuthHeaders;
115
117
  /**
116
118
  * Validate authentication credentials before attempting MCP connection
117
119
  */
@@ -356,31 +356,69 @@ export class MCPClient {
356
356
  const jitter = cappedDelay * 0.25 * (Math.random() - 0.5);
357
357
  return Math.round(cappedDelay + jitter);
358
358
  }
359
+ async resolveAuthCredential() {
360
+ const authMethod = String(this.config.get('authMethod') || '').toLowerCase();
361
+ const token = this.config.get('token');
362
+ const vendorKey = await this.config.getVendorKeyAsync();
363
+ if (authMethod === 'vendor_key' && typeof vendorKey === 'string' && vendorKey.trim().length > 0) {
364
+ return { value: vendorKey.trim(), source: 'vendor_key' };
365
+ }
366
+ if ((authMethod === 'oauth' || authMethod === 'oauth2' || authMethod === 'jwt') &&
367
+ typeof token === 'string' &&
368
+ token.trim().length > 0) {
369
+ return { value: token.trim(), source: 'token' };
370
+ }
371
+ if (typeof token === 'string' && token.trim().length > 0) {
372
+ return { value: token.trim(), source: 'token' };
373
+ }
374
+ if (typeof vendorKey === 'string' && vendorKey.trim().length > 0) {
375
+ return { value: vendorKey.trim(), source: 'vendor_key' };
376
+ }
377
+ const envKey = process.env.LANONASIS_API_KEY;
378
+ if (typeof envKey === 'string' && envKey.trim().length > 0) {
379
+ return { value: envKey.trim(), source: 'env' };
380
+ }
381
+ return null;
382
+ }
383
+ buildAuthHeaders(auth) {
384
+ const headers = {};
385
+ const value = auth.value.trim();
386
+ if (!value) {
387
+ return headers;
388
+ }
389
+ if (auth.source === 'vendor_key' || value.startsWith('lano_') || value.startsWith('lms_')) {
390
+ headers['X-API-Key'] = value;
391
+ headers['X-Auth-Method'] = 'vendor_key';
392
+ headers['X-Project-Scope'] = 'lanonasis-maas';
393
+ return headers;
394
+ }
395
+ if (value.toLowerCase().startsWith('bearer ')) {
396
+ headers['Authorization'] = value;
397
+ headers['X-Auth-Method'] = 'jwt';
398
+ headers['X-Project-Scope'] = 'lanonasis-maas';
399
+ return headers;
400
+ }
401
+ headers['Authorization'] = `Bearer ${value}`;
402
+ headers['X-Auth-Method'] = 'jwt';
403
+ headers['X-Project-Scope'] = 'lanonasis-maas';
404
+ return headers;
405
+ }
359
406
  /**
360
407
  * Validate authentication credentials before attempting MCP connection
361
408
  */
362
409
  async validateAuthBeforeConnect() {
363
- const token = this.config.get('token');
364
- const vendorKey = await this.config.getVendorKeyAsync();
365
- // Check if we have any authentication credentials
366
- if (!token && !vendorKey) {
410
+ const auth = await this.resolveAuthCredential();
411
+ if (!auth) {
367
412
  throw new Error('AUTHENTICATION_REQUIRED: No authentication credentials found. Run "lanonasis auth login" first.');
368
413
  }
369
- // If we have a token, check if it's expired or needs refresh
370
- if (token) {
371
- try {
372
- await this.validateAndRefreshToken(token);
373
- }
374
- catch (error) {
375
- throw new Error(`AUTHENTICATION_INVALID: ${error instanceof Error ? error.message : 'Token validation failed'}`);
376
- }
377
- }
378
- // If we have a vendor key, ensure it is valid (non-empty)
379
- if (vendorKey && !token) {
380
- const validationResult = this.config.validateVendorKeyFormat(vendorKey);
381
- if (validationResult !== true) {
382
- throw new Error(`AUTHENTICATION_INVALID: ${typeof validationResult === 'string' ? validationResult : 'Vendor key is invalid'}`);
414
+ await this.config.refreshTokenIfNeeded();
415
+ const verification = await this.config.verifyCurrentCredentialsWithServer();
416
+ if (!verification.valid) {
417
+ const reason = verification.reason || 'Credential verification failed';
418
+ if (verification.method === 'none') {
419
+ throw new Error(`AUTHENTICATION_REQUIRED: ${reason}`);
383
420
  }
421
+ throw new Error(`AUTHENTICATION_INVALID: ${reason}`);
384
422
  }
385
423
  }
386
424
  /**
@@ -460,12 +498,10 @@ export class MCPClient {
460
498
  async initializeSSE(serverUrl) {
461
499
  // Use the proper SSE endpoint from config
462
500
  const sseUrl = this.config.getMCPSSEUrl() ?? `${serverUrl}/events`;
463
- const token = this.config.get('token');
464
- const vendorKey = await this.config.getVendorKeyAsync();
465
- const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
466
- if (authKey) {
501
+ const auth = await this.resolveAuthCredential();
502
+ if (auth) {
467
503
  // EventSource doesn't support headers directly, append token to URL
468
- this.sseConnection = new EventSource(`${sseUrl}?token=${encodeURIComponent(authKey)}`);
504
+ this.sseConnection = new EventSource(`${sseUrl}?token=${encodeURIComponent(auth.value)}`);
469
505
  this.sseConnection.onmessage = (event) => {
470
506
  try {
471
507
  const data = JSON.parse(event.data);
@@ -479,7 +515,11 @@ export class MCPClient {
479
515
  }
480
516
  };
481
517
  this.sseConnection.onerror = () => {
482
- console.error(chalk.yellow('⚠️ SSE connection error (will retry)'));
518
+ if (process.env.CLI_VERBOSE === 'true') {
519
+ console.error(chalk.yellow('⚠️ SSE connection error (stream disabled for this session)'));
520
+ }
521
+ this.sseConnection?.close();
522
+ this.sseConnection = null;
483
523
  };
484
524
  }
485
525
  }
@@ -487,12 +527,11 @@ export class MCPClient {
487
527
  * Initialize WebSocket connection for enterprise MCP server
488
528
  */
489
529
  async initializeWebSocket(wsUrl) {
490
- const token = this.config.get('token');
491
- const vendorKey = await this.config.getVendorKeyAsync();
492
- const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
493
- if (!authKey) {
530
+ const auth = await this.resolveAuthCredential();
531
+ if (!auth) {
494
532
  throw new Error('API key required for WebSocket mode. Set LANONASIS_API_KEY or login first.');
495
533
  }
534
+ const wsHeaders = this.buildAuthHeaders(auth);
496
535
  return new Promise((resolve, reject) => {
497
536
  try {
498
537
  // Close existing connection if any
@@ -502,10 +541,7 @@ export class MCPClient {
502
541
  }
503
542
  // Create new WebSocket connection with authentication
504
543
  this.wsConnection = new WebSocket(wsUrl, [], {
505
- headers: {
506
- 'Authorization': `Bearer ${authKey}`,
507
- 'X-API-Key': authKey
508
- }
544
+ headers: wsHeaders
509
545
  });
510
546
  this.wsConnection.on('open', () => {
511
547
  console.log(chalk.green('✅ Connected to MCP WebSocket server'));
@@ -653,19 +689,14 @@ export class MCPClient {
653
689
  */
654
690
  async checkRemoteHealth(serverUrl) {
655
691
  const apiUrl = serverUrl ?? this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
656
- const token = this.config.get('token');
657
- const vendorKey = await this.config.getVendorKeyAsync();
658
- const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
659
- if (!authKey) {
692
+ const auth = await this.resolveAuthCredential();
693
+ if (!auth) {
660
694
  throw new Error('No authentication token available');
661
695
  }
662
696
  try {
663
697
  const axios = (await import('axios')).default;
664
698
  await axios.get(`${apiUrl}/health`, {
665
- headers: {
666
- 'Authorization': `Bearer ${authKey}`,
667
- 'X-API-Key': authKey
668
- },
699
+ headers: this.buildAuthHeaders(auth),
669
700
  timeout: 5000
670
701
  });
671
702
  }
@@ -786,10 +817,8 @@ export class MCPClient {
786
817
  */
787
818
  async callRemoteTool(toolName, args) {
788
819
  const apiUrl = this.config.getMCPRestUrl() ?? 'https://mcp.lanonasis.com/api/v1';
789
- const token = this.config.get('token');
790
- const vendorKey = await this.config.getVendorKeyAsync();
791
- const authKey = token || vendorKey || process.env.LANONASIS_API_KEY;
792
- if (!authKey) {
820
+ const auth = await this.resolveAuthCredential();
821
+ if (!auth) {
793
822
  throw new Error('Authentication required. Run "lanonasis auth login" first.');
794
823
  }
795
824
  // Map MCP tool names to REST API endpoints
@@ -845,8 +874,7 @@ export class MCPClient {
845
874
  method: mapping.method,
846
875
  url: `${apiUrl}${endpoint}`,
847
876
  headers: {
848
- 'Authorization': `Bearer ${authKey}`,
849
- 'X-API-Key': authKey,
877
+ ...this.buildAuthHeaders(auth),
850
878
  'Content-Type': 'application/json'
851
879
  },
852
880
  data: mapping.transform ? mapping.transform(args) : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.8",
3
+ "version": "3.9.9",
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",
@@ -86,7 +86,7 @@
86
86
  "typescript": "^5.9.3"
87
87
  },
88
88
  "scripts": {
89
- "build": "rimraf dist && tsc -p tsconfig.json",
89
+ "build": "rimraf dist && tsc -p tsconfig.json && chmod +x dist/index.js dist/mcp-server-entry.js 2>/dev/null || true",
90
90
  "prepublishOnly": "npm run build",
91
91
  "postinstall": "node scripts/postinstall.js",
92
92
  "test": "[ -f node_modules/jest/bin/jest.js ] || bun install --no-save; node --experimental-vm-modules node_modules/jest/bin/jest.js",