@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.
@@ -39,7 +39,7 @@ export const MemorySearchSchema = z.object({
39
39
  threshold: z.number()
40
40
  .min(0)
41
41
  .max(1)
42
- .default(0.7)
42
+ .default(0.55)
43
43
  .describe("Similarity threshold (0-1)"),
44
44
  topic_id: z.string()
45
45
  .uuid()
@@ -134,7 +134,7 @@ export class LanonasisMCPServer {
134
134
  type: 'number',
135
135
  minimum: 0,
136
136
  maximum: 1,
137
- default: 0.7,
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.7]`
592
+ Threshold: [Similarity threshold 0-1, default 0.55]`
593
593
  }
594
594
  }
595
595
  ]
File without changes
@@ -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
- relevance_score: number;
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 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');
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
- // Memory CRUD/search endpoints should always use the API gateway path.
70
- const forceDirectApi = forceApiFromEnv || forceApiFromConfig || isMemoryEndpoint;
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
- // - JWT auth (no vendor key) -> mcp.lanonasis.com (supports JWT tokens)
76
- // - 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)
77
245
  let apiBaseUrl;
78
- const useMcpServer = !forceDirectApi && prefersTokenAuth && !useVendorKeyAuth;
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
- // Force direct REST API mode to bypass MCP routing for troubleshooting.
251
+ // Explicit force: direct to api.lanonasis.com for troubleshooting.
84
252
  apiBaseUrl = this.config.getApiUrl();
85
253
  }
86
254
  else if (useMcpServer) {
87
- // JWT/OAuth tokens work with mcp.lanonasis.com
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
- // Even in forced direct-API mode, prefer bearer token auth when available.
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?.response?.status === 405) {
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));
@@ -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
  */