@lanonasis/cli 3.9.11 → 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
 
@@ -15,6 +15,24 @@ const colors = {
15
15
  muted: chalk.gray,
16
16
  highlight: chalk.white.bold
17
17
  };
18
+ const AUTH_API_KEYS_BASE = '/api/v1/auth/api-keys';
19
+ const VALID_ACCESS_LEVELS = ['public', 'authenticated', 'team', 'admin', 'enterprise'];
20
+ function unwrapApiResponse(response) {
21
+ if (response && typeof response === 'object' && 'data' in response) {
22
+ return response.data ?? response;
23
+ }
24
+ return response;
25
+ }
26
+ function parseScopes(scopes) {
27
+ if (!scopes) {
28
+ return undefined;
29
+ }
30
+ const parsed = scopes
31
+ .split(',')
32
+ .map((scope) => scope.trim())
33
+ .filter(Boolean);
34
+ return parsed.length > 0 ? parsed : undefined;
35
+ }
18
36
  const apiKeysCommand = new Command('api-keys')
19
37
  .alias('keys')
20
38
  .description(colors.info('🔐 Manage API keys securely with enterprise-grade encryption'));
@@ -65,7 +83,8 @@ projectsCommand
65
83
  ]);
66
84
  projectData = { ...projectData, ...answers };
67
85
  }
68
- const project = await apiClient.post('/api-keys/projects', projectData);
86
+ const projectRes = await apiClient.post(`${AUTH_API_KEYS_BASE}/projects`, projectData);
87
+ const project = unwrapApiResponse(projectRes);
69
88
  console.log(chalk.green('✅ Project created successfully!'));
70
89
  console.log(chalk.blue(`Project ID: ${project.id}`));
71
90
  console.log(chalk.blue(`Name: ${project.name}`));
@@ -85,12 +104,12 @@ projectsCommand
85
104
  .option('--json', 'Output as JSON')
86
105
  .action(async (options) => {
87
106
  try {
88
- const projects = await apiClient.get('/api-keys/projects');
107
+ const projects = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/projects`));
89
108
  if (options.json) {
90
109
  console.log(JSON.stringify(projects, null, 2));
91
110
  return;
92
111
  }
93
- if (projects.length === 0) {
112
+ if (!Array.isArray(projects) || projects.length === 0) {
94
113
  console.log(chalk.yellow('No projects found'));
95
114
  return;
96
115
  }
@@ -122,30 +141,29 @@ apiKeysCommand
122
141
  .command('create')
123
142
  .description('Create a new API key')
124
143
  .option('-n, --name <name>', 'API key name')
125
- .option('-v, --value <value>', 'API key value')
126
- .option('-t, --type <type>', 'Key type (api_key, database_url, oauth_token, etc.)')
127
- .option('-e, --environment <env>', 'Environment (development, staging, production)')
128
- .option('-p, --project-id <id>', 'Project ID')
129
- .option('--access-level <level>', 'Access level (public, authenticated, team, admin, enterprise)')
130
- .option('--tags <tags>', 'Comma-separated tags')
131
- .option('--expires-at <date>', 'Expiration date (ISO format)')
132
- .option('--rotation-frequency <days>', 'Rotation frequency in days', '90')
144
+ .option('-d, --description <description>', 'API key description (optional)')
145
+ .option('--access-level <level>', 'Access level (public, authenticated, team, admin, enterprise)', 'team')
146
+ .option('--expires-in-days <days>', 'Expiration in days (default: 365)', '365')
147
+ .option('--scopes <scopes>', 'Comma-separated scopes (optional)')
133
148
  .option('--interactive', 'Interactive mode')
134
149
  .action(async (options) => {
135
150
  try {
151
+ const accessLevel = (options.accessLevel || 'team').toLowerCase();
152
+ const expiresInDays = parseInt(options.expiresInDays, 10);
153
+ if (!VALID_ACCESS_LEVELS.includes(accessLevel)) {
154
+ throw new Error('Invalid access level. Allowed: public, authenticated, team, admin, enterprise');
155
+ }
156
+ if (!Number.isInteger(expiresInDays) || expiresInDays <= 0 || expiresInDays > 3650) {
157
+ throw new Error('expires-in-days must be a positive integer up to 3650');
158
+ }
136
159
  let keyData = {
137
160
  name: options.name,
138
- value: options.value,
139
- keyType: options.type,
140
- environment: options.environment || 'development',
141
- projectId: options.projectId,
142
- accessLevel: options.accessLevel || 'team',
143
- tags: options.tags ? options.tags.split(',').map((tag) => tag.trim()) : [],
144
- expiresAt: options.expiresAt,
145
- rotationFrequency: parseInt(options.rotationFrequency)
161
+ access_level: accessLevel,
162
+ expires_in_days: expiresInDays,
163
+ description: options.description?.trim() || undefined,
164
+ scopes: parseScopes(options.scopes)
146
165
  };
147
- if (options.interactive || !keyData.name || !keyData.value || !keyData.projectId) {
148
- const projects = await apiClient.get('/api-keys/projects');
166
+ if (options.interactive || !keyData.name) {
149
167
  const answers = await inquirer.prompt([
150
168
  {
151
169
  type: 'input',
@@ -155,85 +173,62 @@ apiKeysCommand
155
173
  validate: (input) => input.length > 0 || 'Name is required'
156
174
  },
157
175
  {
158
- type: 'password',
159
- name: 'value',
160
- message: 'API key value:',
161
- when: !keyData.value,
162
- validate: (input) => input.length > 0 || 'Value is required'
163
- },
164
- {
165
- type: 'select',
166
- name: 'keyType',
167
- message: 'Key type:',
168
- when: !keyData.keyType,
169
- choices: [
170
- 'api_key',
171
- 'database_url',
172
- 'oauth_token',
173
- 'certificate',
174
- 'ssh_key',
175
- 'webhook_secret',
176
- 'encryption_key'
177
- ]
178
- },
179
- {
180
- type: 'select',
181
- name: 'environment',
182
- message: 'Environment:',
183
- choices: ['development', 'staging', 'production'],
184
- default: 'development'
185
- },
186
- {
187
- type: 'select',
188
- name: 'projectId',
189
- message: 'Select project:',
190
- when: !keyData.projectId && projects.length > 0,
191
- choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id }))
176
+ type: 'input',
177
+ name: 'description',
178
+ message: 'Description (optional):',
179
+ default: keyData.description || ''
192
180
  },
193
181
  {
194
182
  type: 'select',
195
- name: 'accessLevel',
183
+ name: 'access_level',
196
184
  message: 'Access level:',
197
- choices: ['public', 'authenticated', 'team', 'admin', 'enterprise'],
198
- default: 'team'
185
+ choices: VALID_ACCESS_LEVELS,
186
+ default: keyData.access_level
199
187
  },
200
188
  {
201
- type: 'input',
202
- name: 'tags',
203
- message: 'Tags (comma-separated, optional):',
204
- filter: (input) => input ? input.split(',').map((tag) => tag.trim()) : []
189
+ type: 'number',
190
+ name: 'expires_in_days',
191
+ message: 'Expires in days:',
192
+ default: keyData.expires_in_days,
193
+ validate: (input) => Number.isInteger(input) && input > 0 && input <= 3650 || 'Must be between 1 and 3650 days'
205
194
  },
206
195
  {
207
196
  type: 'input',
208
- name: 'expiresAt',
209
- message: 'Expiration date (YYYY-MM-DD, optional):',
210
- validate: (input) => {
211
- if (!input)
212
- return true;
213
- const date = new Date(input);
214
- return !isNaN(date.getTime()) || 'Please enter a valid date';
215
- },
216
- filter: (input) => input ? new Date(input).toISOString() : undefined
217
- },
218
- {
219
- type: 'number',
220
- name: 'rotationFrequency',
221
- message: 'Rotation frequency (days):',
222
- default: 90,
223
- validate: (input) => input > 0 && input <= 365 || 'Must be between 1 and 365 days'
197
+ name: 'scopes',
198
+ message: 'Scopes (comma-separated, optional):',
199
+ default: (keyData.scopes || []).join(', ')
224
200
  }
225
201
  ]);
226
- keyData = { ...keyData, ...answers };
202
+ keyData = {
203
+ ...keyData,
204
+ ...answers,
205
+ description: typeof answers.description === 'string'
206
+ ? answers.description.trim() || undefined
207
+ : keyData.description,
208
+ scopes: parseScopes(typeof answers.scopes === 'string' ? answers.scopes : undefined) ?? keyData.scopes
209
+ };
227
210
  }
228
- const apiKey = await apiClient.post('/api-keys', keyData);
211
+ const apiKey = unwrapApiResponse(await apiClient.post(AUTH_API_KEYS_BASE, keyData));
229
212
  console.log(colors.success('🔐 API key created successfully!'));
230
213
  console.log(colors.info('━'.repeat(50)));
231
214
  console.log(`${colors.highlight('Key ID:')} ${colors.primary(apiKey.id)}`);
232
215
  console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
233
- console.log(`${colors.highlight('Type:')} ${colors.info(apiKey.keyType)}`);
234
- console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
216
+ console.log(`${colors.highlight('Access Level:')} ${colors.info(apiKey.access_level || keyData.access_level)}`);
217
+ console.log(`${colors.highlight('Permissions:')} ${colors.muted((apiKey.permissions || keyData.scopes || []).join(', ') || 'legacy:full_access')}`);
218
+ if (apiKey.expires_at) {
219
+ console.log(`${colors.highlight('Expires At:')} ${colors.warning(formatDate(apiKey.expires_at))}`);
220
+ }
221
+ if (keyData.description) {
222
+ console.log(`${colors.highlight('Description:')} ${colors.muted(keyData.description)}`);
223
+ }
235
224
  console.log(colors.info('━'.repeat(50)));
236
- console.log(colors.warning('⚠️ The key value is securely encrypted and cannot be retrieved later.'));
225
+ if (apiKey.key) {
226
+ console.log(`${colors.highlight('API Key:')} ${colors.primary(apiKey.key)}`);
227
+ console.log(colors.warning('⚠️ Save this key now. It will not be shown again.'));
228
+ }
229
+ else {
230
+ console.log(colors.warning('⚠️ Key value was not returned. If newly created, it cannot be retrieved later.'));
231
+ }
237
232
  }
238
233
  catch (error) {
239
234
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -245,15 +240,12 @@ apiKeysCommand
245
240
  .command('list')
246
241
  .alias('ls')
247
242
  .description('List API keys')
248
- .option('-p, --project-id <id>', 'Filter by project ID')
243
+ .option('--all', 'Include inactive keys')
249
244
  .option('--json', 'Output as JSON')
250
245
  .action(async (options) => {
251
246
  try {
252
- let url = '/api-keys';
253
- if (options.projectId) {
254
- url += `?projectId=${options.projectId}`;
255
- }
256
- const apiKeys = await apiClient.get(url);
247
+ const query = options.all ? '?active_only=false' : '';
248
+ const apiKeys = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}${query}`));
257
249
  if (options.json) {
258
250
  console.log(JSON.stringify(apiKeys, null, 2));
259
251
  return;
@@ -266,20 +258,19 @@ apiKeysCommand
266
258
  console.log(colors.primary('🔐 API Key Management'));
267
259
  console.log(colors.info('═'.repeat(80)));
268
260
  const table = new Table({
269
- head: ['ID', 'Name', 'Type', 'Environment', 'Status', 'Usage', 'Last Rotated'].map(h => colors.accent(h)),
261
+ head: ['ID', 'Name', 'Access', 'Permissions', 'Service', 'Status', 'Expires'].map(h => colors.accent(h)),
270
262
  style: { head: [], border: [] }
271
263
  });
272
264
  apiKeys.forEach((key) => {
273
- const statusColor = key.status === 'active' ? colors.success :
274
- key.status === 'rotating' ? colors.warning : colors.error;
265
+ const statusColor = key.is_active ? colors.success : colors.error;
275
266
  table.push([
276
267
  truncateText(key.id, 20),
277
268
  key.name,
278
- key.keyType,
279
- key.environment,
280
- statusColor(key.status),
281
- colors.highlight(key.usageCount.toString()),
282
- formatDate(key.lastRotated)
269
+ key.access_level,
270
+ truncateText((key.permissions || []).join(', ') || 'legacy:full_access', 28),
271
+ key.service || 'all',
272
+ statusColor(key.is_active ? 'active' : 'inactive'),
273
+ key.expires_at ? formatDate(key.expires_at) : colors.muted('Never')
283
274
  ]);
284
275
  });
285
276
  console.log(table.toString());
@@ -299,7 +290,7 @@ apiKeysCommand
299
290
  .option('--json', 'Output as JSON')
300
291
  .action(async (keyId, options) => {
301
292
  try {
302
- const apiKey = await apiClient.get(`/api-keys/${keyId}`);
293
+ const apiKey = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/${keyId}`));
303
294
  if (options.json) {
304
295
  console.log(JSON.stringify(apiKey, null, 2));
305
296
  return;
@@ -308,21 +299,20 @@ apiKeysCommand
308
299
  console.log(colors.info('═'.repeat(60)));
309
300
  console.log(`${colors.highlight('ID:')} ${colors.primary(apiKey.id)}`);
310
301
  console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
311
- console.log(`${colors.highlight('Type:')} ${colors.info(apiKey.keyType)}`);
312
- console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
313
- console.log(`${colors.highlight('Project ID:')} ${colors.muted(apiKey.projectId)}`);
314
- console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.accessLevel)}`);
315
- const statusColor = apiKey.status === 'active' ? colors.success :
316
- apiKey.status === 'rotating' ? colors.warning : colors.error;
317
- console.log(`${colors.highlight('Status:')} ${statusColor(apiKey.status)}`);
318
- console.log(`${colors.highlight('Usage Count:')} ${colors.accent(apiKey.usageCount)}`);
319
- console.log(`${colors.highlight('Tags:')} ${colors.muted(apiKey.tags.join(', ') || 'None')}`);
320
- console.log(`${colors.highlight('Rotation Frequency:')} ${colors.info(apiKey.rotationFrequency)} days`);
321
- console.log(`${colors.highlight('Last Rotated:')} ${colors.muted(formatDate(apiKey.lastRotated))}`);
322
- console.log(`${colors.highlight('Created:')} ${colors.muted(formatDate(apiKey.createdAt))}`);
323
- console.log(`${colors.highlight('Updated:')} ${colors.muted(formatDate(apiKey.updatedAt))}`);
324
- if (apiKey.expiresAt) {
325
- console.log(`${colors.highlight('Expires:')} ${colors.warning(formatDate(apiKey.expiresAt))}`);
302
+ if (apiKey.description) {
303
+ console.log(`${colors.highlight('Description:')} ${colors.muted(apiKey.description)}`);
304
+ }
305
+ console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.access_level)}`);
306
+ console.log(`${colors.highlight('Permissions:')} ${colors.muted((apiKey.permissions || []).join(', ') || 'legacy:full_access')}`);
307
+ console.log(`${colors.highlight('Service Scope:')} ${colors.info(apiKey.service || 'all')}`);
308
+ const statusColor = apiKey.is_active ? colors.success : colors.error;
309
+ console.log(`${colors.highlight('Status:')} ${statusColor(apiKey.is_active ? 'active' : 'inactive')}`);
310
+ if (apiKey.last_used_at) {
311
+ console.log(`${colors.highlight('Last Used:')} ${colors.muted(formatDate(apiKey.last_used_at))}`);
312
+ }
313
+ console.log(`${colors.highlight('Created:')} ${colors.muted(formatDate(apiKey.created_at))}`);
314
+ if (apiKey.expires_at) {
315
+ console.log(`${colors.highlight('Expires:')} ${colors.warning(formatDate(apiKey.expires_at))}`);
326
316
  }
327
317
  console.log(colors.info('═'.repeat(60)));
328
318
  }
@@ -337,23 +327,39 @@ apiKeysCommand
337
327
  .description('Update an API key')
338
328
  .argument('<keyId>', 'API key ID')
339
329
  .option('-n, --name <name>', 'New name')
340
- .option('-v, --value <value>', 'New value')
341
- .option('--tags <tags>', 'Comma-separated tags')
342
- .option('--rotation-frequency <days>', 'Rotation frequency in days')
330
+ .option('-d, --description <description>', 'New description')
331
+ .option('--access-level <level>', 'New access level')
332
+ .option('--expires-in-days <days>', 'Set a new expiry in days')
333
+ .option('--clear-expiry', 'Remove the current expiry')
334
+ .option('--scopes <scopes>', 'Replace scopes with a comma-separated list')
343
335
  .option('--interactive', 'Interactive mode')
344
336
  .action(async (keyId, options) => {
345
337
  try {
346
338
  let updateData = {};
347
339
  if (options.name)
348
340
  updateData.name = options.name;
349
- if (options.value)
350
- updateData.value = options.value;
351
- if (options.tags)
352
- updateData.tags = options.tags.split(',').map((tag) => tag.trim());
353
- if (options.rotationFrequency)
354
- updateData.rotationFrequency = parseInt(options.rotationFrequency);
341
+ if (options.description !== undefined)
342
+ updateData.description = options.description.trim() || null;
343
+ if (options.accessLevel) {
344
+ const accessLevel = options.accessLevel.toLowerCase();
345
+ if (!VALID_ACCESS_LEVELS.includes(accessLevel)) {
346
+ throw new Error('Invalid access level. Allowed: public, authenticated, team, admin, enterprise');
347
+ }
348
+ updateData.access_level = accessLevel;
349
+ }
350
+ if (options.expiresInDays) {
351
+ const expiresInDays = parseInt(options.expiresInDays, 10);
352
+ if (!Number.isInteger(expiresInDays) || expiresInDays <= 0 || expiresInDays > 3650) {
353
+ throw new Error('expires-in-days must be a positive integer up to 3650');
354
+ }
355
+ updateData.expires_in_days = expiresInDays;
356
+ }
357
+ if (options.clearExpiry)
358
+ updateData.clear_expiry = true;
359
+ if (options.scopes)
360
+ updateData.scopes = parseScopes(options.scopes);
355
361
  if (options.interactive || Object.keys(updateData).length === 0) {
356
- const current = await apiClient.get(`/api-keys/${keyId}`);
362
+ const current = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/${keyId}`));
357
363
  const answers = await inquirer.prompt([
358
364
  {
359
365
  type: 'input',
@@ -362,47 +368,75 @@ apiKeysCommand
362
368
  default: current.name,
363
369
  when: !updateData.name
364
370
  },
371
+ {
372
+ type: 'input',
373
+ name: 'description',
374
+ message: 'Description (optional):',
375
+ default: current.description || '',
376
+ when: !updateData.description
377
+ },
378
+ {
379
+ type: 'select',
380
+ name: 'access_level',
381
+ message: 'Access level:',
382
+ choices: VALID_ACCESS_LEVELS,
383
+ default: current.access_level,
384
+ when: !updateData.access_level
385
+ },
365
386
  {
366
387
  type: 'confirm',
367
- name: 'updateValue',
368
- message: 'Update the key value?',
388
+ name: 'changeExpiry',
389
+ message: 'Change expiry?',
369
390
  default: false,
370
- when: !updateData.value
391
+ when: updateData.expires_in_days === undefined && !updateData.clear_expiry
371
392
  },
372
393
  {
373
- type: 'password',
374
- name: 'value',
375
- message: 'New key value:',
376
- when: (answers) => answers.updateValue
394
+ type: 'number',
395
+ name: 'expires_in_days',
396
+ message: 'Expires in days:',
397
+ default: current.expires_at ? 365 : 365,
398
+ when: (answers) => answers.changeExpiry === true && updateData.expires_in_days === undefined && !updateData.clear_expiry,
399
+ validate: (input) => Number.isInteger(input) && input > 0 && input <= 3650 || 'Must be between 1 and 3650 days'
377
400
  },
378
401
  {
379
- type: 'input',
380
- name: 'tags',
381
- message: 'Tags (comma-separated):',
382
- default: current.tags.join(', '),
383
- filter: (input) => input.split(',').map((tag) => tag.trim()),
384
- when: !updateData.tags
402
+ type: 'confirm',
403
+ name: 'clear_expiry',
404
+ message: 'Clear expiry instead?',
405
+ default: false,
406
+ when: (answers) => answers.changeExpiry === true && updateData.expires_in_days === undefined && !updateData.clear_expiry && Boolean(current.expires_at)
385
407
  },
386
408
  {
387
- type: 'number',
388
- name: 'rotationFrequency',
389
- message: 'Rotation frequency (days):',
390
- default: current.rotationFrequency,
391
- validate: (input) => input > 0 && input <= 365 || 'Must be between 1 and 365 days',
392
- when: !updateData.rotationFrequency
409
+ type: 'input',
410
+ name: 'scopes',
411
+ message: 'Scopes (comma-separated, optional):',
412
+ default: (current.permissions || []).join(', '),
413
+ when: !updateData.scopes
393
414
  }
394
415
  ]);
395
- updateData = { ...updateData, ...answers };
396
- delete updateData.updateValue;
416
+ updateData = {
417
+ ...updateData,
418
+ ...answers,
419
+ description: typeof answers.description === 'string'
420
+ ? answers.description.trim() || null
421
+ : updateData.description,
422
+ scopes: parseScopes(typeof answers.scopes === 'string' ? answers.scopes : undefined) ?? updateData.scopes
423
+ };
424
+ delete updateData.changeExpiry;
425
+ }
426
+ if (Object.keys(updateData).length === 0) {
427
+ console.log(colors.warning('🚫 Nothing to update'));
428
+ return;
397
429
  }
398
- const updatedKey = await apiClient.put(`/api-keys/${keyId}`, updateData);
430
+ const updatedKey = unwrapApiResponse(await apiClient.put(`${AUTH_API_KEYS_BASE}/${keyId}`, updateData));
399
431
  console.log(colors.success('🔄 API key updated successfully!'));
400
432
  console.log(colors.info('━'.repeat(40)));
401
433
  console.log(`${colors.highlight('Name:')} ${colors.accent(updatedKey.name)}`);
402
- console.log(`${colors.highlight('Status:')} ${colors.success(updatedKey.status)}`);
403
- if (updateData.value) {
404
- console.log(colors.warning('⚠️ The key value has been updated and re-encrypted.'));
434
+ if (updatedKey.description || updateData.description) {
435
+ console.log(`${colors.highlight('Description:')} ${colors.muted(updatedKey.description || updateData.description)}`);
405
436
  }
437
+ console.log(`${colors.highlight('Access Level:')} ${colors.info(updatedKey.access_level)}`);
438
+ console.log(`${colors.highlight('Permissions:')} ${colors.muted((updatedKey.permissions || []).join(', ') || 'legacy:full_access')}`);
439
+ console.log(`${colors.highlight('Expires:')} ${updatedKey.expires_at ? colors.warning(formatDate(updatedKey.expires_at)) : colors.muted('Never')}`);
406
440
  console.log(colors.info('━'.repeat(40)));
407
441
  }
408
442
  catch (error) {
@@ -420,7 +454,7 @@ apiKeysCommand
420
454
  .action(async (keyId, options) => {
421
455
  try {
422
456
  if (!options.force) {
423
- const apiKey = await apiClient.get(`/api-keys/${keyId}`);
457
+ const apiKey = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/${keyId}`));
424
458
  const { confirm } = await inquirer.prompt([
425
459
  {
426
460
  type: 'confirm',
@@ -434,7 +468,7 @@ apiKeysCommand
434
468
  return;
435
469
  }
436
470
  }
437
- await apiClient.delete(`/api-keys/${keyId}`);
471
+ await apiClient.delete(`${AUTH_API_KEYS_BASE}/${keyId}`);
438
472
  console.log(colors.success('🗑️ API key deleted successfully!'));
439
473
  }
440
474
  catch (error) {
@@ -564,7 +598,7 @@ mcpCommand
564
598
  delete toolData.maxConcurrentSessions;
565
599
  delete toolData.maxSessionDuration;
566
600
  }
567
- const tool = await apiClient.post('/api-keys/mcp/tools', toolData);
601
+ const tool = unwrapApiResponse(await apiClient.post(`${AUTH_API_KEYS_BASE}/mcp/tools`, toolData));
568
602
  console.log(colors.success('🤖 MCP tool registered successfully!'));
569
603
  console.log(colors.info('━'.repeat(50)));
570
604
  console.log(`${colors.highlight('Tool ID:')} ${colors.primary(tool.toolId)}`);
@@ -584,12 +618,12 @@ mcpCommand
584
618
  .option('--json', 'Output as JSON')
585
619
  .action(async (options) => {
586
620
  try {
587
- const tools = await apiClient.get('/api-keys/mcp/tools');
621
+ const tools = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/mcp/tools`));
588
622
  if (options.json) {
589
623
  console.log(JSON.stringify(tools, null, 2));
590
624
  return;
591
625
  }
592
- if (tools.length === 0) {
626
+ if (!Array.isArray(tools) || tools.length === 0) {
593
627
  console.log(colors.warning('⚠️ No MCP tools found'));
594
628
  console.log(colors.muted('Run: lanonasis api-keys mcp register-tool'));
595
629
  return;
@@ -644,7 +678,8 @@ mcpCommand
644
678
  };
645
679
  if (options.interactive || !requestData.toolId || !requestData.organizationId ||
646
680
  requestData.keyNames.length === 0 || !requestData.environment || !requestData.justification) {
647
- const tools = await apiClient.get('/api-keys/mcp/tools');
681
+ const mcpTools = unwrapApiResponse(await apiClient.get(`${AUTH_API_KEYS_BASE}/mcp/tools`));
682
+ const tools = Array.isArray(mcpTools) ? mcpTools : [];
648
683
  const answers = await inquirer.prompt([
649
684
  {
650
685
  type: 'select',
@@ -695,7 +730,7 @@ mcpCommand
695
730
  ]);
696
731
  requestData = { ...requestData, ...answers };
697
732
  }
698
- const response = await apiClient.post('/api-keys/mcp/request-access', requestData);
733
+ const response = unwrapApiResponse(await apiClient.post(`${AUTH_API_KEYS_BASE}/mcp/request-access`, requestData));
699
734
  console.log(colors.success('🔐 Access request created successfully!'));
700
735
  console.log(colors.info('━'.repeat(50)));
701
736
  console.log(`${colors.highlight('Request ID:')} ${colors.primary(response.requestId)}`);
@@ -721,7 +756,7 @@ analyticsCommand
721
756
  .option('--json', 'Output as JSON')
722
757
  .action(async (options) => {
723
758
  try {
724
- let url = '/api-keys/analytics/usage';
759
+ let url = `${AUTH_API_KEYS_BASE}/analytics/usage`;
725
760
  const params = new URLSearchParams();
726
761
  if (options.keyId)
727
762
  params.append('keyId', options.keyId);
@@ -730,12 +765,12 @@ analyticsCommand
730
765
  if (params.toString()) {
731
766
  url += `?${params.toString()}`;
732
767
  }
733
- const analytics = await apiClient.get(url);
768
+ const analytics = unwrapApiResponse(await apiClient.get(url));
734
769
  if (options.json) {
735
770
  console.log(JSON.stringify(analytics, null, 2));
736
771
  return;
737
772
  }
738
- if (analytics.length === 0) {
773
+ if (!Array.isArray(analytics) || analytics.length === 0) {
739
774
  console.log(chalk.yellow('No usage data found'));
740
775
  return;
741
776
  }
@@ -770,16 +805,16 @@ analyticsCommand
770
805
  .option('--json', 'Output as JSON')
771
806
  .action(async (options) => {
772
807
  try {
773
- let url = '/api-keys/analytics/security-events';
808
+ let url = `${AUTH_API_KEYS_BASE}/analytics/security-events`;
774
809
  if (options.severity) {
775
810
  url += `?severity=${options.severity}`;
776
811
  }
777
- const events = await apiClient.get(url);
812
+ const events = unwrapApiResponse(await apiClient.get(url));
778
813
  if (options.json) {
779
814
  console.log(JSON.stringify(events, null, 2));
780
815
  return;
781
816
  }
782
- if (events.length === 0) {
817
+ if (!Array.isArray(events) || events.length === 0) {
783
818
  console.log(colors.success('✅ No security events found'));
784
819
  return;
785
820
  }
@@ -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();
@@ -439,6 +439,12 @@ export function mcpCommands(program) {
439
439
  if (options.args) {
440
440
  try {
441
441
  args = JSON.parse(options.args);
442
+ // Validate that args is a plain object (not array or primitive)
443
+ if (typeof args !== 'object' || args === null || Array.isArray(args)) {
444
+ spinner.fail('Arguments must be a JSON object, not an array or primitive value');
445
+ console.log(chalk.yellow('Example: --args \'{"key": "value"}\''));
446
+ process.exit(1);
447
+ }
442
448
  }
443
449
  catch {
444
450
  spinner.fail('Invalid JSON arguments');
@@ -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();
@@ -103,12 +151,20 @@ const ensureBehaviorActions = (value, fieldName, options = {}) => {
103
151
  if (!tool) {
104
152
  throw new Error(`${fieldName}[${index}].tool is required`);
105
153
  }
106
- const parsed = { tool };
107
- if (entry.params !== undefined) {
108
- if (!isPlainObject(entry.params)) {
109
- throw new Error(`${fieldName}[${index}].params must be a JSON object`);
154
+ const outcome = typeof entry.outcome === 'string' ? entry.outcome.trim() : '';
155
+ if (!['success', 'partial', 'failed'].includes(outcome)) {
156
+ throw new Error(`${fieldName}[${index}].outcome must be success, partial, or failed`);
157
+ }
158
+ const parsed = {
159
+ tool,
160
+ outcome: outcome,
161
+ };
162
+ const rawParameters = entry.parameters ?? entry.params;
163
+ if (rawParameters !== undefined) {
164
+ if (!isPlainObject(rawParameters)) {
165
+ throw new Error(`${fieldName}[${index}].parameters must be a JSON object`);
110
166
  }
111
- parsed.params = entry.params;
167
+ parsed.parameters = rawParameters;
112
168
  }
113
169
  if (entry.timestamp !== undefined) {
114
170
  if (typeof entry.timestamp !== 'string' || !entry.timestamp.trim()) {
@@ -127,6 +183,33 @@ const ensureBehaviorActions = (value, fieldName, options = {}) => {
127
183
  return parsed;
128
184
  });
129
185
  };
186
+ const ensureCompletedSteps = (value, fieldName) => {
187
+ if (value === undefined) {
188
+ return [];
189
+ }
190
+ if (!Array.isArray(value)) {
191
+ throw new Error(`${fieldName} must be a JSON array`);
192
+ }
193
+ return value.map((entry, index) => {
194
+ if (typeof entry === 'string') {
195
+ const normalized = entry.trim();
196
+ if (!normalized) {
197
+ throw new Error(`${fieldName}[${index}] must be a non-empty string`);
198
+ }
199
+ return normalized;
200
+ }
201
+ if (isPlainObject(entry)) {
202
+ const tool = typeof entry.tool === 'string'
203
+ ? entry.tool?.trim()
204
+ : '';
205
+ if (!tool) {
206
+ throw new Error(`${fieldName}[${index}].tool must be a non-empty string`);
207
+ }
208
+ return tool;
209
+ }
210
+ throw new Error(`${fieldName}[${index}] must be a string or an object with a tool field`);
211
+ });
212
+ };
130
213
  const clampThreshold = (value) => {
131
214
  if (!Number.isFinite(value))
132
215
  return 0.55;
@@ -980,13 +1063,21 @@ export function memoryCommands(program) {
980
1063
  intelligence
981
1064
  .command('health-check')
982
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(', ')})`)
983
1070
  .option('--json', 'Output raw JSON payload')
984
1071
  .action(async (options) => {
985
1072
  try {
986
1073
  const spinner = ora('Running intelligence health check...').start();
987
1074
  const transport = await createIntelligenceTransport();
988
1075
  const userId = await resolveCurrentUserId();
989
- 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
+ });
990
1081
  spinner.stop();
991
1082
  printIntelligenceResult('🩺 Intelligence Health Check', result, options);
992
1083
  }
@@ -1001,6 +1092,9 @@ export function memoryCommands(program) {
1001
1092
  .description('Suggest tags for a memory')
1002
1093
  .argument('<memory-id>', 'Memory ID')
1003
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(', ')})`)
1004
1098
  .option('--json', 'Output raw JSON payload')
1005
1099
  .action(async (memoryId, options) => {
1006
1100
  try {
@@ -1014,6 +1108,7 @@ export function memoryCommands(program) {
1014
1108
  max_suggestions: maxSuggestions,
1015
1109
  include_existing_tags: true,
1016
1110
  response_format: 'json',
1111
+ ...buildIntelligenceContextPayload(options),
1017
1112
  });
1018
1113
  spinner.stop();
1019
1114
  printIntelligenceResult('🏷️ Tag Suggestions', result, options);
@@ -1030,6 +1125,10 @@ export function memoryCommands(program) {
1030
1125
  .argument('<memory-id>', 'Source memory ID')
1031
1126
  .option('--limit <number>', 'Maximum related memories', '5')
1032
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(', ')})`)
1033
1132
  .option('--json', 'Output raw JSON payload')
1034
1133
  .action(async (memoryId, options) => {
1035
1134
  try {
@@ -1042,6 +1141,7 @@ export function memoryCommands(program) {
1042
1141
  limit: Math.max(1, Math.min(20, parseInt(options.limit || '5', 10))),
1043
1142
  similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.7'))),
1044
1143
  response_format: 'json',
1144
+ ...buildIntelligenceContextPayload(options),
1045
1145
  });
1046
1146
  spinner.stop();
1047
1147
  printIntelligenceResult('🔗 Related Memories', result, options);
@@ -1057,6 +1157,10 @@ export function memoryCommands(program) {
1057
1157
  .description('Detect duplicate memory entries')
1058
1158
  .option('--threshold <number>', 'Similarity threshold (0-1)', '0.88')
1059
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(', ')})`)
1060
1164
  .option('--json', 'Output raw JSON payload')
1061
1165
  .action(async (options) => {
1062
1166
  try {
@@ -1068,6 +1172,7 @@ export function memoryCommands(program) {
1068
1172
  similarity_threshold: Math.max(0, Math.min(1, parseFloat(options.threshold || '0.88'))),
1069
1173
  max_pairs: Math.max(10, Math.min(500, parseInt(options.maxPairs || '100', 10))),
1070
1174
  response_format: 'json',
1175
+ ...buildIntelligenceContextPayload(options),
1071
1176
  });
1072
1177
  spinner.stop();
1073
1178
  printIntelligenceResult('🧬 Duplicate Detection', result, options);
@@ -1084,6 +1189,10 @@ export function memoryCommands(program) {
1084
1189
  .option('--topic <topic>', 'Optional topic filter')
1085
1190
  .option('--type <type>', `Optional memory type filter (${MEMORY_TYPE_CHOICES.join(', ')})`)
1086
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(', ')})`)
1087
1196
  .option('--json', 'Output raw JSON payload')
1088
1197
  .action(async (options) => {
1089
1198
  try {
@@ -1100,6 +1209,7 @@ export function memoryCommands(program) {
1100
1209
  memory_type: memoryType,
1101
1210
  max_memories: Math.max(5, Math.min(200, parseInt(options.maxMemories || '50', 10))),
1102
1211
  response_format: 'json',
1212
+ ...buildIntelligenceContextPayload(options),
1103
1213
  });
1104
1214
  spinner.stop();
1105
1215
  printIntelligenceResult('💡 Memory Insights', result, options);
@@ -1114,6 +1224,10 @@ export function memoryCommands(program) {
1114
1224
  .command('analyze-patterns')
1115
1225
  .description('Analyze memory usage patterns')
1116
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(', ')})`)
1117
1231
  .option('--json', 'Output raw JSON payload')
1118
1232
  .action(async (options) => {
1119
1233
  try {
@@ -1124,6 +1238,7 @@ export function memoryCommands(program) {
1124
1238
  user_id: userId,
1125
1239
  time_range_days: Math.max(1, Math.min(365, parseInt(options.days || '30', 10))),
1126
1240
  response_format: 'json',
1241
+ ...buildIntelligenceContextPayload(options),
1127
1242
  });
1128
1243
  spinner.stop();
1129
1244
  printIntelligenceResult('📈 Pattern Analysis', result, options);
@@ -1215,7 +1330,7 @@ export function memoryCommands(program) {
1215
1330
  .description('Suggest next actions from learned behavior patterns')
1216
1331
  .requiredOption('--task <text>', 'Current task description')
1217
1332
  .option('--state <json>', 'Additional current state JSON object')
1218
- .option('--completed-steps <json>', 'Completed steps JSON array')
1333
+ .option('--completed-steps <json>', 'Completed steps JSON array (strings preferred)')
1219
1334
  .option('--max-suggestions <number>', 'Maximum suggestions', '3')
1220
1335
  .option('--json', 'Output raw JSON payload')
1221
1336
  .action(async (options) => {
@@ -1226,9 +1341,7 @@ export function memoryCommands(program) {
1226
1341
  const parsedState = parseJsonOption(options.state, '--state');
1227
1342
  const state = ensureJsonObject(parsedState, '--state') || {};
1228
1343
  const parsedCompletedSteps = parseJsonOption(options.completedSteps, '--completed-steps');
1229
- const completedSteps = parsedCompletedSteps === undefined
1230
- ? undefined
1231
- : ensureBehaviorActions(parsedCompletedSteps, '--completed-steps', { allowEmpty: true });
1344
+ const completedSteps = ensureCompletedSteps(parsedCompletedSteps, '--completed-steps');
1232
1345
  const result = await postIntelligenceEndpoint(transport, '/intelligence/behavior-suggest', {
1233
1346
  user_id: userId,
1234
1347
  current_state: {
package/dist/index.js CHANGED
@@ -280,10 +280,16 @@ authCmd
280
280
  const profileClient = new APIClient();
281
281
  profileClient.noExit = true;
282
282
  const profile = await profileClient.getUserProfile();
283
+ await cliConfig.updateCurrentUserProfile(profile);
284
+ const organizationId = profile.organization_id || profile.organizationId;
283
285
  console.log(`Email: ${profile.email}`);
284
286
  if (profile.name)
285
287
  console.log(`Name: ${profile.name}`);
288
+ if (organizationId)
289
+ console.log(`Organization: ${organizationId}`);
286
290
  console.log(`Role: ${profile.role}`);
291
+ if (profile.plan)
292
+ console.log(`Plan: ${profile.plan}`);
287
293
  if (profile.provider)
288
294
  console.log(`Provider: ${profile.provider}`);
289
295
  if (profile.last_sign_in_at) {
@@ -736,13 +742,21 @@ program
736
742
  const profileClient = new APIClient();
737
743
  profileClient.noExit = true;
738
744
  const profile = await profileClient.getUserProfile();
745
+ await cliConfig.updateCurrentUserProfile(profile);
746
+ const organizationId = profile.organization_id || profile.organizationId;
739
747
  console.log(chalk.blue.bold('👤 Current User'));
740
748
  console.log('━'.repeat(40));
741
749
  console.log(`Email: ${chalk.white(profile.email)}`);
742
750
  if (profile.name) {
743
751
  console.log(`Name: ${chalk.white(profile.name)}`);
744
752
  }
753
+ if (organizationId) {
754
+ console.log(`Organization: ${chalk.white(organizationId)}`);
755
+ }
745
756
  console.log(`Role: ${chalk.white(profile.role)}`);
757
+ if (profile.plan) {
758
+ console.log(`Plan: ${chalk.white(profile.plan)}`);
759
+ }
746
760
  if (profile.provider) {
747
761
  console.log(`Provider: ${chalk.white(profile.provider)}`);
748
762
  }
@@ -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;
@@ -169,7 +172,10 @@ export interface UserProfile {
169
172
  email: string;
170
173
  name: string | null;
171
174
  avatar_url: string | null;
175
+ organization_id?: string | null;
176
+ organizationId?: string | null;
172
177
  role: string;
178
+ plan?: string | null;
173
179
  provider: string | null;
174
180
  project_scope: string | null;
175
181
  platform: string | null;
@@ -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;
@@ -52,6 +54,9 @@ export declare class CLIConfig {
52
54
  private vendorKeyCache?;
53
55
  private isLegacyHashedCredential;
54
56
  private getLegacyHashedVendorKeyReason;
57
+ private normalizeOptionalString;
58
+ private extractOrganizationId;
59
+ private buildUserProfile;
55
60
  constructor();
56
61
  private getApiKeyStorage;
57
62
  /**
@@ -120,12 +125,14 @@ export declare class CLIConfig {
120
125
  getToken(): string | undefined;
121
126
  getAuthMethod(): string | undefined;
122
127
  getCurrentUser(): Promise<UserProfile | undefined>;
128
+ updateCurrentUserProfile(profile: Record<string, unknown>): Promise<void>;
123
129
  isAuthenticated(): Promise<boolean>;
124
130
  logout(): Promise<void>;
125
131
  clear(): Promise<void>;
126
132
  exists(): Promise<boolean>;
127
133
  validateStoredCredentials(): Promise<boolean>;
128
134
  refreshTokenIfNeeded(): Promise<void>;
135
+ private refreshViaOAuthTokenEndpoint;
129
136
  clearInvalidCredentials(): Promise<void>;
130
137
  incrementFailureCount(): Promise<void>;
131
138
  resetFailureCount(): Promise<void>;
@@ -21,6 +21,35 @@ export class CLIConfig {
21
21
  getLegacyHashedVendorKeyReason() {
22
22
  return 'Stored vendor key is in legacy hashed format. Run "lanonasis auth login --vendor-key <your-key>" to refresh secure storage.';
23
23
  }
24
+ normalizeOptionalString(value) {
25
+ if (typeof value === 'string') {
26
+ const trimmed = value.trim();
27
+ return trimmed.length > 0 ? trimmed : undefined;
28
+ }
29
+ if (value === null || value === undefined) {
30
+ return undefined;
31
+ }
32
+ return String(value);
33
+ }
34
+ extractOrganizationId(source) {
35
+ return this.normalizeOptionalString(source.organization_id)
36
+ ?? this.normalizeOptionalString(source.organizationId);
37
+ }
38
+ buildUserProfile(source, existing) {
39
+ const email = this.normalizeOptionalString(source.email) ?? existing?.email ?? '';
40
+ const organization_id = this.extractOrganizationId(source) ?? existing?.organization_id ?? '';
41
+ const role = this.normalizeOptionalString(source.role) ?? existing?.role ?? '';
42
+ const plan = this.normalizeOptionalString(source.plan) ?? existing?.plan ?? '';
43
+ if (!email && !organization_id && !role && !plan) {
44
+ return existing;
45
+ }
46
+ return {
47
+ email,
48
+ organization_id,
49
+ role,
50
+ plan
51
+ };
52
+ }
24
53
  constructor() {
25
54
  this.configDir = path.join(os.homedir(), '.maas');
26
55
  this.configPath = path.join(this.configDir, 'config.json');
@@ -897,12 +926,7 @@ export class CLIConfig {
897
926
  this.config.tokenExpiry = decoded.exp;
898
927
  }
899
928
  // Store user info
900
- this.config.user = {
901
- email: String(decoded.email || ''),
902
- organization_id: String(decoded.organizationId || ''),
903
- role: String(decoded.role || ''),
904
- plan: String(decoded.plan || '')
905
- };
929
+ this.config.user = this.buildUserProfile(decoded, this.config.user);
906
930
  }
907
931
  catch {
908
932
  // Invalid token, don't store user info or expiry
@@ -921,6 +945,23 @@ export class CLIConfig {
921
945
  async getCurrentUser() {
922
946
  return this.config.user;
923
947
  }
948
+ async updateCurrentUserProfile(profile) {
949
+ const nextUser = this.buildUserProfile(profile, this.config.user);
950
+ if (!nextUser) {
951
+ return;
952
+ }
953
+ const currentUser = this.config.user;
954
+ const changed = !currentUser
955
+ || currentUser.email !== nextUser.email
956
+ || currentUser.organization_id !== nextUser.organization_id
957
+ || currentUser.role !== nextUser.role
958
+ || currentUser.plan !== nextUser.plan;
959
+ if (!changed) {
960
+ return;
961
+ }
962
+ this.config.user = nextUser;
963
+ await this.save();
964
+ }
924
965
  async isAuthenticated() {
925
966
  // Attempt refresh for OAuth sessions before checks (prevents intermittent auth dropouts).
926
967
  // This is safe to call even when not using OAuth; it will no-op.
@@ -1242,9 +1283,9 @@ export class CLIConfig {
1242
1283
  if (String(this.config.authMethod || '').toLowerCase() === 'vendor_key') {
1243
1284
  return;
1244
1285
  }
1286
+ const refreshToken = this.get('refresh_token');
1245
1287
  // OAuth token refresh (opaque tokens + refresh_token + token_expires_at)
1246
- if (this.config.authMethod === 'oauth') {
1247
- const refreshToken = this.get('refresh_token');
1288
+ if (this.config.authMethod === 'oauth' || this.config.authMethod === 'oauth2') {
1248
1289
  if (!refreshToken) {
1249
1290
  return;
1250
1291
  }
@@ -1269,37 +1310,7 @@ export class CLIConfig {
1269
1310
  return;
1270
1311
  }
1271
1312
  await this.discoverServices();
1272
- const authBase = this.getDiscoveredApiUrl();
1273
- const resp = await axios.post(`${authBase}/oauth/token`, {
1274
- grant_type: 'refresh_token',
1275
- refresh_token: refreshToken,
1276
- client_id: 'lanonasis-cli'
1277
- }, {
1278
- headers: { 'Content-Type': 'application/json' },
1279
- timeout: 10000,
1280
- proxy: false
1281
- });
1282
- // Some gateways wrap responses as `{ data: { ... } }`.
1283
- const raw = resp?.data;
1284
- const payload = raw && typeof raw === 'object' && raw.data && typeof raw.data === 'object'
1285
- ? raw.data
1286
- : raw;
1287
- const accessToken = payload?.access_token ?? payload?.token;
1288
- const refreshedRefreshToken = payload?.refresh_token;
1289
- const expiresIn = payload?.expires_in;
1290
- if (typeof accessToken !== 'string' || accessToken.length === 0) {
1291
- throw new Error('Token refresh response missing access_token');
1292
- }
1293
- // setToken() assumes JWT by default; ensure authMethod stays oauth after storing.
1294
- await this.setToken(accessToken);
1295
- this.config.authMethod = 'oauth';
1296
- if (typeof refreshedRefreshToken === 'string' && refreshedRefreshToken.length > 0) {
1297
- this.config.refresh_token = refreshedRefreshToken;
1298
- }
1299
- if (typeof expiresIn === 'number' && Number.isFinite(expiresIn)) {
1300
- this.config.token_expires_at = Date.now() + (expiresIn * 1000);
1301
- }
1302
- await this.save().catch(() => { });
1313
+ await this.refreshViaOAuthTokenEndpoint(refreshToken, this.getDiscoveredApiUrl(), 'oauth');
1303
1314
  return;
1304
1315
  }
1305
1316
  // Check if token is JWT and if it's close to expiry
@@ -1318,20 +1329,11 @@ export class CLIConfig {
1318
1329
  const exp = typeof decoded.exp === 'number' ? decoded.exp : 0;
1319
1330
  // Refresh if token expires within 5 minutes
1320
1331
  if (exp > 0 && (exp - now) < 300) {
1321
- // Import axios dynamically
1322
- await this.discoverServices();
1323
- const authBase = this.config.discoveredServices?.auth_base || 'https://auth.lanonasis.com';
1324
- // Attempt token refresh
1325
- const response = await axios.post(`${authBase}/v1/auth/refresh`, {}, {
1326
- headers: {
1327
- 'Authorization': `Bearer ${token}`,
1328
- 'X-Project-Scope': 'lanonasis-maas'
1329
- },
1330
- timeout: 10000
1331
- });
1332
- if (response.data.token) {
1333
- await this.setToken(response.data.token);
1332
+ if (!refreshToken) {
1333
+ return;
1334
1334
  }
1335
+ await this.discoverServices();
1336
+ await this.refreshViaOAuthTokenEndpoint(refreshToken, this.getDiscoveredApiUrl(), 'jwt');
1335
1337
  }
1336
1338
  }
1337
1339
  catch (err) {
@@ -1342,6 +1344,36 @@ export class CLIConfig {
1342
1344
  }
1343
1345
  }
1344
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
+ }
1345
1377
  async clearInvalidCredentials() {
1346
1378
  this.config.token = undefined;
1347
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
  }
@@ -51,9 +51,21 @@ export class ConnectionManagerImpl {
51
51
  try {
52
52
  // Load persisted configuration first
53
53
  await this.loadConfig();
54
- // First, try to detect the server path
54
+ // Prefer a configured path only if the file exists (ignore stale machine-specific commits)
55
55
  const configuredPath = this.config.localServerPath?.trim();
56
- const serverPath = configuredPath || (await this.detectServerPath());
56
+ let serverPath = null;
57
+ if (configuredPath) {
58
+ try {
59
+ await fs.access(configuredPath);
60
+ serverPath = configuredPath;
61
+ }
62
+ catch {
63
+ serverPath = await this.detectServerPath();
64
+ }
65
+ }
66
+ else {
67
+ serverPath = await this.detectServerPath();
68
+ }
57
69
  if (!serverPath) {
58
70
  return {
59
71
  success: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.11",
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",