@lanonasis/cli 3.9.12 → 3.9.13

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,17 @@
1
1
  # Changelog - @lanonasis/cli
2
2
 
3
+ ## [3.9.13] - 2026-04-02
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - **JWT/password sessions now refresh through the real auth-gateway contract**: Expiring CLI sessions no longer call the nonexistent `/v1/auth/refresh` route. Refreshable JWT and OAuth sessions now use `POST /oauth/token` with `grant_type=refresh_token`, matching the live auth-gateway implementation.
8
+ - **Password login now persists refresh metadata**: Username/password authentication now saves `refresh_token` and `token_expires_at` from the login response, so successful JWT sessions can actually refresh instead of silently falling back to re-login loops.
9
+ - **MCP client refresh flow no longer drifts from the main CLI auth flow**: Removed the stale `/auth/refresh` path and incorrect `refreshToken` config key lookup in favor of the shared `CLIConfig.refreshTokenIfNeeded()` implementation.
10
+
11
+ ### 🔄 Dependency Updates
12
+
13
+ - **Bundled `@lanonasis/mem-intel-sdk` updated to `2.1.0`**: Aligns the CLI with the newly published scoped intelligence query contract and predictive route support.
14
+
3
15
  ## [3.9.11] - 2026-03-27
4
16
 
5
17
  ### 🐛 Bug Fixes
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # @lanonasis/cli v3.9.11 - Stable Stats & Cleaner Startup
1
+ # @lanonasis/cli v3.9.13 - Auth Refresh Reliability
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.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.
8
+ 🎉 **NEW IN v3.9.13**: JWT/password CLI sessions now refresh through the real auth-gateway OAuth token contract, password login persists refresh metadata correctly, the MCP client no longer uses stale refresh routes, and the bundled `@lanonasis/mem-intel-sdk` is updated to `2.1.0` for scoped intelligence query support.
9
9
 
10
10
  ## 🚀 Quick Start
11
11
 
@@ -815,8 +815,16 @@ async function handleCredentialsFlow(options, config) {
815
815
  if (process.env.CLI_VERBOSE === 'true') {
816
816
  console.log(chalk.dim(` JWT received (length: ${authToken.length})`));
817
817
  }
818
+ const refreshToken = response.refresh_token;
819
+ const expiresIn = response.expires_in;
818
820
  // Store JWT token for API authentication
819
821
  await config.setToken(authToken);
822
+ if (typeof refreshToken === 'string' && refreshToken.length > 0) {
823
+ config.set('refresh_token', refreshToken);
824
+ }
825
+ if (typeof expiresIn === 'number' && Number.isFinite(expiresIn)) {
826
+ config.set('token_expires_at', Date.now() + (expiresIn * 1000));
827
+ }
820
828
  await config.setAndSave('authMethod', 'jwt');
821
829
  spinner.succeed('Login successful');
822
830
  console.log();
@@ -22,6 +22,12 @@ const MEMORY_TYPE_CHOICES = [
22
22
  'personal',
23
23
  'workflow',
24
24
  ];
25
+ const QUERY_SCOPE_CHOICES = [
26
+ 'personal',
27
+ 'team',
28
+ 'organization',
29
+ 'hybrid',
30
+ ];
25
31
  const coerceMemoryType = (value) => {
26
32
  if (typeof value !== 'string')
27
33
  return undefined;
@@ -34,6 +40,48 @@ const coerceMemoryType = (value) => {
34
40
  }
35
41
  return undefined;
36
42
  };
43
+ const coerceQueryScope = (value) => {
44
+ if (typeof value !== 'string')
45
+ return undefined;
46
+ const normalized = value.trim().toLowerCase();
47
+ if (QUERY_SCOPE_CHOICES.includes(normalized)) {
48
+ return normalized;
49
+ }
50
+ return undefined;
51
+ };
52
+ const parseMemoryTypesOption = (value) => {
53
+ if (!value?.trim())
54
+ return undefined;
55
+ const parsed = value
56
+ .split(',')
57
+ .map((entry) => coerceMemoryType(entry))
58
+ .filter((entry) => Boolean(entry));
59
+ if (parsed.length === 0) {
60
+ return undefined;
61
+ }
62
+ return [...new Set(parsed)];
63
+ };
64
+ const buildIntelligenceContextPayload = (options) => {
65
+ const payload = {};
66
+ if (options.organizationId?.trim()) {
67
+ payload.organization_id = options.organizationId.trim();
68
+ }
69
+ if (options.topicId?.trim()) {
70
+ payload.topic_id = options.topicId.trim();
71
+ }
72
+ if (options.scope) {
73
+ const scope = coerceQueryScope(options.scope);
74
+ if (!scope) {
75
+ throw new Error(`Invalid scope \"${options.scope}\". Expected one of: ${QUERY_SCOPE_CHOICES.join(', ')}`);
76
+ }
77
+ payload.query_scope = scope;
78
+ }
79
+ const memoryTypes = parseMemoryTypesOption(options.memoryTypes);
80
+ if (memoryTypes?.length) {
81
+ payload.memory_types = memoryTypes;
82
+ }
83
+ return payload;
84
+ };
37
85
  const resolveInputMode = async () => {
38
86
  const config = new CLIConfig();
39
87
  await config.init();
@@ -1015,13 +1063,21 @@ export function memoryCommands(program) {
1015
1063
  intelligence
1016
1064
  .command('health-check')
1017
1065
  .description('Run memory intelligence health check')
1066
+ .option('--organization-id <id>', 'Optional organization context')
1067
+ .option('--topic-id <id>', 'Optional topic context')
1068
+ .option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
1069
+ .option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
1018
1070
  .option('--json', 'Output raw JSON payload')
1019
1071
  .action(async (options) => {
1020
1072
  try {
1021
1073
  const spinner = ora('Running intelligence health check...').start();
1022
1074
  const transport = await createIntelligenceTransport();
1023
1075
  const userId = await resolveCurrentUserId();
1024
- const result = await postIntelligenceEndpoint(transport, '/intelligence/health-check', { user_id: userId, response_format: 'json' });
1076
+ const result = await postIntelligenceEndpoint(transport, '/intelligence/health-check', {
1077
+ user_id: userId,
1078
+ response_format: 'json',
1079
+ ...buildIntelligenceContextPayload(options),
1080
+ });
1025
1081
  spinner.stop();
1026
1082
  printIntelligenceResult('🩺 Intelligence Health Check', result, options);
1027
1083
  }
@@ -1036,6 +1092,9 @@ export function memoryCommands(program) {
1036
1092
  .description('Suggest tags for a memory')
1037
1093
  .argument('<memory-id>', 'Memory ID')
1038
1094
  .option('--max <number>', 'Maximum suggestions', '8')
1095
+ .option('--organization-id <id>', 'Optional organization context')
1096
+ .option('--topic-id <id>', 'Optional topic context')
1097
+ .option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
1039
1098
  .option('--json', 'Output raw JSON payload')
1040
1099
  .action(async (memoryId, options) => {
1041
1100
  try {
@@ -1049,6 +1108,7 @@ export function memoryCommands(program) {
1049
1108
  max_suggestions: maxSuggestions,
1050
1109
  include_existing_tags: true,
1051
1110
  response_format: 'json',
1111
+ ...buildIntelligenceContextPayload(options),
1052
1112
  });
1053
1113
  spinner.stop();
1054
1114
  printIntelligenceResult('🏷️ Tag Suggestions', result, options);
@@ -1065,6 +1125,10 @@ export function memoryCommands(program) {
1065
1125
  .argument('<memory-id>', 'Source memory ID')
1066
1126
  .option('--limit <number>', 'Maximum related memories', '5')
1067
1127
  .option('--threshold <number>', 'Similarity threshold (0-1)', '0.7')
1128
+ .option('--organization-id <id>', 'Optional organization context')
1129
+ .option('--topic-id <id>', 'Optional topic context')
1130
+ .option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
1131
+ .option('--memory-types <types>', `Optional comma-separated candidate memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
1068
1132
  .option('--json', 'Output raw JSON payload')
1069
1133
  .action(async (memoryId, options) => {
1070
1134
  try {
@@ -1077,6 +1141,7 @@ export function memoryCommands(program) {
1077
1141
  limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
1078
1142
  similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
1079
1143
  response_format: 'json',
1144
+ ...buildIntelligenceContextPayload(options),
1080
1145
  });
1081
1146
  spinner.stop();
1082
1147
  printIntelligenceResult('🔗 Related Memories', result, options);
@@ -1092,6 +1157,10 @@ export function memoryCommands(program) {
1092
1157
  .description('Detect duplicate memory entries')
1093
1158
  .option('--threshold <number>', 'Similarity threshold (0-1)', '0.88')
1094
1159
  .option('--max-pairs <number>', 'Maximum duplicate pairs to inspect', '100')
1160
+ .option('--organization-id <id>', 'Optional organization context')
1161
+ .option('--topic-id <id>', 'Optional topic context')
1162
+ .option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
1163
+ .option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
1095
1164
  .option('--json', 'Output raw JSON payload')
1096
1165
  .action(async (options) => {
1097
1166
  try {
@@ -1103,6 +1172,7 @@ export function memoryCommands(program) {
1103
1172
  similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.88'))),
1104
1173
  max_pairs: Math.max(10, Math.min(500, parseInt(options.maxPairs || '100', 10))),
1105
1174
  response_format: 'json',
1175
+ ...buildIntelligenceContextPayload(options),
1106
1176
  });
1107
1177
  spinner.stop();
1108
1178
  printIntelligenceResult('🧬 Duplicate Detection', result, options);
@@ -1119,6 +1189,10 @@ export function memoryCommands(program) {
1119
1189
  .option('--topic <topic>', 'Optional topic filter')
1120
1190
  .option('--type <type>', `Optional memory type filter (${MEMORY_TYPE_CHOICES.join(', ')})`)
1121
1191
  .option('--max-memories <number>', 'Maximum memories to analyze', '50')
1192
+ .option('--organization-id <id>', 'Optional organization context')
1193
+ .option('--topic-id <id>', 'Optional topic context')
1194
+ .option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
1195
+ .option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
1122
1196
  .option('--json', 'Output raw JSON payload')
1123
1197
  .action(async (options) => {
1124
1198
  try {
@@ -1135,6 +1209,7 @@ export function memoryCommands(program) {
1135
1209
  memory_type: memoryType,
1136
1210
  max_memories: Math.max(5, Math.min(200, parseInt(options.maxMemories || '50', 10))),
1137
1211
  response_format: 'json',
1212
+ ...buildIntelligenceContextPayload(options),
1138
1213
  });
1139
1214
  spinner.stop();
1140
1215
  printIntelligenceResult('💡 Memory Insights', result, options);
@@ -1149,6 +1224,10 @@ export function memoryCommands(program) {
1149
1224
  .command('analyze-patterns')
1150
1225
  .description('Analyze memory usage patterns')
1151
1226
  .option('--days <number>', 'Days to include in analysis', '30')
1227
+ .option('--organization-id <id>', 'Optional organization context')
1228
+ .option('--topic-id <id>', 'Optional topic context')
1229
+ .option('--scope <scope>', `Optional query scope (${QUERY_SCOPE_CHOICES.join(', ')})`)
1230
+ .option('--memory-types <types>', `Optional comma-separated memory types (${MEMORY_TYPE_CHOICES.join(', ')})`)
1152
1231
  .option('--json', 'Output raw JSON payload')
1153
1232
  .action(async (options) => {
1154
1233
  try {
@@ -1159,6 +1238,7 @@ export function memoryCommands(program) {
1159
1238
  user_id: userId,
1160
1239
  time_range_days: Math.max(1, Math.min(365, parseInt(options.days || '30', 10))),
1161
1240
  response_format: 'json',
1241
+ ...buildIntelligenceContextPayload(options),
1162
1242
  });
1163
1243
  spinner.stop();
1164
1244
  printIntelligenceResult('📈 Pattern Analysis', result, options);
@@ -9,8 +9,11 @@ export interface AuthResponse {
9
9
  created_at: string;
10
10
  updated_at: string;
11
11
  };
12
- token: string;
13
- expires_at: string;
12
+ token?: string;
13
+ access_token?: string;
14
+ refresh_token?: string;
15
+ expires_in?: number;
16
+ expires_at?: string;
14
17
  }
15
18
  export interface RegisterRequest {
16
19
  email: string;
@@ -27,6 +27,8 @@ interface CLIConfigData {
27
27
  lastManualEndpointUpdate?: string;
28
28
  vendorKey?: string | undefined;
29
29
  authMethod?: 'jwt' | 'vendor_key' | 'oauth' | 'oauth2' | undefined;
30
+ refresh_token?: string | undefined;
31
+ token_expires_at?: number | string | undefined;
30
32
  tokenExpiry?: number | undefined;
31
33
  lastValidated?: string | undefined;
32
34
  deviceId?: string;
@@ -130,6 +132,7 @@ export declare class CLIConfig {
130
132
  exists(): Promise<boolean>;
131
133
  validateStoredCredentials(): Promise<boolean>;
132
134
  refreshTokenIfNeeded(): Promise<void>;
135
+ private refreshViaOAuthTokenEndpoint;
133
136
  clearInvalidCredentials(): Promise<void>;
134
137
  incrementFailureCount(): Promise<void>;
135
138
  resetFailureCount(): Promise<void>;
@@ -1283,9 +1283,9 @@ export class CLIConfig {
1283
1283
  if (String(this.config.authMethod || '').toLowerCase() === 'vendor_key') {
1284
1284
  return;
1285
1285
  }
1286
+ const refreshToken = this.get('refresh_token');
1286
1287
  // OAuth token refresh (opaque tokens + refresh_token + token_expires_at)
1287
- if (this.config.authMethod === 'oauth') {
1288
- const refreshToken = this.get('refresh_token');
1288
+ if (this.config.authMethod === 'oauth' || this.config.authMethod === 'oauth2') {
1289
1289
  if (!refreshToken) {
1290
1290
  return;
1291
1291
  }
@@ -1310,37 +1310,7 @@ export class CLIConfig {
1310
1310
  return;
1311
1311
  }
1312
1312
  await this.discoverServices();
1313
- const authBase = this.getDiscoveredApiUrl();
1314
- const resp = await axios.post(`${authBase}/oauth/token`, {
1315
- grant_type: 'refresh_token',
1316
- refresh_token: refreshToken,
1317
- client_id: 'lanonasis-cli'
1318
- }, {
1319
- headers: { 'Content-Type': 'application/json' },
1320
- timeout: 10000,
1321
- proxy: false
1322
- });
1323
- // Some gateways wrap responses as `{ data: { ... } }`.
1324
- const raw = resp?.data;
1325
- const payload = raw && typeof raw === 'object' && raw.data && typeof raw.data === 'object'
1326
- ? raw.data
1327
- : raw;
1328
- const accessToken = payload?.access_token ?? payload?.token;
1329
- const refreshedRefreshToken = payload?.refresh_token;
1330
- const expiresIn = payload?.expires_in;
1331
- if (typeof accessToken !== 'string' || accessToken.length === 0) {
1332
- throw new Error('Token refresh response missing access_token');
1333
- }
1334
- // setToken() assumes JWT by default; ensure authMethod stays oauth after storing.
1335
- await this.setToken(accessToken);
1336
- this.config.authMethod = 'oauth';
1337
- if (typeof refreshedRefreshToken === 'string' && refreshedRefreshToken.length > 0) {
1338
- this.config.refresh_token = refreshedRefreshToken;
1339
- }
1340
- if (typeof expiresIn === 'number' && Number.isFinite(expiresIn)) {
1341
- this.config.token_expires_at = Date.now() + (expiresIn * 1000);
1342
- }
1343
- await this.save().catch(() => { });
1313
+ await this.refreshViaOAuthTokenEndpoint(refreshToken, this.getDiscoveredApiUrl(), 'oauth');
1344
1314
  return;
1345
1315
  }
1346
1316
  // Check if token is JWT and if it's close to expiry
@@ -1359,20 +1329,11 @@ export class CLIConfig {
1359
1329
  const exp = typeof decoded.exp === 'number' ? decoded.exp : 0;
1360
1330
  // Refresh if token expires within 5 minutes
1361
1331
  if (exp > 0 && (exp - now) < 300) {
1362
- // Import axios dynamically
1363
- await this.discoverServices();
1364
- const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
1365
- // Attempt token refresh
1366
- const response = await axios.post(`${authBase}/v1/auth/refresh`, {}, {
1367
- headers: {
1368
- 'Authorization': `Bearer ${token}`,
1369
- 'X-Project-Scope': 'lanonasis-maas'
1370
- },
1371
- timeout: 10000
1372
- });
1373
- if (response.data.token) {
1374
- await this.setToken(response.data.token);
1332
+ if (!refreshToken) {
1333
+ return;
1375
1334
  }
1335
+ await this.discoverServices();
1336
+ await this.refreshViaOAuthTokenEndpoint(refreshToken, this.getDiscoveredApiUrl(), 'jwt');
1376
1337
  }
1377
1338
  }
1378
1339
  catch (err) {
@@ -1383,6 +1344,36 @@ export class CLIConfig {
1383
1344
  }
1384
1345
  }
1385
1346
  }
1347
+ async refreshViaOAuthTokenEndpoint(refreshToken, authBase, authMethod) {
1348
+ const resp = await axios.post(`${authBase}/oauth/token`, {
1349
+ grant_type: 'refresh_token',
1350
+ refresh_token: refreshToken,
1351
+ client_id: 'lanonasis-cli'
1352
+ }, {
1353
+ headers: { 'Content-Type': 'application/json' },
1354
+ timeout: 10000,
1355
+ proxy: false
1356
+ });
1357
+ const raw = resp?.data;
1358
+ const payload = raw && typeof raw === 'object' && raw.data && typeof raw.data === 'object'
1359
+ ? raw.data
1360
+ : raw;
1361
+ const accessToken = payload?.access_token ?? payload?.token;
1362
+ const refreshedRefreshToken = payload?.refresh_token;
1363
+ const expiresIn = payload?.expires_in;
1364
+ if (typeof accessToken !== 'string' || accessToken.length === 0) {
1365
+ throw new Error('Token refresh response missing access_token');
1366
+ }
1367
+ await this.setToken(accessToken);
1368
+ this.config.authMethod = authMethod;
1369
+ if (typeof refreshedRefreshToken === 'string' && refreshedRefreshToken.length > 0) {
1370
+ this.config.refresh_token = refreshedRefreshToken;
1371
+ }
1372
+ if (typeof expiresIn === 'number' && Number.isFinite(expiresIn)) {
1373
+ this.config.token_expires_at = Date.now() + (expiresIn * 1000);
1374
+ }
1375
+ await this.save().catch(() => { });
1376
+ }
1386
1377
  async clearInvalidCredentials() {
1387
1378
  this.config.token = undefined;
1388
1379
  this.config.vendorKey = undefined;
@@ -451,20 +451,11 @@ export class MCPClient {
451
451
  * Refresh token if needed
452
452
  */
453
453
  async refreshTokenIfNeeded() {
454
- const refreshToken = this.config.get('refreshToken');
455
- if (!refreshToken) {
456
- throw new Error('No refresh token available. Please re-authenticate.');
457
- }
458
454
  try {
459
- const axios = (await import('axios')).default;
460
- const authUrl = this.config.get('authUrl') ?? 'https://api.lanonasis.com';
461
- const response = await axios.post(`${authUrl}/auth/refresh`, {
462
- refresh_token: refreshToken
463
- }, {
464
- timeout: 10000
465
- });
466
- if (response.data.access_token) {
467
- await this.config.setAndSave('token', response.data.access_token);
455
+ const previousToken = this.config.getToken();
456
+ await this.config.refreshTokenIfNeeded();
457
+ const currentToken = this.config.getToken();
458
+ if (currentToken && currentToken !== previousToken) {
468
459
  console.log(chalk.green('✓ Token refreshed successfully'));
469
460
  }
470
461
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.12",
3
+ "version": "3.9.13",
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.6",
55
+ "@lanonasis/mem-intel-sdk": "2.1.0",
56
56
  "@lanonasis/oauth-client": "2.0.4",
57
57
  "@lanonasis/security-sdk": "1.0.5",
58
58
  "@modelcontextprotocol/sdk": "^1.28.0",
59
- "axios": "^1.13.6",
59
+ "axios": "^1.14.0",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-progress": "^3.12.0",
62
62
  "cli-table3": "^0.6.5",