@lanonasis/cli 3.9.8 → 3.9.10
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/dist/commands/api-keys.js +7 -7
- package/dist/commands/auth.js +2 -2
- package/dist/commands/config.js +1 -1
- package/dist/commands/guide.js +3 -3
- package/dist/commands/mcp.js +76 -9
- package/dist/commands/memory.js +45 -22
- package/dist/core/dashboard.js +4 -4
- package/dist/index.js +8 -1
- package/dist/mcp-server-entry.js +0 -0
- package/dist/utils/api.d.ts +12 -0
- package/dist/utils/api.js +164 -12
- package/dist/utils/config.d.ts +10 -5
- package/dist/utils/config.js +66 -32
- package/dist/utils/mcp-client.d.ts +2 -0
- package/dist/utils/mcp-client.js +74 -46
- package/package.json +2 -2
|
@@ -162,7 +162,7 @@ apiKeysCommand
|
|
|
162
162
|
validate: (input) => input.length > 0 || 'Value is required'
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
|
-
type: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
675
|
+
type: 'select',
|
|
676
676
|
name: 'environment',
|
|
677
677
|
message: 'Environment:',
|
|
678
678
|
when: !requestData.environment,
|
package/dist/commands/auth.js
CHANGED
|
@@ -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: '
|
|
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.
|
|
824
|
+
await config.setAndSave('authMethod', 'jwt');
|
|
825
825
|
spinner.succeed('Login successful');
|
|
826
826
|
console.log();
|
|
827
827
|
console.log(chalk.green('✓ Authenticated successfully'));
|
package/dist/commands/config.js
CHANGED
|
@@ -520,7 +520,7 @@ export function configCommands(program) {
|
|
|
520
520
|
// const mcpServerPath = config.get<string>('mcpServerPath');
|
|
521
521
|
const mcpServerUrl = config.get('mcpServerUrl');
|
|
522
522
|
if (mcpPreference) {
|
|
523
|
-
if (['local', 'remote', 'auto'].includes(mcpPreference)) {
|
|
523
|
+
if (['local', 'remote', 'websocket', 'auto'].includes(mcpPreference)) {
|
|
524
524
|
console.log(chalk.green(` ✓ MCP preference: ${mcpPreference}`));
|
|
525
525
|
validation.mcpConfigValid = true;
|
|
526
526
|
}
|
package/dist/commands/guide.js
CHANGED
|
@@ -188,7 +188,7 @@ export class UserGuidanceSystem {
|
|
|
188
188
|
default: 'https://api.lanonasis.com/api/v1'
|
|
189
189
|
},
|
|
190
190
|
{
|
|
191
|
-
type: '
|
|
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: '
|
|
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: '
|
|
338
|
+
type: 'select',
|
|
339
339
|
name: 'shell',
|
|
340
340
|
message: 'Which shell do you use?',
|
|
341
341
|
choices: [
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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')
|
package/dist/commands/memory.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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 (
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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: '
|
|
846
|
+
type: 'select',
|
|
824
847
|
name: 'type',
|
|
825
848
|
message: 'Memory type:',
|
|
826
849
|
choices: [...MEMORY_TYPE_CHOICES],
|
package/dist/core/dashboard.js
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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()) {
|
package/dist/mcp-server-entry.js
CHANGED
|
File without changes
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// -
|
|
98
|
-
// -
|
|
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 &&
|
|
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
|
-
//
|
|
251
|
+
// Explicit force: direct to api.lanonasis.com for troubleshooting.
|
|
106
252
|
apiBaseUrl = this.config.getApiUrl();
|
|
107
253
|
}
|
|
108
254
|
else if (useMcpServer) {
|
|
109
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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));
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -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
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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>;
|
package/dist/utils/config.js
CHANGED
|
@@ -670,15 +670,17 @@ export class CLIConfig {
|
|
|
670
670
|
return !!this.config.manualEndpointOverrides;
|
|
671
671
|
}
|
|
672
672
|
/**
|
|
673
|
-
* Clears the in-memory auth cache
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
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).
|
|
966
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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 -
|
|
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
|
-
//
|
|
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
|
|
1074
|
+
// Check if this is a network/transient error vs explicit auth rejection.
|
|
1069
1075
|
if (error.response) {
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
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
|
*/
|
package/dist/utils/mcp-client.js
CHANGED
|
@@ -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
|
|
364
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
464
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
491
|
-
|
|
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
|
|
657
|
-
|
|
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
|
|
790
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "3.9.10",
|
|
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",
|