@lanonasis/cli 3.9.10 → 3.9.11

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog - @lanonasis/cli
2
2
 
3
+ ## [3.9.11] - 2026-03-27
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - **`memory stats` no longer crashes on wrapped responses**: The CLI now normalizes both flat and `{ data: ... }` stats payloads and safely defaults optional fields like `total_size_bytes` and `avg_access_count` when the backend omits them.
8
+ - **Secure storage no longer initializes on every command startup**: Vendor key storage is now lazy-loaded, which prevents unnecessary keychain fallback warnings on CLI commands that do not access vendor key storage.
9
+ - **Bundled auth client updated to `@lanonasis/oauth-client@2.0.4`**: Pulls in the ESM-safe native keychain loader fix and semver-compliant optional React peer metadata.
10
+
3
11
  ## [3.9.8] - 2026-02-25
4
12
 
5
13
  ### ✨ New Features
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # @lanonasis/cli v3.9.7 - OAuth PKCE Auth & whoami
1
+ # @lanonasis/cli v3.9.11 - Stable Stats & Cleaner Startup
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/@lanonasis/cli)](https://www.npmjs.com/package/@lanonasis/cli)
4
4
  [![Downloads](https://img.shields.io/npm/dt/@lanonasis/cli)](https://www.npmjs.com/package/@lanonasis/cli)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Golden Contract](https://img.shields.io/badge/Onasis--Core-v0.1%20Compliant-gold)](https://api.lanonasis.com/.well-known/onasis.json)
7
7
 
8
- 🎉 **NEW IN v3.9.7**: Full OAuth PKCE session support across CLI and API gateway. New `onasis whoami` command. `auth status` now shows live user profile and probes real memory API access. Seven auth verification fixes eliminate false-positive "Authenticated: Yes" reports.
8
+ 🎉 **NEW IN v3.9.11**: `onasis memory stats` now handles wrapped/partial backend responses without crashing, vendor key secure storage is lazy-loaded so unrelated commands no longer trigger noisy fallback warnings, and the bundled `@lanonasis/oauth-client` is updated to `2.0.4` for the ESM-safe keychain loader fix.
9
9
 
10
10
  ## 🚀 Quick Start
11
11
 
@@ -712,12 +712,8 @@ async function handleOAuthFlow(config) {
712
712
  await config.set('refresh_token', tokens.refresh_token);
713
713
  await config.set('token_expires_at', Date.now() + (tokens.expires_in * 1000));
714
714
  await config.set('authMethod', 'oauth');
715
- // The OAuth access token from auth-gateway works as the API token for all services
716
- // Store it as the vendor key equivalent for MCP and API access
717
715
  spinner.text = 'Configuring unified access...';
718
716
  spinner.start();
719
- // Use the OAuth access token directly - it's already an auth-gateway token
720
- await config.setVendorKey(tokens.access_token);
721
717
  spinner.succeed('Unified authentication configured');
722
718
  console.log();
723
719
  console.log(chalk.green('✓ OAuth2 authentication successful'));
@@ -235,8 +235,16 @@ const createIntelligenceTransport = async () => {
235
235
  }),
236
236
  };
237
237
  }
238
- // Legacy key path: use CLI API client auth middleware directly.
239
- return { mode: 'api' };
238
+ // Pre-hashed key (64 hex chars from oauth-client normalizeApiKey): inject via header.
239
+ // The edge functions accept pre-hashed keys after the auth.ts shared utility update.
240
+ return {
241
+ mode: 'sdk',
242
+ client: new MemoryIntelligenceClient({
243
+ apiUrl,
244
+ allowMissingAuth: true,
245
+ headers: { 'X-API-Key': apiKey },
246
+ }),
247
+ };
240
248
  }
241
249
  throw new Error('Authentication required. Run "lanonasis auth login" first.');
242
250
  };
@@ -185,7 +185,10 @@ export declare class APIClient {
185
185
  private config;
186
186
  /** When true, throw on 401/403 instead of printing+exiting (for callers that handle errors) */
187
187
  noExit: boolean;
188
+ private isLikelyHashedCredential;
188
189
  private normalizeMemoryEntry;
190
+ private tryNormalizeMemoryEntry;
191
+ private normalizeMemoryStats;
189
192
  private shouldUseLegacyMemoryRpcFallback;
190
193
  private shouldRetryViaApiGateway;
191
194
  private shouldRetryViaSupabaseMemoryFunctions;
package/dist/utils/api.js CHANGED
@@ -2,11 +2,22 @@ import axios from 'axios';
2
2
  import chalk from 'chalk';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { CLIConfig } from './config.js';
5
+ const MEMORY_TYPES = [
6
+ 'context',
7
+ 'project',
8
+ 'knowledge',
9
+ 'reference',
10
+ 'personal',
11
+ 'workflow'
12
+ ];
5
13
  export class APIClient {
6
14
  client;
7
15
  config;
8
16
  /** When true, throw on 401/403 instead of printing+exiting (for callers that handle errors) */
9
17
  noExit = false;
18
+ isLikelyHashedCredential(value) {
19
+ return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
20
+ }
10
21
  normalizeMemoryEntry(payload) {
11
22
  // API responses are inconsistent across gateways:
12
23
  // - Some return the memory entry directly
@@ -27,13 +38,77 @@ export class APIClient {
27
38
  }
28
39
  return payload;
29
40
  }
41
+ tryNormalizeMemoryEntry(payload) {
42
+ if (!payload || typeof payload !== 'object') {
43
+ return undefined;
44
+ }
45
+ const normalized = this.normalizeMemoryEntry(payload);
46
+ if (!normalized || typeof normalized !== 'object') {
47
+ return undefined;
48
+ }
49
+ const normalizedRecord = normalized;
50
+ return typeof normalizedRecord.id === 'string' ? normalized : undefined;
51
+ }
52
+ normalizeMemoryStats(payload) {
53
+ if (!payload || typeof payload !== 'object') {
54
+ throw new Error('Memory stats endpoint returned an invalid response.');
55
+ }
56
+ const envelope = payload;
57
+ const rawStats = envelope.data && typeof envelope.data === 'object' && !Array.isArray(envelope.data)
58
+ ? envelope.data
59
+ : envelope;
60
+ const rawByType = rawStats.memories_by_type && typeof rawStats.memories_by_type === 'object' && !Array.isArray(rawStats.memories_by_type)
61
+ ? rawStats.memories_by_type
62
+ : rawStats.by_type && typeof rawStats.by_type === 'object' && !Array.isArray(rawStats.by_type)
63
+ ? rawStats.by_type
64
+ : {};
65
+ const memoriesByType = MEMORY_TYPES.reduce((accumulator, memoryType) => {
66
+ const value = rawByType[memoryType];
67
+ accumulator[memoryType] = typeof value === 'number' ? value : 0;
68
+ return accumulator;
69
+ }, {});
70
+ const recentMemories = Array.isArray(rawStats.recent_memories)
71
+ ? rawStats.recent_memories
72
+ .map((entry) => this.tryNormalizeMemoryEntry(entry))
73
+ .filter((entry) => entry !== undefined)
74
+ : [];
75
+ const totalMemories = typeof rawStats.total_memories === 'number'
76
+ ? rawStats.total_memories
77
+ : Object.values(memoriesByType).reduce((sum, count) => sum + count, 0);
78
+ return {
79
+ total_memories: totalMemories,
80
+ memories_by_type: memoriesByType,
81
+ total_size_bytes: typeof rawStats.total_size_bytes === 'number' ? rawStats.total_size_bytes : 0,
82
+ avg_access_count: typeof rawStats.avg_access_count === 'number' ? rawStats.avg_access_count : 0,
83
+ most_accessed_memory: this.tryNormalizeMemoryEntry(rawStats.most_accessed_memory),
84
+ recent_memories: recentMemories
85
+ };
86
+ }
30
87
  shouldUseLegacyMemoryRpcFallback(error) {
31
88
  const status = error?.response?.status;
32
89
  const errorData = error?.response?.data;
33
90
  const message = `${errorData?.error || ''} ${errorData?.message || ''}`.toLowerCase();
91
+ const rawRequestUrl = String(error?.config?.url || '');
92
+ const requestPath = (() => {
93
+ try {
94
+ if (/^https?:\/\//i.test(rawRequestUrl)) {
95
+ return new URL(rawRequestUrl).pathname;
96
+ }
97
+ }
98
+ catch {
99
+ // Fall back to the raw URL if URL parsing fails.
100
+ }
101
+ return rawRequestUrl;
102
+ })();
103
+ const normalizedRequestUrl = requestPath.startsWith('/memory')
104
+ ? this.normalizeMcpPathToApi(requestPath)
105
+ : requestPath;
34
106
  if (status === 405) {
35
107
  return true;
36
108
  }
109
+ if (status === 404 && /^\/api\/v1\/memories\/[^/?#]+$/.test(normalizedRequestUrl)) {
110
+ return true;
111
+ }
37
112
  if (status === 400 && message.includes('memory id is required')) {
38
113
  return true;
39
114
  }
@@ -238,6 +313,9 @@ export class APIClient {
238
313
  const forceDirectApi = forceApiFromEnv || forceApiFromConfig || forceDirectApiRetry;
239
314
  const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
240
315
  const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
316
+ if (authMethod === 'vendor_key' && this.isLikelyHashedCredential(vendorKey)) {
317
+ throw new Error('Stored vendor key is in legacy hashed format. Run "lanonasis auth login --vendor-key <your-key>" to refresh secure storage.');
318
+ }
241
319
  // Determine the correct API base URL:
242
320
  // - Auth endpoints -> auth.lanonasis.com
243
321
  // - Memory/MCP operations (JWT or vendor key) -> mcp.lanonasis.com (the memory service)
@@ -567,7 +645,7 @@ export class APIClient {
567
645
  return this.normalizeMemoryEntry(response.data);
568
646
  }
569
647
  catch (error) {
570
- if (this.shouldUseLegacyMemoryRpcFallback(error)) {
648
+ if (this.shouldUseLegacyMemoryRpcFallback(error) || error?.response?.status === 404) {
571
649
  const fallback = await this.client.post('/api/v1/memory/update', {
572
650
  id,
573
651
  ...data
@@ -585,7 +663,7 @@ export class APIClient {
585
663
  await this.client.delete(`/api/v1/memories/${id}`);
586
664
  }
587
665
  catch (error) {
588
- if (this.shouldUseLegacyMemoryRpcFallback(error)) {
666
+ if (this.shouldUseLegacyMemoryRpcFallback(error) || error?.response?.status === 404) {
589
667
  await this.client.post('/api/v1/memory/delete', { id });
590
668
  return;
591
669
  }
@@ -601,7 +679,7 @@ export class APIClient {
601
679
  }
602
680
  async getMemoryStats() {
603
681
  const response = await this.client.get('/api/v1/memories/stats');
604
- return response.data;
682
+ return this.normalizeMemoryStats(response.data);
605
683
  }
606
684
  async bulkDeleteMemories(memoryIds) {
607
685
  const response = await this.client.post('/api/v1/memories/bulk/delete', {
@@ -48,9 +48,12 @@ export declare class CLIConfig {
48
48
  private static readonly CONFIG_VERSION;
49
49
  private authCheckCache;
50
50
  private readonly AUTH_CACHE_TTL;
51
- private apiKeyStorage;
51
+ private apiKeyStorage?;
52
52
  private vendorKeyCache?;
53
+ private isLegacyHashedCredential;
54
+ private getLegacyHashedVendorKeyReason;
53
55
  constructor();
56
+ private getApiKeyStorage;
54
57
  /**
55
58
  * Overrides the configuration storage directory. Primarily used for tests.
56
59
  */
@@ -15,12 +15,22 @@ export class CLIConfig {
15
15
  AUTH_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
16
16
  apiKeyStorage;
17
17
  vendorKeyCache;
18
+ isLegacyHashedCredential(value) {
19
+ return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
20
+ }
21
+ getLegacyHashedVendorKeyReason() {
22
+ return 'Stored vendor key is in legacy hashed format. Run "lanonasis auth login --vendor-key <your-key>" to refresh secure storage.';
23
+ }
18
24
  constructor() {
19
25
  this.configDir = path.join(os.homedir(), '.maas');
20
26
  this.configPath = path.join(this.configDir, 'config.json');
21
27
  this.lockFile = path.join(this.configDir, 'config.lock');
22
- // Initialize secure storage for vendor keys using oauth-client's ApiKeyStorage
23
- this.apiKeyStorage = new ApiKeyStorage();
28
+ }
29
+ getApiKeyStorage() {
30
+ if (!this.apiKeyStorage) {
31
+ this.apiKeyStorage = new ApiKeyStorage();
32
+ }
33
+ return this.apiKeyStorage;
24
34
  }
25
35
  /**
26
36
  * Overrides the configuration storage directory. Primarily used for tests.
@@ -627,6 +637,13 @@ export class CLIConfig {
627
637
  await this.discoverServices();
628
638
  const token = this.getToken();
629
639
  const vendorKey = await this.getVendorKeyAsync();
640
+ if (this.config.authMethod === 'vendor_key' && this.isLegacyHashedCredential(vendorKey)) {
641
+ return {
642
+ valid: false,
643
+ method: 'vendor_key',
644
+ reason: this.getLegacyHashedVendorKeyReason()
645
+ };
646
+ }
630
647
  if (this.config.authMethod === 'vendor_key' && vendorKey) {
631
648
  return this.verifyVendorKeyWithAuthGateway(vendorKey);
632
649
  }
@@ -710,8 +727,9 @@ export class CLIConfig {
710
727
  }
711
728
  // Initialize and store using ApiKeyStorage from @lanonasis/oauth-client
712
729
  // This handles encryption automatically (AES-256-GCM with machine-derived key)
713
- await this.apiKeyStorage.initialize();
714
- await this.apiKeyStorage.store({
730
+ const apiKeyStorage = this.getApiKeyStorage();
731
+ await apiKeyStorage.initialize();
732
+ await apiKeyStorage.store({
715
733
  apiKey: trimmedKey,
716
734
  organizationId: this.config.user?.organization_id,
717
735
  userId: this.config.user?.email,
@@ -835,8 +853,9 @@ export class CLIConfig {
835
853
  */
836
854
  async getVendorKeyAsync() {
837
855
  try {
838
- await this.apiKeyStorage.initialize();
839
- const stored = await this.apiKeyStorage.retrieve();
856
+ const apiKeyStorage = this.getApiKeyStorage();
857
+ await apiKeyStorage.initialize();
858
+ const stored = await apiKeyStorage.retrieve();
840
859
  if (stored) {
841
860
  this.vendorKeyCache = stored.apiKey;
842
861
  return this.vendorKeyCache;
@@ -912,6 +931,13 @@ export class CLIConfig {
912
931
  const vendorKey = await this.getVendorKeyAsync();
913
932
  if (!vendorKey)
914
933
  return false;
934
+ if (this.isLegacyHashedCredential(vendorKey)) {
935
+ if (process.env.CLI_VERBOSE === 'true') {
936
+ console.warn(`⚠️ ${this.getLegacyHashedVendorKeyReason()}`);
937
+ }
938
+ this.authCheckCache = { isValid: false, timestamp: Date.now() };
939
+ return false;
940
+ }
915
941
  // Check in-memory cache first (5-minute TTL)
916
942
  if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
917
943
  return this.authCheckCache.isValid;
@@ -1146,10 +1172,13 @@ export class CLIConfig {
1146
1172
  this.vendorKeyCache = undefined;
1147
1173
  this.config.vendorKey = undefined;
1148
1174
  this.config.authMethod = undefined;
1175
+ this.config.refresh_token = undefined;
1176
+ this.config.token_expires_at = undefined;
1149
1177
  try {
1150
- await this.apiKeyStorage.initialize();
1178
+ const apiKeyStorage = this.getApiKeyStorage();
1179
+ await apiKeyStorage.initialize();
1151
1180
  // ApiKeyStorage may implement clear() to remove encrypted secrets
1152
- const storage = this.apiKeyStorage;
1181
+ const storage = apiKeyStorage;
1153
1182
  if (typeof storage.clear === 'function') {
1154
1183
  await storage.clear();
1155
1184
  }
@@ -1270,14 +1299,6 @@ export class CLIConfig {
1270
1299
  if (typeof expiresIn === 'number' && Number.isFinite(expiresIn)) {
1271
1300
  this.config.token_expires_at = Date.now() + (expiresIn * 1000);
1272
1301
  }
1273
- // Keep the encrypted "vendor key" in sync for MCP/WebSocket clients that use X-API-Key.
1274
- // This does not change authMethod away from oauth (setVendorKey guards against that).
1275
- try {
1276
- await this.setVendorKey(accessToken);
1277
- }
1278
- catch {
1279
- // Non-fatal: bearer token refresh still helps API calls.
1280
- }
1281
1302
  await this.save().catch(() => { });
1282
1303
  return;
1283
1304
  }
@@ -1327,9 +1348,23 @@ export class CLIConfig {
1327
1348
  this.config.user = undefined;
1328
1349
  this.config.authMethod = undefined;
1329
1350
  this.config.tokenExpiry = undefined;
1351
+ this.config.refresh_token = undefined;
1352
+ this.config.token_expires_at = undefined;
1330
1353
  this.config.lastValidated = undefined;
1331
1354
  this.config.authFailureCount = 0;
1332
1355
  this.config.lastAuthFailure = undefined;
1356
+ this.vendorKeyCache = undefined;
1357
+ try {
1358
+ const apiKeyStorage = this.getApiKeyStorage();
1359
+ await apiKeyStorage.initialize();
1360
+ const storage = apiKeyStorage;
1361
+ if (typeof storage.clear === 'function') {
1362
+ await storage.clear();
1363
+ }
1364
+ }
1365
+ catch {
1366
+ // Ignore secure storage cleanup failures while invalidating credentials
1367
+ }
1333
1368
  await this.save();
1334
1369
  }
1335
1370
  async incrementFailureCount() {
@@ -360,6 +360,10 @@ export class MCPClient {
360
360
  const authMethod = String(this.config.get('authMethod') || '').toLowerCase();
361
361
  const token = this.config.get('token');
362
362
  const vendorKey = await this.config.getVendorKeyAsync();
363
+ const isLikelyHashedCredential = typeof vendorKey === 'string' && /^[a-f0-9]{64}$/i.test(vendorKey.trim());
364
+ if (authMethod === 'vendor_key' && isLikelyHashedCredential) {
365
+ throw new Error('AUTHENTICATION_INVALID: Stored vendor key is in legacy hashed format. Run "lanonasis auth login --vendor-key <your-key>" to refresh secure storage.');
366
+ }
363
367
  if (authMethod === 'vendor_key' && typeof vendorKey === 'string' && vendorKey.trim().length > 0) {
364
368
  return { value: vendorKey.trim(), source: 'vendor_key' };
365
369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.10",
3
+ "version": "3.9.11",
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",
@@ -52,11 +52,11 @@
52
52
  "CHANGELOG.md"
53
53
  ],
54
54
  "dependencies": {
55
- "@lanonasis/mem-intel-sdk": "2.0.3",
56
- "@lanonasis/oauth-client": "2.0.0",
55
+ "@lanonasis/mem-intel-sdk": "2.0.6",
56
+ "@lanonasis/oauth-client": "2.0.4",
57
57
  "@lanonasis/security-sdk": "1.0.5",
58
- "@modelcontextprotocol/sdk": "^1.26.0",
59
- "axios": "^1.13.5",
58
+ "@modelcontextprotocol/sdk": "^1.28.0",
59
+ "axios": "^1.13.6",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-progress": "^3.12.0",
62
62
  "cli-table3": "^0.6.5",
@@ -64,26 +64,40 @@
64
64
  "date-fns": "^4.1.0",
65
65
  "dotenv": "^17.3.1",
66
66
  "eventsource": "^4.1.0",
67
- "inquirer": "^13.2.5",
67
+ "inquirer": "^13.3.2",
68
68
  "jwt-decode": "^4.0.0",
69
69
  "open": "^11.0.0",
70
70
  "ora": "^9.3.0",
71
71
  "table": "^6.9.0",
72
72
  "word-wrap": "^1.2.5",
73
- "ws": "^8.19.0",
73
+ "ws": "^8.20.0",
74
74
  "zod": "^4.3.6"
75
75
  },
76
76
  "devDependencies": {
77
- "@jest/globals": "^30.2.0",
77
+ "@jest/globals": "^30.3.0",
78
78
  "@types/cli-progress": "^3.11.6",
79
79
  "@types/inquirer": "^9.0.9",
80
- "@types/node": "^25.3.0",
80
+ "@types/node": "^25.5.0",
81
81
  "@types/ws": "^8.18.1",
82
- "fast-check": "^4.5.3",
83
- "jest": "^30.2.0",
82
+ "fast-check": "^4.6.0",
83
+ "jest": "^30.3.0",
84
84
  "rimraf": "^6.1.3",
85
85
  "ts-jest": "^29.4.6",
86
- "typescript": "^5.9.3"
86
+ "typescript": "^6.0.2"
87
+ },
88
+ "overrides": {
89
+ "@jest/transform": "^30.3.0",
90
+ "@hono/node-server": "^1.19.10",
91
+ "ajv": "^8.18.0",
92
+ "babel-jest": "^30.3.0",
93
+ "brace-expansion": "^1.1.13",
94
+ "express-rate-limit": "^8.2.2",
95
+ "handlebars": "^4.7.9",
96
+ "hono": "^4.12.8",
97
+ "jest-util": "^30.3.0",
98
+ "picomatch": "^2.3.2",
99
+ "qs": "^6.15.0",
100
+ "test-exclude": "^8.0.0"
87
101
  },
88
102
  "scripts": {
89
103
  "build": "rimraf dist && tsc -p tsconfig.json && chmod +x dist/index.js dist/mcp-server-entry.js 2>/dev/null || true",