@lanonasis/cli 3.9.7 → 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.
- package/CHANGELOG.md +18 -0
- package/dist/commands/api-keys.js +7 -7
- package/dist/commands/auth.js +2 -2
- package/dist/commands/completion.js +10 -0
- package/dist/commands/guide.js +3 -3
- package/dist/commands/mcp.js +78 -11
- package/dist/commands/memory.js +544 -11
- package/dist/core/dashboard.js +4 -4
- package/dist/index.js +8 -1
- package/dist/mcp/schemas/tool-schemas.js +1 -1
- package/dist/mcp/server/lanonasis-server.js +2 -2
- package/dist/mcp-server-entry.js +0 -0
- package/dist/utils/api.d.ts +22 -1
- package/dist/utils/api.js +198 -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 +3 -2
|
@@ -134,7 +134,7 @@ export class LanonasisMCPServer {
|
|
|
134
134
|
type: 'number',
|
|
135
135
|
minimum: 0,
|
|
136
136
|
maximum: 1,
|
|
137
|
-
default: 0.
|
|
137
|
+
default: 0.55,
|
|
138
138
|
description: 'Similarity threshold'
|
|
139
139
|
}
|
|
140
140
|
},
|
|
@@ -589,7 +589,7 @@ Tags: [Optional comma-separated tags]`
|
|
|
589
589
|
|
|
590
590
|
Query: ${args?.query || '[Enter search terms]'}
|
|
591
591
|
Limit: [Number of results, default 10]
|
|
592
|
-
Threshold: [Similarity threshold 0-1, default 0.
|
|
592
|
+
Threshold: [Similarity threshold 0-1, default 0.55]`
|
|
593
593
|
}
|
|
594
594
|
}
|
|
595
595
|
]
|
package/dist/mcp-server-entry.js
CHANGED
|
File without changes
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface LoginRequest {
|
|
|
22
22
|
password: string;
|
|
23
23
|
}
|
|
24
24
|
export type MemoryType = 'context' | 'project' | 'knowledge' | 'reference' | 'personal' | 'workflow';
|
|
25
|
+
export type WriteIntent = 'new' | 'continue' | 'auto';
|
|
25
26
|
export interface MemoryEntry {
|
|
26
27
|
id: string;
|
|
27
28
|
title: string;
|
|
@@ -29,6 +30,7 @@ export interface MemoryEntry {
|
|
|
29
30
|
memory_type: MemoryType;
|
|
30
31
|
tags: string[];
|
|
31
32
|
topic_id?: string | null;
|
|
33
|
+
topic_key?: string | null;
|
|
32
34
|
user_id: string;
|
|
33
35
|
organization_id: string;
|
|
34
36
|
metadata?: Record<string, unknown>;
|
|
@@ -43,7 +45,11 @@ export interface CreateMemoryRequest {
|
|
|
43
45
|
memory_type?: MemoryType;
|
|
44
46
|
tags?: string[];
|
|
45
47
|
topic_id?: string;
|
|
48
|
+
topic_key?: string;
|
|
46
49
|
metadata?: Record<string, unknown>;
|
|
50
|
+
continuity_key?: string;
|
|
51
|
+
idempotency_key?: string;
|
|
52
|
+
write_intent?: WriteIntent;
|
|
47
53
|
}
|
|
48
54
|
export interface UpdateMemoryRequest {
|
|
49
55
|
title?: string;
|
|
@@ -51,7 +57,11 @@ export interface UpdateMemoryRequest {
|
|
|
51
57
|
memory_type?: MemoryType;
|
|
52
58
|
tags?: string[];
|
|
53
59
|
topic_id?: string | null;
|
|
60
|
+
topic_key?: string;
|
|
54
61
|
metadata?: Record<string, unknown>;
|
|
62
|
+
continuity_key?: string;
|
|
63
|
+
idempotency_key?: string;
|
|
64
|
+
write_intent?: WriteIntent;
|
|
55
65
|
}
|
|
56
66
|
export interface GetMemoriesParams {
|
|
57
67
|
page?: number;
|
|
@@ -60,6 +70,8 @@ export interface GetMemoriesParams {
|
|
|
60
70
|
memory_type?: MemoryType;
|
|
61
71
|
tags?: string[] | string;
|
|
62
72
|
topic_id?: string;
|
|
73
|
+
topic_key?: string;
|
|
74
|
+
include_deleted?: boolean;
|
|
63
75
|
user_id?: string;
|
|
64
76
|
sort?: 'created_at' | 'updated_at' | 'last_accessed' | 'access_count' | 'title';
|
|
65
77
|
order?: 'asc' | 'desc';
|
|
@@ -71,11 +83,14 @@ export interface SearchMemoryRequest {
|
|
|
71
83
|
memory_types?: MemoryType[];
|
|
72
84
|
tags?: string[];
|
|
73
85
|
topic_id?: string;
|
|
86
|
+
topic_key?: string;
|
|
74
87
|
limit?: number;
|
|
75
88
|
threshold?: number;
|
|
89
|
+
include_deleted?: boolean;
|
|
90
|
+
response_mode?: 'full' | 'compact' | 'timeline';
|
|
76
91
|
}
|
|
77
92
|
export interface MemorySearchResult extends MemoryEntry {
|
|
78
|
-
|
|
93
|
+
similarity_score: number;
|
|
79
94
|
}
|
|
80
95
|
export interface MemoryStats {
|
|
81
96
|
total_memories: number;
|
|
@@ -172,6 +187,12 @@ export declare class APIClient {
|
|
|
172
187
|
noExit: boolean;
|
|
173
188
|
private normalizeMemoryEntry;
|
|
174
189
|
private shouldUseLegacyMemoryRpcFallback;
|
|
190
|
+
private shouldRetryViaApiGateway;
|
|
191
|
+
private shouldRetryViaSupabaseMemoryFunctions;
|
|
192
|
+
private shouldUsePostListFallback;
|
|
193
|
+
private getSupabaseFunctionsBaseUrl;
|
|
194
|
+
private mapMemoryApiRouteToSupabaseFunctions;
|
|
195
|
+
private normalizeMcpPathToApi;
|
|
175
196
|
constructor();
|
|
176
197
|
login(email: string, password: string): Promise<AuthResponse>;
|
|
177
198
|
register(email: string, password: string, organizationName?: string): Promise<AuthResponse>;
|
package/dist/utils/api.js
CHANGED
|
@@ -42,6 +42,155 @@ export class APIClient {
|
|
|
42
42
|
}
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
|
+
shouldRetryViaApiGateway(error) {
|
|
46
|
+
const baseURL = String(error?.config?.baseURL || '');
|
|
47
|
+
const code = String(error?.code || '');
|
|
48
|
+
const alreadyRetried = Boolean(error?.config?.__retriedViaApiGateway);
|
|
49
|
+
if (alreadyRetried)
|
|
50
|
+
return false;
|
|
51
|
+
if (!baseURL.includes('mcp.lanonasis.com'))
|
|
52
|
+
return false;
|
|
53
|
+
return code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED';
|
|
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
|
+
}
|
|
184
|
+
normalizeMcpPathToApi(url) {
|
|
185
|
+
// MCP HTTP compatibility path -> API gateway REST paths
|
|
186
|
+
if (url === '/memory') {
|
|
187
|
+
return '/api/v1/memories';
|
|
188
|
+
}
|
|
189
|
+
if (url.startsWith('/memory/')) {
|
|
190
|
+
return url.replace('/memory/', '/api/v1/memories/');
|
|
191
|
+
}
|
|
192
|
+
return url;
|
|
193
|
+
}
|
|
45
194
|
constructor() {
|
|
46
195
|
this.config = new CLIConfig();
|
|
47
196
|
this.client = axios.create({
|
|
@@ -60,31 +209,50 @@ export class APIClient {
|
|
|
60
209
|
const authMethod = this.config.get('authMethod');
|
|
61
210
|
const vendorKey = await this.config.getVendorKeyAsync();
|
|
62
211
|
const token = this.config.getToken();
|
|
63
|
-
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');
|
|
64
217
|
const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true'
|
|
65
218
|
|| process.env.CLI_FORCE_API === 'true'
|
|
66
219
|
|| process.env.ONASIS_FORCE_API === 'true';
|
|
67
220
|
const forceApiFromConfig = this.config.get('forceApi') === true
|
|
68
221
|
|| this.config.get('connectionTransport') === 'api';
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
233
|
+
const forceDirectApiRetry = config
|
|
234
|
+
.__forceDirectApiGatewayRetry === true;
|
|
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;
|
|
71
239
|
const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
|
|
72
240
|
const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
|
|
73
241
|
// Determine the correct API base URL:
|
|
74
242
|
// - Auth endpoints -> auth.lanonasis.com
|
|
75
|
-
// -
|
|
76
|
-
// -
|
|
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)
|
|
77
245
|
let apiBaseUrl;
|
|
78
|
-
const useMcpServer = !forceDirectApi &&
|
|
246
|
+
const useMcpServer = !forceDirectApi && !isAuthEndpoint && (prefersTokenAuth || useVendorKeyAuth || isMemoryEndpoint);
|
|
79
247
|
if (isAuthEndpoint) {
|
|
80
248
|
apiBaseUrl = discoveredServices?.auth_base || 'https://auth.lanonasis.com';
|
|
81
249
|
}
|
|
82
250
|
else if (forceDirectApi) {
|
|
83
|
-
//
|
|
251
|
+
// Explicit force: direct to api.lanonasis.com for troubleshooting.
|
|
84
252
|
apiBaseUrl = this.config.getApiUrl();
|
|
85
253
|
}
|
|
86
254
|
else if (useMcpServer) {
|
|
87
|
-
//
|
|
255
|
+
// Memory service lives at mcp.lanonasis.com — accepts JWT, OAuth, and vendor keys.
|
|
88
256
|
apiBaseUrl = 'https://mcp.lanonasis.com/api/v1';
|
|
89
257
|
}
|
|
90
258
|
else {
|
|
@@ -101,9 +269,7 @@ export class APIClient {
|
|
|
101
269
|
config.headers['X-Project-Scope'] = 'lanonasis-maas';
|
|
102
270
|
}
|
|
103
271
|
// Enhanced Authentication Support
|
|
104
|
-
//
|
|
105
|
-
// This avoids accidentally sending an OAuth access token as X-API-Key (we store it
|
|
106
|
-
// in secure storage for MCP/WebSocket usage), which can cause 401s.
|
|
272
|
+
// In forced direct-API mode, prefer bearer token auth when available.
|
|
107
273
|
const preferVendorKeyInDirectApiMode = forceDirectApi && Boolean(vendorKey) && !prefersTokenAuth;
|
|
108
274
|
if (preferVendorKeyInDirectApiMode) {
|
|
109
275
|
// Vendor key authentication (validated server-side)
|
|
@@ -147,6 +313,26 @@ export class APIClient {
|
|
|
147
313
|
}
|
|
148
314
|
return response;
|
|
149
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
|
+
}
|
|
324
|
+
if (this.shouldRetryViaApiGateway(error)) {
|
|
325
|
+
const retryConfig = {
|
|
326
|
+
...error.config,
|
|
327
|
+
__retriedViaApiGateway: true,
|
|
328
|
+
__forceDirectApiGatewayRetry: true
|
|
329
|
+
};
|
|
330
|
+
retryConfig.baseURL = this.config.getApiUrl();
|
|
331
|
+
if (typeof retryConfig.url === 'string') {
|
|
332
|
+
retryConfig.url = this.normalizeMcpPathToApi(retryConfig.url);
|
|
333
|
+
}
|
|
334
|
+
return this.client.request(retryConfig);
|
|
335
|
+
}
|
|
150
336
|
if (error.response) {
|
|
151
337
|
const { status, data } = error.response;
|
|
152
338
|
if (status === 401) {
|
|
@@ -213,7 +399,7 @@ export class APIClient {
|
|
|
213
399
|
}
|
|
214
400
|
catch (error) {
|
|
215
401
|
// Backward-compatible fallback: newer API contracts may reject GET list.
|
|
216
|
-
if (error
|
|
402
|
+
if (this.shouldUsePostListFallback(error)) {
|
|
217
403
|
const limit = Number(params.limit || 20);
|
|
218
404
|
const page = Number(params.page || 1);
|
|
219
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
|
*/
|