@lanonasis/cli 3.9.5 → 3.9.7

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.
@@ -22,7 +22,7 @@ export const MemoryCreateSchema = z.object({
22
22
  .uuid()
23
23
  .optional()
24
24
  .describe("Optional topic ID for organization"),
25
- metadata: z.record(z.any())
25
+ metadata: z.record(z.string(), z.any())
26
26
  .optional()
27
27
  .describe("Additional metadata")
28
28
  });
@@ -71,7 +71,7 @@ export const MemoryUpdateSchema = z.object({
71
71
  tags: z.array(z.string())
72
72
  .optional()
73
73
  .describe("New tags (replaces existing)"),
74
- metadata: z.record(z.any())
74
+ metadata: z.record(z.string(), z.any())
75
75
  .optional()
76
76
  .describe("New metadata (merges with existing)")
77
77
  });
@@ -229,7 +229,7 @@ export const BulkOperationSchema = z.object({
229
229
  .describe("Bulk operation type"),
230
230
  entity_type: z.enum(["memory", "topic", "apikey"])
231
231
  .describe("Entity type for bulk operation"),
232
- items: z.array(z.record(z.any()))
232
+ items: z.array(z.record(z.string(), z.any()))
233
233
  .min(1)
234
234
  .max(100)
235
235
  .describe("Items for bulk operation"),
@@ -249,7 +249,7 @@ export const ImportExportSchema = z.object({
249
249
  file_path: z.string()
250
250
  .optional()
251
251
  .describe("File path for import/export"),
252
- filters: z.record(z.any())
252
+ filters: z.record(z.string(), z.any())
253
253
  .optional()
254
254
  .describe("Filters for export")
255
255
  });
@@ -257,7 +257,7 @@ export const ImportExportSchema = z.object({
257
257
  export const ToolExecutionSchema = z.object({
258
258
  tool_name: z.string()
259
259
  .describe("Name of the tool to execute"),
260
- arguments: z.record(z.any())
260
+ arguments: z.record(z.string(), z.any())
261
261
  .describe("Tool arguments"),
262
262
  timeout: z.number()
263
263
  .positive()
@@ -308,7 +308,7 @@ export class SchemaValidator {
308
308
  }
309
309
  catch (error) {
310
310
  if (error instanceof z.ZodError) {
311
- throw new Error(`Validation error: ${error.errors
311
+ throw new Error(`Validation error: ${error.issues
312
312
  .map(e => `${e.path.join('.')}: ${e.message}`)
313
313
  .join(', ')}`);
314
314
  }
@@ -326,7 +326,7 @@ export class SchemaValidator {
326
326
  else {
327
327
  return {
328
328
  success: false,
329
- errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`)
329
+ errors: result.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)
330
330
  };
331
331
  }
332
332
  }
@@ -149,10 +149,29 @@ export interface ApiErrorResponse {
149
149
  status_code: number;
150
150
  details?: Record<string, unknown>;
151
151
  }
152
+ export interface UserProfile {
153
+ id: string;
154
+ email: string;
155
+ name: string | null;
156
+ avatar_url: string | null;
157
+ role: string;
158
+ provider: string | null;
159
+ project_scope: string | null;
160
+ platform: string | null;
161
+ created_at: string | null;
162
+ last_sign_in_at: string | null;
163
+ metadata?: {
164
+ locale: string | null;
165
+ timezone: string | null;
166
+ };
167
+ }
152
168
  export declare class APIClient {
153
169
  private client;
154
170
  private config;
171
+ /** When true, throw on 401/403 instead of printing+exiting (for callers that handle errors) */
172
+ noExit: boolean;
155
173
  private normalizeMemoryEntry;
174
+ private shouldUseLegacyMemoryRpcFallback;
156
175
  constructor();
157
176
  login(email: string, password: string): Promise<AuthResponse>;
158
177
  register(email: string, password: string, organizationName?: string): Promise<AuthResponse>;
@@ -170,6 +189,12 @@ export declare class APIClient {
170
189
  updateTopic(id: string, data: UpdateTopicRequest): Promise<MemoryTopic>;
171
190
  deleteTopic(id: string): Promise<void>;
172
191
  getHealth(): Promise<HealthStatus>;
192
+ /**
193
+ * Fetch the current user's profile from the auth gateway (GET /v1/auth/me).
194
+ * Works for all auth methods: OAuth Bearer token, vendor key (X-API-Key), and JWT.
195
+ * The /auth/ prefix causes the request interceptor to route this to auth_base.
196
+ */
197
+ getUserProfile(): Promise<UserProfile>;
173
198
  get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
174
199
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
175
200
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
package/dist/utils/api.js CHANGED
@@ -5,6 +5,8 @@ import { CLIConfig } from './config.js';
5
5
  export class APIClient {
6
6
  client;
7
7
  config;
8
+ /** When true, throw on 401/403 instead of printing+exiting (for callers that handle errors) */
9
+ noExit = false;
8
10
  normalizeMemoryEntry(payload) {
9
11
  // API responses are inconsistent across gateways:
10
12
  // - Some return the memory entry directly
@@ -25,6 +27,21 @@ export class APIClient {
25
27
  }
26
28
  return payload;
27
29
  }
30
+ shouldUseLegacyMemoryRpcFallback(error) {
31
+ const status = error?.response?.status;
32
+ const errorData = error?.response?.data;
33
+ const message = `${errorData?.error || ''} ${errorData?.message || ''}`.toLowerCase();
34
+ if (status === 405) {
35
+ return true;
36
+ }
37
+ if (status === 400 && message.includes('memory id is required')) {
38
+ return true;
39
+ }
40
+ if (status === 400 && message.includes('method not allowed')) {
41
+ return true;
42
+ }
43
+ return false;
44
+ }
28
45
  constructor() {
29
46
  this.config = new CLIConfig();
30
47
  this.client = axios.create({
@@ -43,12 +60,14 @@ export class APIClient {
43
60
  const authMethod = this.config.get('authMethod');
44
61
  const vendorKey = await this.config.getVendorKeyAsync();
45
62
  const token = this.config.getToken();
63
+ const isMemoryEndpoint = typeof config.url === 'string' && config.url.startsWith('/api/v1/memories');
46
64
  const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true'
47
65
  || process.env.CLI_FORCE_API === 'true'
48
66
  || process.env.ONASIS_FORCE_API === 'true';
49
67
  const forceApiFromConfig = this.config.get('forceApi') === true
50
68
  || this.config.get('connectionTransport') === 'api';
51
- const forceDirectApi = forceApiFromEnv || forceApiFromConfig;
69
+ // Memory CRUD/search endpoints should always use the API gateway path.
70
+ const forceDirectApi = forceApiFromEnv || forceApiFromConfig || isMemoryEndpoint;
52
71
  const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
53
72
  const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
54
73
  // Determine the correct API base URL:
@@ -131,11 +150,21 @@ export class APIClient {
131
150
  if (error.response) {
132
151
  const { status, data } = error.response;
133
152
  if (status === 401) {
153
+ // Invalidate the local auth cache so the next isAuthenticated() call
154
+ // performs a fresh server check rather than returning a stale result.
155
+ this.config.invalidateAuthCache().catch(() => { });
156
+ if (this.noExit) {
157
+ // Caller handles the error (e.g. auth status probe) — throw so try/catch fires
158
+ return Promise.reject(error);
159
+ }
134
160
  console.error(chalk.red('✖ Authentication failed'));
135
161
  console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
136
162
  process.exit(1);
137
163
  }
138
164
  if (status === 403) {
165
+ if (this.noExit) {
166
+ return Promise.reject(error);
167
+ }
139
168
  console.error(chalk.red('✖ Permission denied'));
140
169
  if (data.message) {
141
170
  console.error(chalk.gray(data.message));
@@ -183,11 +212,91 @@ export class APIClient {
183
212
  return response.data;
184
213
  }
185
214
  catch (error) {
186
- // Backward-compatible fallback: newer API contracts may reject GET list and prefer search-only.
215
+ // Backward-compatible fallback: newer API contracts may reject GET list.
187
216
  if (error?.response?.status === 405) {
188
217
  const limit = Number(params.limit || 20);
189
218
  const page = Number(params.page || 1);
190
219
  const offset = Number(params.offset ?? Math.max(0, (page - 1) * limit));
220
+ // Preferred fallback: POST list endpoint (avoids triggering vector search for plain listings).
221
+ const listPayload = {
222
+ limit,
223
+ offset
224
+ };
225
+ if (params.memory_type) {
226
+ listPayload.memory_type = params.memory_type;
227
+ }
228
+ if (params.tags) {
229
+ listPayload.tags = Array.isArray(params.tags)
230
+ ? params.tags
231
+ : String(params.tags).split(',').map((tag) => tag.trim()).filter(Boolean);
232
+ }
233
+ if (params.topic_id) {
234
+ listPayload.topic_id = params.topic_id;
235
+ }
236
+ if (params.user_id) {
237
+ listPayload.user_id = params.user_id;
238
+ }
239
+ if (params.sort || params.sort_by) {
240
+ listPayload.sort_by = params.sort_by || params.sort;
241
+ }
242
+ if (params.order || params.sort_order) {
243
+ listPayload.sort_order = params.sort_order || params.order;
244
+ }
245
+ for (const endpoint of ['/api/v1/memories/list', '/api/v1/memory/list']) {
246
+ try {
247
+ const listResponse = await this.client.post(endpoint, listPayload);
248
+ const payload = listResponse.data || {};
249
+ const resultsArray = Array.isArray(payload.data)
250
+ ? payload.data
251
+ : Array.isArray(payload.memories)
252
+ ? payload.memories
253
+ : Array.isArray(payload.results)
254
+ ? payload.results
255
+ : [];
256
+ const memories = resultsArray.map((entry) => this.normalizeMemoryEntry(entry));
257
+ const pagination = (payload.pagination && typeof payload.pagination === 'object')
258
+ ? payload.pagination
259
+ : {};
260
+ const total = Number.isFinite(Number(pagination.total))
261
+ ? Number(pagination.total)
262
+ : Number.isFinite(Number(payload.total))
263
+ ? Number(payload.total)
264
+ : memories.length;
265
+ const pages = Number.isFinite(Number(pagination.total_pages))
266
+ ? Number(pagination.total_pages)
267
+ : Number.isFinite(Number(pagination.pages))
268
+ ? Number(pagination.pages)
269
+ : Math.max(1, Math.ceil(total / limit));
270
+ const currentPage = Number.isFinite(Number(pagination.page))
271
+ ? Number(pagination.page)
272
+ : Math.max(1, Math.floor(offset / limit) + 1);
273
+ const hasMore = typeof pagination.has_more === 'boolean'
274
+ ? pagination.has_more
275
+ : typeof pagination.has_next === 'boolean'
276
+ ? pagination.has_next
277
+ : (offset + memories.length) < total;
278
+ return {
279
+ ...payload,
280
+ data: memories,
281
+ memories,
282
+ pagination: {
283
+ total,
284
+ limit,
285
+ offset,
286
+ has_more: hasMore,
287
+ page: currentPage,
288
+ pages
289
+ }
290
+ };
291
+ }
292
+ catch (listError) {
293
+ if (listError?.response?.status === 404 || listError?.response?.status === 405) {
294
+ continue;
295
+ }
296
+ throw listError;
297
+ }
298
+ }
299
+ // Secondary fallback: search endpoint for legacy contracts that expose only search.
191
300
  const searchPayload = {
192
301
  query: '*',
193
302
  limit,
@@ -251,15 +360,51 @@ export class APIClient {
251
360
  }
252
361
  }
253
362
  async getMemory(id) {
254
- const response = await this.client.get(`/api/v1/memories/${id}`);
255
- return this.normalizeMemoryEntry(response.data);
363
+ try {
364
+ const response = await this.client.get(`/api/v1/memories/${id}`);
365
+ return this.normalizeMemoryEntry(response.data);
366
+ }
367
+ catch (error) {
368
+ if (this.shouldUseLegacyMemoryRpcFallback(error)) {
369
+ const fallback = await this.client.post('/api/v1/memory/get', { id });
370
+ const payload = fallback.data && typeof fallback.data === 'object'
371
+ ? fallback.data.data ?? fallback.data
372
+ : fallback.data;
373
+ return this.normalizeMemoryEntry(payload);
374
+ }
375
+ throw error;
376
+ }
256
377
  }
257
378
  async updateMemory(id, data) {
258
- const response = await this.client.put(`/api/v1/memories/${id}`, data);
259
- return this.normalizeMemoryEntry(response.data);
379
+ try {
380
+ const response = await this.client.put(`/api/v1/memories/${id}`, data);
381
+ return this.normalizeMemoryEntry(response.data);
382
+ }
383
+ catch (error) {
384
+ if (this.shouldUseLegacyMemoryRpcFallback(error)) {
385
+ const fallback = await this.client.post('/api/v1/memory/update', {
386
+ id,
387
+ ...data
388
+ });
389
+ const payload = fallback.data && typeof fallback.data === 'object'
390
+ ? fallback.data.data ?? fallback.data
391
+ : fallback.data;
392
+ return this.normalizeMemoryEntry(payload);
393
+ }
394
+ throw error;
395
+ }
260
396
  }
261
397
  async deleteMemory(id) {
262
- await this.client.delete(`/api/v1/memories/${id}`);
398
+ try {
399
+ await this.client.delete(`/api/v1/memories/${id}`);
400
+ }
401
+ catch (error) {
402
+ if (this.shouldUseLegacyMemoryRpcFallback(error)) {
403
+ await this.client.post('/api/v1/memory/delete', { id });
404
+ return;
405
+ }
406
+ throw error;
407
+ }
263
408
  }
264
409
  async searchMemories(query, options = {}) {
265
410
  const response = await this.client.post('/api/v1/memories/search', {
@@ -303,6 +448,15 @@ export class APIClient {
303
448
  const response = await this.client.get('/health');
304
449
  return response.data;
305
450
  }
451
+ /**
452
+ * Fetch the current user's profile from the auth gateway (GET /v1/auth/me).
453
+ * Works for all auth methods: OAuth Bearer token, vendor key (X-API-Key), and JWT.
454
+ * The /auth/ prefix causes the request interceptor to route this to auth_base.
455
+ */
456
+ async getUserProfile() {
457
+ const response = await this.client.get('/v1/auth/me');
458
+ return response.data;
459
+ }
306
460
  // Generic HTTP methods
307
461
  async get(url, config) {
308
462
  const response = await this.client.get(url, config);
@@ -34,6 +34,12 @@ interface CLIConfigData {
34
34
  lastAuthFailure?: string | undefined;
35
35
  [key: string]: unknown;
36
36
  }
37
+ export type RemoteAuthVerification = {
38
+ valid: boolean;
39
+ method: 'token' | 'vendor_key' | 'none';
40
+ endpoint?: string;
41
+ reason?: string;
42
+ };
37
43
  export declare class CLIConfig {
38
44
  private configDir;
39
45
  private configPath;
@@ -70,11 +76,25 @@ export declare class CLIConfig {
70
76
  private resolveFallbackEndpoints;
71
77
  private logFallbackUsage;
72
78
  private pingAuthHealth;
79
+ private getAuthVerificationEndpoints;
80
+ private extractAuthErrorMessage;
81
+ private verifyTokenWithAuthGateway;
82
+ private verifyVendorKeyWithAuthGateway;
83
+ verifyCurrentCredentialsWithServer(): Promise<RemoteAuthVerification>;
73
84
  setManualEndpoints(endpoints: Partial<CLIConfigData['discoveredServices']>): Promise<void>;
74
85
  hasManualEndpointOverrides(): boolean;
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.
91
+ */
92
+ invalidateAuthCache(): Promise<void>;
75
93
  clearManualEndpointOverrides(): Promise<void>;
76
94
  getDiscoveredApiUrl(): string;
77
- setVendorKey(vendorKey: string): Promise<void>;
95
+ setVendorKey(vendorKey: string, options?: {
96
+ skipServerValidation?: boolean;
97
+ }): Promise<void>;
78
98
  validateVendorKeyFormat(vendorKey: string): string | boolean;
79
99
  private validateVendorKeyWithServer;
80
100
  getVendorKey(): string | undefined;