@lanonasis/cli 3.9.3 → 3.9.5

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
@@ -16,6 +16,11 @@
16
16
  - Path translation handles endpoint differences between API and MCP servers
17
17
  - Vendor key authentication continues to route to main API server
18
18
 
19
+ - **Frozen Terminal During Text Input**: Fixed SSE/WebSocket event handlers interfering with inline text editor
20
+ - Real-time update messages (📡) now only display in verbose mode
21
+ - Prevents terminal freeze during interactive prompts (memory create, update)
22
+ - Raw terminal mode no longer conflicts with background MCP events
23
+
19
24
  - **Missing CLI Option**: The `--vendor-key` option was defined in code but not exposed in CLI
20
25
  - Now properly registered in command-line interface
21
26
 
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # @lanonasis/cli v3.9.0 - Enterprise Security & Professional UX
1
+ # @lanonasis/cli v3.9.3 - Enterprise Security & Professional UX
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**: Professional CLI UX with seamless inline text editing, intelligent MCP connection management, and first-run onboarding. Advanced Model Context Protocol (MCP) support with multi-server connections, enhanced error handling, and enterprise-grade transport protocols. Revolutionary interactive CLI experience with guided workflows and **Golden Contract compliance**.
8
+ 🎉 **NEW IN v3.9.3**: Fixed JWT authentication routing for username/password login, resolved frozen terminal during interactive input, and added non-interactive vendor key authentication (`-k` flag). Professional CLI UX with seamless inline text editing, intelligent MCP connection management, and first-run onboarding.
9
9
 
10
10
  ## 🚀 Quick Start
11
11
 
@@ -151,7 +151,11 @@ onasis login --vendor-key pk_xxxxx.sk_xxxxx // ✅ Automatically hashed
151
151
  Best for API integrations and automation. Copy the vendor key value exactly as shown in your LanOnasis dashboard (keys may vary in format):
152
152
 
153
153
  ```bash
154
- onasis login --vendor-key <your-vendor-key>
154
+ # Full option
155
+ onasis auth login --vendor-key <your-vendor-key>
156
+
157
+ # Short form (for scripts and CI/CD)
158
+ onasis auth login -k <your-vendor-key>
155
159
  ```
156
160
 
157
161
  ### 2. OAuth Browser Authentication
@@ -179,6 +183,13 @@ onasis auth status # Check current authentication
179
183
  onasis auth logout # Logout from current session
180
184
  ```
181
185
 
186
+ **Auth Login Options:**
187
+ | Short | Long | Description |
188
+ |-------|------|-------------|
189
+ | `-k` | `--vendor-key <key>` | Authenticate with vendor key (non-interactive) |
190
+ | `-e` | `--email <email>` | Email for credentials login |
191
+ | `-p` | `--password <pass>` | Password for credentials login |
192
+
182
193
  ## 💻 Shell Completions
183
194
 
184
195
  ### Installation Guide
@@ -224,24 +235,74 @@ onasis quickstart # Essential commands reference
224
235
 
225
236
  ```bash
226
237
  # List memories
227
- onasis memory list
228
- onasis memory list --memory-type context --limit 20 --sort-by created_at
238
+ onasis memory list # or: onasis memory ls
239
+ onasis memory list --type context --limit 20
240
+
241
+ # Create memories (non-interactive)
242
+ onasis memory create -t "Project Notes" -c "Important information"
243
+ onasis memory create -t "Reference" --type reference --tags "docs,api"
244
+
245
+ # Create memory via JSON payload
246
+ onasis memory create --json '{"title":"Design decisions","type":"project","content":"Summary...","tags":["architecture","design"]}'
229
247
 
230
- # Create memories
231
- onasis memory create --title "Project Notes" --content "Important information"
232
- onasis memory create --title "Reference" --memory-type reference --tags "docs,api"
248
+ # Create memory from a file
249
+ onasis memory create -t "Session notes" --content-file ./notes.md
250
+
251
+ # Create memories (interactive)
252
+ onasis memory create -i # Interactive mode with inline editor
253
+ onasis memory create # Prompts for missing fields
233
254
 
234
255
  # Search memories
235
256
  onasis memory search "api integration"
236
- onasis memory search "meeting notes" --memory-types context,reference
257
+ onasis memory search "meeting notes" --type context
237
258
 
238
259
  # Memory operations
239
260
  onasis memory get <id> # Get specific memory
240
- onasis memory update <id> --title "New Title"
261
+ onasis memory update <id> -t "New Title" # Update title
262
+ onasis memory update <id> -i # Interactive update
241
263
  onasis memory delete <id> # Delete memory
242
264
  onasis memory stats # Memory statistics
243
265
  ```
244
266
 
267
+ #### `onasis memory save-session`
268
+
269
+ Save the current CLI session context (CWD + git branch/status + changed files) as a memory entry so you can persist what you worked on and pick it up later.
270
+
271
+ - `--test-summary`: Stores a human-readable test result summary (e.g., `Vitest: 53 passed, 1 skipped`) in the saved session memory.
272
+ - `--title`: Sets the memory title (default: `Session summary`).
273
+ - `--type`: Sets the memory type (default: `project`).
274
+ - `--tags`: Comma-separated tags for session metadata (default: `session,cli`).
275
+
276
+ **Examples**
277
+ ```bash
278
+ onasis memory save-session --test-summary "Vitest: 53 passed, 1 skipped"
279
+ onasis memory save-session --title "API client fixes" --type project --tags "session,cli,testing"
280
+ ```
281
+
282
+ See **Session management** below for related commands.
283
+
284
+ #### Session management
285
+
286
+ Sessions are stored as memory entries (tagged `session,cli` by default). Related commands:
287
+
288
+ ```bash
289
+ onasis memory save-session
290
+ onasis memory list-sessions
291
+ onasis memory load-session <id>
292
+ onasis memory delete-session <id>
293
+ ```
294
+
295
+ **Create/Update Options:**
296
+ | Short | Long | Description |
297
+ |-------|------|-------------|
298
+ | `-t` | `--title` | Memory title |
299
+ | `-c` | `--content` | Memory content |
300
+ | `-i` | `--interactive` | Interactive mode |
301
+ | | `--type` | Memory type (context, project, knowledge, etc.) |
302
+ | | `--tags` | Comma-separated tags |
303
+ | | `--json` | JSON payload (title, content, type/memory_type, tags, topic_id) |
304
+ | | `--content-file` | Read content from a file |
305
+
245
306
  ### Topic Management
246
307
 
247
308
  ```bash
@@ -612,4 +673,3 @@ The CLI will show the authorization URL - copy and paste it into your browser ma
612
673
 
613
674
  **Token refresh failed:**
614
675
  Run `onasis auth login` to re-authenticate.
615
-
@@ -11,11 +11,13 @@ export function configCommands(program) {
11
11
  .action(async (key, value) => {
12
12
  const config = new CLIConfig();
13
13
  await config.init();
14
+ let shouldSave = true;
14
15
  // Handle special cases
15
16
  switch (key) {
16
17
  case 'api-url':
17
18
  await config.setApiUrl(value);
18
19
  console.log(chalk.green('✓ API URL updated:'), value);
20
+ shouldSave = false; // setApiUrl already persists
19
21
  break;
20
22
  case 'ai-integration':
21
23
  if (value === 'claude-mcp') {
@@ -42,11 +44,19 @@ export function configCommands(program) {
42
44
  config.set('mcpServerUrl', value);
43
45
  console.log(chalk.green('✓ MCP server URL updated:'), value);
44
46
  break;
47
+ case 'force-api':
48
+ config.set('forceApi', value === 'true');
49
+ config.set('connectionTransport', value === 'true' ? 'api' : 'auto');
50
+ console.log(chalk.green('✓ Force direct API mode:'), value === 'true' ? 'enabled' : 'disabled');
51
+ break;
45
52
  default:
46
53
  // Generic config set
47
54
  config.set(key, value);
48
55
  console.log(chalk.green(`✓ ${key} set to:`), value);
49
56
  }
57
+ if (shouldSave) {
58
+ await config.save();
59
+ }
50
60
  });
51
61
  // Generic config get command
52
62
  program
@@ -101,6 +111,7 @@ export function configCommands(program) {
101
111
  { key: 'mcp-use-remote', description: 'Use remote MCP server', current: config.get('mcpUseRemote') || false },
102
112
  { key: 'mcp-server-path', description: 'Local MCP server path', current: config.get('mcpServerPath') || 'default' },
103
113
  { key: 'mcp-server-url', description: 'Remote MCP server URL', current: config.get('mcpServerUrl') || 'https://mcp.lanonasis.com' },
114
+ { key: 'force-api', description: 'Force direct API transport', current: config.get('forceApi') || false },
104
115
  { key: 'mcpEnabled', description: 'MCP integration enabled', current: config.get('mcpEnabled') || false }
105
116
  ];
106
117
  configOptions.forEach(opt => {
@@ -8,6 +8,30 @@ import { apiClient } from '../utils/api.js';
8
8
  import { formatBytes, truncateText } from '../utils/formatting.js';
9
9
  import { CLIConfig } from '../utils/config.js';
10
10
  import { createTextInputHandler } from '../ux/index.js';
11
+ import * as fs from 'fs/promises';
12
+ import { exec as execCb } from 'node:child_process';
13
+ import { promisify } from 'node:util';
14
+ const exec = promisify(execCb);
15
+ const MEMORY_TYPE_CHOICES = [
16
+ 'context',
17
+ 'project',
18
+ 'knowledge',
19
+ 'reference',
20
+ 'personal',
21
+ 'workflow',
22
+ ];
23
+ const coerceMemoryType = (value) => {
24
+ if (typeof value !== 'string')
25
+ return undefined;
26
+ const normalized = value.trim().toLowerCase();
27
+ // Backward compatibility for older docs/examples.
28
+ if (normalized === 'conversation')
29
+ return 'context';
30
+ if (MEMORY_TYPE_CHOICES.includes(normalized)) {
31
+ return normalized;
32
+ }
33
+ return undefined;
34
+ };
11
35
  const resolveInputMode = async () => {
12
36
  const config = new CLIConfig();
13
37
  await config.init();
@@ -41,13 +65,53 @@ export function memoryCommands(program) {
41
65
  .description('Create a new memory entry')
42
66
  .option('-t, --title <title>', 'memory title')
43
67
  .option('-c, --content <content>', 'memory content')
44
- .option('--type <type>', 'memory type (conversation, knowledge, project, context, reference)', 'context')
68
+ .option('--type <type>', `memory type (${MEMORY_TYPE_CHOICES.join(', ')})`)
45
69
  .option('--tags <tags>', 'comma-separated tags')
46
70
  .option('--topic-id <id>', 'topic ID')
47
71
  .option('-i, --interactive', 'interactive mode')
72
+ .option('--json <json>', 'JSON payload (title, content, type/memory_type, tags[], topic_id)')
73
+ .option('--content-file <path>', 'Read memory content from a file (overrides --content)')
48
74
  .action(async (options) => {
49
75
  try {
50
- let { title, content, type, tags, topicId, interactive } = options;
76
+ let { title, content, type, tags, topicId, interactive, json, contentFile } = options;
77
+ // 1) JSON payload (optional)
78
+ if (json) {
79
+ let parsed;
80
+ try {
81
+ parsed = JSON.parse(json);
82
+ }
83
+ catch (err) {
84
+ const msg = err instanceof Error ? err.message : 'Invalid JSON';
85
+ throw new Error(`Invalid --json payload: ${msg}`);
86
+ }
87
+ if (!title && typeof parsed.title === 'string')
88
+ title = parsed.title;
89
+ if (!content && typeof parsed.content === 'string')
90
+ content = parsed.content;
91
+ const parsedType = parsed.memory_type ?? parsed.type;
92
+ if (!type && parsedType !== undefined) {
93
+ const coerced = coerceMemoryType(parsedType);
94
+ if (!coerced) {
95
+ throw new Error(`Invalid memory type in --json payload. Expected one of: ${MEMORY_TYPE_CHOICES.join(', ')}`);
96
+ }
97
+ type = coerced;
98
+ }
99
+ if (!tags) {
100
+ if (Array.isArray(parsed.tags)) {
101
+ tags = parsed.tags.map((t) => String(t)).join(',');
102
+ }
103
+ else if (typeof parsed.tags === 'string') {
104
+ tags = parsed.tags;
105
+ }
106
+ }
107
+ const parsedTopic = parsed.topic_id ?? parsed.topicId;
108
+ if (!topicId && typeof parsedTopic === 'string')
109
+ topicId = parsedTopic;
110
+ }
111
+ // 2) Content file (optional)
112
+ if (contentFile) {
113
+ content = await fs.readFile(contentFile, 'utf-8');
114
+ }
51
115
  if (interactive || (!title || !content)) {
52
116
  const inputMode = await resolveInputMode();
53
117
  const answers = await inquirer.prompt([
@@ -62,7 +126,7 @@ export function memoryCommands(program) {
62
126
  type: 'list',
63
127
  name: 'type',
64
128
  message: 'Memory type:',
65
- choices: ['conversation', 'knowledge', 'project', 'context', 'reference'],
129
+ choices: [...MEMORY_TYPE_CHOICES],
66
130
  default: type || 'context',
67
131
  },
68
132
  {
@@ -86,11 +150,12 @@ export function memoryCommands(program) {
86
150
  if (!content || content.trim().length === 0) {
87
151
  throw new Error('Content is required');
88
152
  }
153
+ const resolvedType = type ?? 'context';
89
154
  const spinner = ora('Creating memory...').start();
90
155
  const memoryData = {
91
156
  title,
92
157
  content,
93
- memory_type: type
158
+ memory_type: resolvedType
94
159
  };
95
160
  if (tags) {
96
161
  memoryData.tags = tags.split(',').map((tag) => tag.trim()).filter(Boolean);
@@ -115,6 +180,211 @@ export function memoryCommands(program) {
115
180
  process.exit(1);
116
181
  }
117
182
  });
183
+ // Save current working session context as a memory
184
+ program
185
+ .command('save-session')
186
+ .description('Save current session context (git branch/status + optional test summary) as a memory')
187
+ .option('-t, --title <title>', 'memory title', 'Session summary')
188
+ .option('--type <type>', `memory type (${MEMORY_TYPE_CHOICES.join(', ')})`, 'project')
189
+ .option('--tags <tags>', 'comma-separated tags', 'session,cli')
190
+ .option('--test-summary <text>', 'Optional test summary to include')
191
+ .action(async (options) => {
192
+ try {
193
+ const spinner = ora('Collecting session info...').start();
194
+ const cwd = process.cwd();
195
+ const runGit = async (cmd) => {
196
+ try {
197
+ const { stdout } = await exec(cmd, { cwd });
198
+ return String(stdout || '').trim();
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ };
204
+ const branch = await runGit('git rev-parse --abbrev-ref HEAD');
205
+ const status = await runGit('git status --porcelain');
206
+ const changedFiles = status
207
+ ? status
208
+ .split('\n')
209
+ .map((line) => line.trim())
210
+ .filter(Boolean)
211
+ .map((line) => line.replace(/^.. /, ''))
212
+ : [];
213
+ const lines = [];
214
+ lines.push(`# Session Summary`);
215
+ lines.push('');
216
+ lines.push(`- Date: ${new Date().toISOString()}`);
217
+ lines.push(`- CWD: ${cwd}`);
218
+ if (branch)
219
+ lines.push(`- Git branch: ${branch}`);
220
+ lines.push('');
221
+ lines.push('## Changes');
222
+ if (changedFiles.length === 0) {
223
+ lines.push('No uncommitted changes detected (or git not available).');
224
+ }
225
+ else {
226
+ lines.push(changedFiles.map((f) => `- ${f}`).join('\n'));
227
+ }
228
+ lines.push('');
229
+ if (options.testSummary) {
230
+ lines.push('## Test Summary');
231
+ lines.push(options.testSummary);
232
+ lines.push('');
233
+ }
234
+ spinner.text = 'Saving session memory...';
235
+ const resolvedType = coerceMemoryType(options.type) ?? 'project';
236
+ const memoryData = {
237
+ title: options.title || 'Session summary',
238
+ content: lines.join('\n'),
239
+ memory_type: resolvedType
240
+ };
241
+ if (options.tags) {
242
+ memoryData.tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
243
+ }
244
+ const memory = await apiClient.createMemory(memoryData);
245
+ spinner.succeed('Session saved');
246
+ console.log();
247
+ console.log(chalk.green('✓ Memory created:'));
248
+ console.log(` ID: ${chalk.cyan(memory.id)}`);
249
+ console.log(` Title: ${memory.title}`);
250
+ console.log(` Type: ${memory.memory_type}`);
251
+ }
252
+ catch (error) {
253
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
254
+ console.error(chalk.red('✖ Failed to save session:'), errorMessage);
255
+ process.exit(1);
256
+ }
257
+ });
258
+ // Session management helpers (sessions are stored as memory entries tagged `session,cli` by default)
259
+ program
260
+ .command('list-sessions')
261
+ .description('List saved CLI sessions (memories tagged session,cli by default)')
262
+ .option('-p, --page <page>', 'page number', '1')
263
+ .option('-l, --limit <limit>', 'number of entries per page', '20')
264
+ .option('--type <type>', 'filter by memory type', 'project')
265
+ .option('--tags <tags>', 'filter by tags (comma-separated)', 'session,cli')
266
+ .option('--sort <field>', 'sort by field (created_at, updated_at, title, last_accessed)', 'created_at')
267
+ .option('--order <order>', 'sort order (asc, desc)', 'desc')
268
+ .action(async (options) => {
269
+ try {
270
+ const spinner = ora('Fetching sessions...').start();
271
+ const params = {
272
+ page: parseInt(options.page || '1'),
273
+ limit: parseInt(options.limit || '20'),
274
+ sort: options.sort || 'created_at',
275
+ order: options.order || 'desc'
276
+ };
277
+ if (options.type)
278
+ params.memory_type = options.type;
279
+ if (options.tags)
280
+ params.tags = options.tags;
281
+ const result = await apiClient.getMemories(params);
282
+ spinner.stop();
283
+ const memories = result.memories || result.data || [];
284
+ if (memories.length === 0) {
285
+ console.log(chalk.yellow('No sessions found'));
286
+ return;
287
+ }
288
+ console.log(chalk.blue.bold(`\n📚 Sessions (${result.pagination.total} total)`));
289
+ console.log(chalk.gray(`Page ${result.pagination.page || 1} of ${result.pagination.pages || Math.ceil(result.pagination.total / result.pagination.limit)}`));
290
+ console.log();
291
+ const outputFormat = process.env.CLI_OUTPUT_FORMAT || 'table';
292
+ if (outputFormat === 'json') {
293
+ console.log(JSON.stringify(result, null, 2));
294
+ return;
295
+ }
296
+ const tableData = memories.map((memory) => [
297
+ truncateText(memory.title, 30),
298
+ memory.memory_type,
299
+ memory.tags.slice(0, 3).join(', '),
300
+ format(new Date(memory.created_at), 'MMM dd, yyyy'),
301
+ memory.access_count
302
+ ]);
303
+ const tableConfig = {
304
+ header: ['Title', 'Type', 'Tags', 'Created', 'Access'],
305
+ columnDefault: {
306
+ width: 20,
307
+ wrapWord: true
308
+ },
309
+ columns: [
310
+ { width: 30 },
311
+ { width: 12 },
312
+ { width: 20 },
313
+ { width: 12 },
314
+ { width: 8 }
315
+ ]
316
+ };
317
+ console.log(table([tableConfig.header, ...tableData], {
318
+ columnDefault: tableConfig.columnDefault,
319
+ columns: tableConfig.columns
320
+ }));
321
+ if (result.pagination.pages > 1) {
322
+ console.log(chalk.gray(`\nUse --page ${result.pagination.page + 1} for next page`));
323
+ }
324
+ }
325
+ catch (error) {
326
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
327
+ console.error(chalk.red('✖ Failed to list sessions:'), errorMessage);
328
+ process.exit(1);
329
+ }
330
+ });
331
+ program
332
+ .command('load-session')
333
+ .description('Load a saved session by memory ID (prints the saved session context)')
334
+ .argument('<id>', 'session memory ID')
335
+ .action(async (id) => {
336
+ try {
337
+ const spinner = ora('Loading session...').start();
338
+ const memory = await apiClient.getMemory(id);
339
+ spinner.stop();
340
+ const outputFormat = process.env.CLI_OUTPUT_FORMAT || 'text';
341
+ if (outputFormat === 'json') {
342
+ console.log(JSON.stringify(memory, null, 2));
343
+ return;
344
+ }
345
+ console.log(chalk.blue.bold('\n📌 Session'));
346
+ console.log(chalk.gray(`${memory.title} (${memory.id})`));
347
+ console.log();
348
+ console.log(memory.content);
349
+ }
350
+ catch (error) {
351
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
352
+ console.error(chalk.red('✖ Failed to load session:'), errorMessage);
353
+ process.exit(1);
354
+ }
355
+ });
356
+ program
357
+ .command('delete-session')
358
+ .description('Delete a saved session by memory ID')
359
+ .argument('<id>', 'session memory ID')
360
+ .option('-f, --force', 'skip confirmation')
361
+ .action(async (id, options) => {
362
+ try {
363
+ if (!options.force) {
364
+ const memory = await apiClient.getMemory(id);
365
+ const answer = await inquirer.prompt([
366
+ {
367
+ type: 'confirm',
368
+ name: 'confirm',
369
+ message: `Are you sure you want to delete session "${memory.title}"?`,
370
+ default: false
371
+ }
372
+ ]);
373
+ if (!answer.confirm) {
374
+ console.log(chalk.yellow('Deletion cancelled'));
375
+ return;
376
+ }
377
+ }
378
+ const spinner = ora('Deleting session...').start();
379
+ await apiClient.deleteMemory(id);
380
+ spinner.succeed('Session deleted successfully');
381
+ }
382
+ catch (error) {
383
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
384
+ console.error(chalk.red('✖ Failed to delete session:'), errorMessage);
385
+ process.exit(1);
386
+ }
387
+ });
118
388
  // List memories
119
389
  program
120
390
  .command('list')
@@ -294,7 +564,7 @@ export function memoryCommands(program) {
294
564
  .argument('<id>', 'memory ID')
295
565
  .option('-t, --title <title>', 'new title')
296
566
  .option('-c, --content <content>', 'new content')
297
- .option('--type <type>', 'new memory type')
567
+ .option('--type <type>', `new memory type (${MEMORY_TYPE_CHOICES.join(', ')})`)
298
568
  .option('--tags <tags>', 'new tags (comma-separated)')
299
569
  .option('-i, --interactive', 'interactive mode')
300
570
  .action(async (id, options) => {
@@ -317,7 +587,7 @@ export function memoryCommands(program) {
317
587
  type: 'list',
318
588
  name: 'type',
319
589
  message: 'Memory type:',
320
- choices: ['conversation', 'knowledge', 'project', 'context', 'reference'],
590
+ choices: [...MEMORY_TYPE_CHOICES],
321
591
  default: currentMemory.memory_type,
322
592
  },
323
593
  {
@@ -64,12 +64,23 @@ program
64
64
  process.env.MEMORY_API_URL = opts.apiUrl;
65
65
  }
66
66
  process.env.CLI_OUTPUT_FORMAT = opts.output;
67
+ const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true' ||
68
+ process.env.CLI_FORCE_API === 'true' ||
69
+ process.env.ONASIS_FORCE_API === 'true';
70
+ const forceApiFromConfig = cliConfig.get('forceApi') === true ||
71
+ cliConfig.get('connectionTransport') === 'api';
72
+ const forceDirectApi = forceApiFromEnv || forceApiFromConfig || opts.mcp === false;
73
+ if (forceDirectApi) {
74
+ process.env.LANONASIS_FORCE_API = 'true';
75
+ }
67
76
  // Auto-initialize MCP unless disabled
68
77
  const isMcpFlow = actionCommand.name() === 'mcp' ||
69
78
  actionCommand.parent?.name?.() === 'mcp' ||
70
79
  actionCommand.name() === 'mcp-server' ||
71
80
  actionCommand.parent?.name?.() === 'mcp-server';
72
- if (opts.mcp !== false && !isMcpFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
81
+ const isConfigFlow = actionCommand.name() === 'config' ||
82
+ actionCommand.parent?.name?.() === 'config';
83
+ if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
73
84
  try {
74
85
  const client = getMCPClient();
75
86
  if (!client.isConnectedToServer()) {
@@ -87,6 +98,9 @@ program
87
98
  }
88
99
  }
89
100
  }
101
+ else if (forceDirectApi && process.env.CLI_VERBOSE === 'true') {
102
+ console.log(colors.muted('MCP auto-connect skipped (force direct API enabled)'));
103
+ }
90
104
  });
91
105
  // Enhanced global error handler
92
106
  process.on('uncaughtException', (error) => {
@@ -324,11 +338,10 @@ const topicCmd = program
324
338
  .description('Topic management commands');
325
339
  requireAuth(topicCmd);
326
340
  topicCommands(topicCmd);
327
- // Configuration commands (require auth)
341
+ // Configuration commands (no auth required)
328
342
  const configCmd = program
329
343
  .command('config')
330
344
  .description('Configuration management');
331
- requireAuth(configCmd);
332
345
  configCommands(configCmd);
333
346
  // Organization commands (require auth)
334
347
  const orgCmd = program
package/dist/index.js CHANGED
@@ -53,6 +53,18 @@ program
53
53
  process.env.MEMORY_API_URL = opts.apiUrl;
54
54
  }
55
55
  process.env.CLI_OUTPUT_FORMAT = opts.output;
56
+ const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true' ||
57
+ process.env.CLI_FORCE_API === 'true' ||
58
+ process.env.ONASIS_FORCE_API === 'true';
59
+ const forceApiFromConfig = cliConfig.get('forceApi') === true ||
60
+ cliConfig.get('connectionTransport') === 'api';
61
+ const forceDirectApi = forceApiFromEnv || forceApiFromConfig || opts.mcp === false;
62
+ if (process.env.CLI_VERBOSE === 'true') {
63
+ console.log(colors.muted(`transport flags: env=${forceApiFromEnv} config=${forceApiFromConfig} no_mcp=${opts.mcp === false}`));
64
+ }
65
+ if (forceDirectApi) {
66
+ process.env.LANONASIS_FORCE_API = 'true';
67
+ }
56
68
  const skipOnboarding = actionCommand.name() === 'init' ||
57
69
  actionCommand.name() === 'auth' ||
58
70
  actionCommand.parent?.name?.() === 'auth';
@@ -76,7 +88,9 @@ program
76
88
  actionCommand.parent?.name?.() === 'mcp' ||
77
89
  actionCommand.name() === 'mcp-server' ||
78
90
  actionCommand.parent?.name?.() === 'mcp-server';
79
- if (opts.mcp !== false && !isMcpFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
91
+ const isConfigFlow = actionCommand.name() === 'config' ||
92
+ actionCommand.parent?.name?.() === 'config';
93
+ if (!forceDirectApi && !isMcpFlow && !isConfigFlow && !['init', 'auth', 'login', 'health', 'status'].includes(actionCommand.name())) {
80
94
  try {
81
95
  const client = getMCPClient();
82
96
  if (!client.isConnectedToServer()) {
@@ -94,6 +108,9 @@ program
94
108
  }
95
109
  }
96
110
  }
111
+ else if (forceDirectApi && process.env.CLI_VERBOSE === 'true') {
112
+ console.log(colors.muted('MCP auto-connect skipped (force direct API enabled)'));
113
+ }
97
114
  });
98
115
  // Enhanced global error handler
99
116
  process.on('uncaughtException', (error) => {
@@ -399,11 +416,10 @@ const topicCmd = program
399
416
  .description('Topic management commands');
400
417
  requireAuth(topicCmd);
401
418
  topicCommands(topicCmd);
402
- // Configuration commands (require auth)
419
+ // Configuration commands (no auth required)
403
420
  const configCmd = program
404
421
  .command('config')
405
422
  .description('Configuration management');
406
- requireAuth(configCmd);
407
423
  configCommands(configCmd);
408
424
  // Organization commands (require auth)
409
425
  const orgCmd = program
@@ -91,18 +91,18 @@ export declare const MemoryListSchema: z.ZodObject<{
91
91
  tags?: string[];
92
92
  limit?: number;
93
93
  topic_id?: string;
94
+ order?: "desc" | "asc";
94
95
  memory_type?: "context" | "reference" | "note";
95
96
  offset?: number;
96
97
  sort_by?: "title" | "created_at" | "updated_at";
97
- order?: "desc" | "asc";
98
98
  }, {
99
99
  tags?: string[];
100
100
  limit?: number;
101
101
  topic_id?: string;
102
+ order?: "desc" | "asc";
102
103
  memory_type?: "context" | "reference" | "note";
103
104
  offset?: number;
104
105
  sort_by?: "title" | "created_at" | "updated_at";
105
- order?: "desc" | "asc";
106
106
  }>;
107
107
  export declare const TopicCreateSchema: z.ZodObject<{
108
108
  name: z.ZodString;
@@ -201,14 +201,14 @@ export declare const SystemConfigSchema: z.ZodObject<{
201
201
  scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
202
202
  }, "strip", z.ZodTypeAny, {
203
203
  value?: any;
204
- key?: string;
205
204
  action?: "get" | "set" | "reset";
206
205
  scope?: "user" | "global";
206
+ key?: string;
207
207
  }, {
208
208
  value?: any;
209
- key?: string;
210
209
  action?: "get" | "set" | "reset";
211
210
  scope?: "user" | "global";
211
+ key?: string;
212
212
  }>;
213
213
  export declare const BulkOperationSchema: z.ZodObject<{
214
214
  operation: z.ZodEnum<["create", "update", "delete"]>;
@@ -463,18 +463,18 @@ export declare const MCPSchemas: {
463
463
  tags?: string[];
464
464
  limit?: number;
465
465
  topic_id?: string;
466
+ order?: "desc" | "asc";
466
467
  memory_type?: "context" | "reference" | "note";
467
468
  offset?: number;
468
469
  sort_by?: "title" | "created_at" | "updated_at";
469
- order?: "desc" | "asc";
470
470
  }, {
471
471
  tags?: string[];
472
472
  limit?: number;
473
473
  topic_id?: string;
474
+ order?: "desc" | "asc";
474
475
  memory_type?: "context" | "reference" | "note";
475
476
  offset?: number;
476
477
  sort_by?: "title" | "created_at" | "updated_at";
477
- order?: "desc" | "asc";
478
478
  }>;
479
479
  };
480
480
  topic: {
@@ -579,14 +579,14 @@ export declare const MCPSchemas: {
579
579
  scope: z.ZodDefault<z.ZodEnum<["user", "global"]>>;
580
580
  }, "strip", z.ZodTypeAny, {
581
581
  value?: any;
582
- key?: string;
583
582
  action?: "get" | "set" | "reset";
584
583
  scope?: "user" | "global";
584
+ key?: string;
585
585
  }, {
586
586
  value?: any;
587
- key?: string;
588
587
  action?: "get" | "set" | "reset";
589
588
  scope?: "user" | "global";
589
+ key?: string;
590
590
  }>;
591
591
  };
592
592
  operations: {
@@ -54,11 +54,15 @@ export interface UpdateMemoryRequest {
54
54
  metadata?: Record<string, unknown>;
55
55
  }
56
56
  export interface GetMemoriesParams {
57
+ page?: number;
57
58
  limit?: number;
58
59
  offset?: number;
59
60
  memory_type?: MemoryType;
60
61
  tags?: string[] | string;
61
62
  topic_id?: string;
63
+ user_id?: string;
64
+ sort?: 'created_at' | 'updated_at' | 'last_accessed' | 'access_count' | 'title';
65
+ order?: 'asc' | 'desc';
62
66
  sort_by?: 'created_at' | 'updated_at' | 'last_accessed' | 'access_count';
63
67
  sort_order?: 'asc' | 'desc';
64
68
  }
@@ -148,6 +152,7 @@ export interface ApiErrorResponse {
148
152
  export declare class APIClient {
149
153
  private client;
150
154
  private config;
155
+ private normalizeMemoryEntry;
151
156
  constructor();
152
157
  login(email: string, password: string): Promise<AuthResponse>;
153
158
  register(email: string, password: string, organizationName?: string): Promise<AuthResponse>;
package/dist/utils/api.js CHANGED
@@ -5,6 +5,26 @@ import { CLIConfig } from './config.js';
5
5
  export class APIClient {
6
6
  client;
7
7
  config;
8
+ normalizeMemoryEntry(payload) {
9
+ // API responses are inconsistent across gateways:
10
+ // - Some return the memory entry directly
11
+ // - Some wrap it in `{ data: <memory>, message?: string }`
12
+ if (payload && typeof payload === 'object') {
13
+ const obj = payload;
14
+ const directId = obj.id;
15
+ if (typeof directId === 'string' && directId.length > 0) {
16
+ return payload;
17
+ }
18
+ const data = obj.data;
19
+ if (data && typeof data === 'object') {
20
+ const dataObj = data;
21
+ if (typeof dataObj.id === 'string' && dataObj.id.length > 0) {
22
+ return data;
23
+ }
24
+ }
25
+ }
26
+ return payload;
27
+ }
8
28
  constructor() {
9
29
  this.config = new CLIConfig();
10
30
  this.client = axios.create({
@@ -13,6 +33,8 @@ export class APIClient {
13
33
  // Setup request interceptor to add auth token and headers
14
34
  this.client.interceptors.request.use(async (config) => {
15
35
  await this.config.init();
36
+ // Keep OAuth sessions alive automatically (prevents intermittent "auth required" cutouts).
37
+ await this.config.refreshTokenIfNeeded();
16
38
  // Service Discovery
17
39
  await this.config.discoverServices();
18
40
  // Use appropriate base URL based on endpoint and auth method
@@ -20,17 +42,26 @@ export class APIClient {
20
42
  const discoveredServices = this.config.get('discoveredServices');
21
43
  const authMethod = this.config.get('authMethod');
22
44
  const vendorKey = await this.config.getVendorKeyAsync();
45
+ const token = this.config.getToken();
46
+ const forceApiFromEnv = process.env.LANONASIS_FORCE_API === 'true'
47
+ || process.env.CLI_FORCE_API === 'true'
48
+ || process.env.ONASIS_FORCE_API === 'true';
49
+ const forceApiFromConfig = this.config.get('forceApi') === true
50
+ || this.config.get('connectionTransport') === 'api';
51
+ const forceDirectApi = forceApiFromEnv || forceApiFromConfig;
52
+ const prefersTokenAuth = Boolean(token) && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
53
+ const useVendorKeyAuth = Boolean(vendorKey) && !prefersTokenAuth;
23
54
  // Determine the correct API base URL:
24
55
  // - Auth endpoints -> auth.lanonasis.com
25
56
  // - JWT auth (no vendor key) -> mcp.lanonasis.com (supports JWT tokens)
26
57
  // - Vendor key auth -> api.lanonasis.com (requires vendor key)
27
58
  let apiBaseUrl;
28
- const useMcpServer = !vendorKey && (authMethod === 'jwt' || authMethod === 'oauth' || authMethod === 'oauth2');
59
+ const useMcpServer = !forceDirectApi && prefersTokenAuth && !useVendorKeyAuth;
29
60
  if (isAuthEndpoint) {
30
61
  apiBaseUrl = discoveredServices?.auth_base || 'https://auth.lanonasis.com';
31
62
  }
32
- else if (vendorKey) {
33
- // Vendor key works with api.lanonasis.com
63
+ else if (forceDirectApi) {
64
+ // Force direct REST API mode to bypass MCP routing for troubleshooting.
34
65
  apiBaseUrl = this.config.getApiUrl();
35
66
  }
36
67
  else if (useMcpServer) {
@@ -51,8 +82,22 @@ export class APIClient {
51
82
  config.headers['X-Project-Scope'] = 'lanonasis-maas';
52
83
  }
53
84
  // Enhanced Authentication Support
54
- const token = this.config.getToken();
55
- if (vendorKey) {
85
+ // Even in forced direct-API mode, prefer bearer token auth when available.
86
+ // This avoids accidentally sending an OAuth access token as X-API-Key (we store it
87
+ // in secure storage for MCP/WebSocket usage), which can cause 401s.
88
+ const preferVendorKeyInDirectApiMode = forceDirectApi && Boolean(vendorKey) && !prefersTokenAuth;
89
+ if (preferVendorKeyInDirectApiMode) {
90
+ // Vendor key authentication (validated server-side)
91
+ // Send raw key - server handles hashing for comparison
92
+ config.headers['X-API-Key'] = vendorKey;
93
+ config.headers['X-Auth-Method'] = 'vendor_key';
94
+ }
95
+ else if (prefersTokenAuth) {
96
+ // JWT/OAuth token authentication takes precedence when both are present.
97
+ config.headers.Authorization = `Bearer ${token}`;
98
+ config.headers['X-Auth-Method'] = 'jwt';
99
+ }
100
+ else if (vendorKey) {
56
101
  // Vendor key authentication (validated server-side)
57
102
  // Send raw key - server handles hashing for comparison
58
103
  config.headers['X-API-Key'] = vendorKey;
@@ -69,7 +114,10 @@ export class APIClient {
69
114
  // Add project scope for Golden Contract compliance
70
115
  config.headers['X-Project-Scope'] = 'lanonasis-maas';
71
116
  if (process.env.CLI_VERBOSE === 'true') {
117
+ const transportMode = forceDirectApi ? 'api-forced' : (useMcpServer ? 'mcp-http' : 'api');
118
+ config.headers['X-Transport-Mode'] = transportMode;
72
119
  console.log(chalk.dim(`→ ${config.method?.toUpperCase()} ${config.url} [${requestId}]`));
120
+ console.log(chalk.dim(` transport=${transportMode} baseURL=${config.baseURL}`));
73
121
  }
74
122
  return config;
75
123
  });
@@ -84,7 +132,7 @@ export class APIClient {
84
132
  const { status, data } = error.response;
85
133
  if (status === 401) {
86
134
  console.error(chalk.red('✖ Authentication failed'));
87
- console.log(chalk.yellow('Please run:'), chalk.white('memory login'));
135
+ console.log(chalk.yellow('Please run:'), chalk.white('lanonasis auth login'));
88
136
  process.exit(1);
89
137
  }
90
138
  if (status === 403) {
@@ -127,19 +175,88 @@ export class APIClient {
127
175
  // All memory endpoints use /api/v1/memories path (plural, per REST conventions)
128
176
  async createMemory(data) {
129
177
  const response = await this.client.post('/api/v1/memories', data);
130
- return response.data;
178
+ return this.normalizeMemoryEntry(response.data);
131
179
  }
132
180
  async getMemories(params = {}) {
133
- const response = await this.client.get('/api/v1/memories', { params });
134
- return response.data;
181
+ try {
182
+ const response = await this.client.get('/api/v1/memories', { params });
183
+ return response.data;
184
+ }
185
+ catch (error) {
186
+ // Backward-compatible fallback: newer API contracts may reject GET list and prefer search-only.
187
+ if (error?.response?.status === 405) {
188
+ const limit = Number(params.limit || 20);
189
+ const page = Number(params.page || 1);
190
+ const offset = Number(params.offset ?? Math.max(0, (page - 1) * limit));
191
+ const searchPayload = {
192
+ query: '*',
193
+ limit,
194
+ threshold: 0
195
+ };
196
+ if (params.memory_type) {
197
+ searchPayload.memory_types = [params.memory_type];
198
+ }
199
+ if (params.tags) {
200
+ searchPayload.tags = Array.isArray(params.tags)
201
+ ? params.tags
202
+ : String(params.tags).split(',').map((tag) => tag.trim()).filter(Boolean);
203
+ }
204
+ if (params.topic_id) {
205
+ searchPayload.topic_id = params.topic_id;
206
+ }
207
+ if (offset > 0) {
208
+ searchPayload.offset = offset;
209
+ }
210
+ const fallback = await this.client.post('/api/v1/memories/search', searchPayload);
211
+ const payload = fallback.data || {};
212
+ const resultsArray = Array.isArray(payload.data)
213
+ ? payload.data
214
+ : Array.isArray(payload.results)
215
+ ? payload.results
216
+ : [];
217
+ const memories = resultsArray
218
+ .map((entry) => {
219
+ // Some gateways/search endpoints wrap results as `{ data: <memory> }`.
220
+ if (entry && typeof entry === 'object') {
221
+ const obj = entry;
222
+ const data = obj.data;
223
+ if (data && typeof data === 'object') {
224
+ const dataObj = data;
225
+ if (typeof dataObj.id === 'string' && dataObj.id.length > 0) {
226
+ return data;
227
+ }
228
+ }
229
+ }
230
+ return entry;
231
+ })
232
+ .map((entry) => this.normalizeMemoryEntry(entry));
233
+ const total = Number.isFinite(payload.total) ? Number(payload.total) : memories.length;
234
+ const pages = Math.max(1, Math.ceil(total / limit));
235
+ const currentPage = Math.max(1, Math.floor(offset / limit) + 1);
236
+ return {
237
+ ...payload,
238
+ data: memories,
239
+ memories,
240
+ pagination: {
241
+ total,
242
+ limit,
243
+ offset,
244
+ has_more: (offset + memories.length) < total,
245
+ page: currentPage,
246
+ pages
247
+ }
248
+ };
249
+ }
250
+ throw error;
251
+ }
135
252
  }
136
253
  async getMemory(id) {
137
254
  const response = await this.client.get(`/api/v1/memories/${id}`);
138
- return response.data;
255
+ return this.normalizeMemoryEntry(response.data);
139
256
  }
140
257
  async updateMemory(id, data) {
141
258
  const response = await this.client.put(`/api/v1/memories/${id}`, data);
142
- return response.data;
259
+ return this.normalizeMemoryEntry(response.data);
143
260
  }
144
261
  async deleteMemory(id) {
145
262
  await this.client.delete(`/api/v1/memories/${id}`);
@@ -678,6 +678,9 @@ export class CLIConfig {
678
678
  return this.config.user;
679
679
  }
680
680
  async isAuthenticated() {
681
+ // Attempt refresh for OAuth sessions before checks (prevents intermittent auth dropouts).
682
+ // This is safe to call even when not using OAuth; it will no-op.
683
+ await this.refreshTokenIfNeeded();
681
684
  // Check if using vendor key authentication
682
685
  if (this.config.authMethod === 'vendor_key') {
683
686
  // Use async method to read from encrypted ApiKeyStorage
@@ -736,6 +739,16 @@ export class CLIConfig {
736
739
  const token = this.getToken();
737
740
  if (!token)
738
741
  return false;
742
+ // OAuth tokens are often opaque (not JWT). Prefer local expiry metadata when present.
743
+ if (this.config.authMethod === 'oauth') {
744
+ const tokenExpiresAt = this.get('token_expires_at');
745
+ if (typeof tokenExpiresAt === 'number') {
746
+ const isValid = Date.now() < tokenExpiresAt;
747
+ this.authCheckCache = { isValid, timestamp: Date.now() };
748
+ return isValid;
749
+ }
750
+ // Fall through to legacy validation when we don't have expiry metadata.
751
+ }
739
752
  // Check cache first
740
753
  if (this.authCheckCache && (Date.now() - this.authCheckCache.timestamp) < this.AUTH_CACHE_TTL) {
741
754
  return this.authCheckCache.isValid;
@@ -968,11 +981,85 @@ export class CLIConfig {
968
981
  return;
969
982
  }
970
983
  try {
984
+ // OAuth token refresh (opaque tokens + refresh_token + token_expires_at)
985
+ if (this.config.authMethod === 'oauth') {
986
+ const refreshToken = this.get('refresh_token');
987
+ if (!refreshToken) {
988
+ return;
989
+ }
990
+ const tokenExpiresAtRaw = this.get('token_expires_at');
991
+ const tokenExpiresAt = (() => {
992
+ const n = typeof tokenExpiresAtRaw === 'number'
993
+ ? tokenExpiresAtRaw
994
+ : typeof tokenExpiresAtRaw === 'string'
995
+ ? Number(tokenExpiresAtRaw)
996
+ : undefined;
997
+ if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) {
998
+ return undefined;
999
+ }
1000
+ // Support both seconds and milliseconds since epoch.
1001
+ // Seconds are ~1.7e9; ms are ~1.7e12.
1002
+ return n < 1e11 ? n * 1000 : n;
1003
+ })();
1004
+ const nowMs = Date.now();
1005
+ const refreshWindowMs = 5 * 60 * 1000; // 5 minutes
1006
+ // If we don't know expiry, don't force a refresh.
1007
+ if (typeof tokenExpiresAt !== 'number' || nowMs < (tokenExpiresAt - refreshWindowMs)) {
1008
+ return;
1009
+ }
1010
+ await this.discoverServices();
1011
+ const authBase = this.getDiscoveredApiUrl();
1012
+ const resp = await axios.post(`${authBase}/oauth/token`, {
1013
+ grant_type: 'refresh_token',
1014
+ refresh_token: refreshToken,
1015
+ client_id: 'lanonasis-cli'
1016
+ }, {
1017
+ headers: { 'Content-Type': 'application/json' },
1018
+ timeout: 10000,
1019
+ proxy: false
1020
+ });
1021
+ // Some gateways wrap responses as `{ data: { ... } }`.
1022
+ const raw = resp?.data;
1023
+ const payload = raw && typeof raw === 'object' && raw.data && typeof raw.data === 'object'
1024
+ ? raw.data
1025
+ : raw;
1026
+ const accessToken = payload?.access_token ?? payload?.token;
1027
+ const refreshedRefreshToken = payload?.refresh_token;
1028
+ const expiresIn = payload?.expires_in;
1029
+ if (typeof accessToken !== 'string' || accessToken.length === 0) {
1030
+ throw new Error('Token refresh response missing access_token');
1031
+ }
1032
+ // setToken() assumes JWT by default; ensure authMethod stays oauth after storing.
1033
+ await this.setToken(accessToken);
1034
+ this.config.authMethod = 'oauth';
1035
+ if (typeof refreshedRefreshToken === 'string' && refreshedRefreshToken.length > 0) {
1036
+ this.config.refresh_token = refreshedRefreshToken;
1037
+ }
1038
+ if (typeof expiresIn === 'number' && Number.isFinite(expiresIn)) {
1039
+ this.config.token_expires_at = Date.now() + (expiresIn * 1000);
1040
+ }
1041
+ // Keep the encrypted "vendor key" in sync for MCP/WebSocket clients that use X-API-Key.
1042
+ // This does not change authMethod away from oauth (setVendorKey guards against that).
1043
+ try {
1044
+ await this.setVendorKey(accessToken);
1045
+ }
1046
+ catch {
1047
+ // Non-fatal: bearer token refresh still helps API calls.
1048
+ }
1049
+ await this.save().catch(() => { });
1050
+ return;
1051
+ }
971
1052
  // Check if token is JWT and if it's close to expiry
972
1053
  if (token.startsWith('cli_')) {
973
1054
  // CLI tokens don't need refresh, they're long-lived
974
1055
  return;
975
1056
  }
1057
+ // Only attempt JWT refresh for tokens that look like JWTs.
1058
+ // OAuth access tokens in this system can be opaque strings; treating them as JWTs
1059
+ // creates noisy failures and can cause unwanted state writes.
1060
+ if (token.split('.').length !== 3) {
1061
+ return;
1062
+ }
976
1063
  const decoded = jwtDecode(token);
977
1064
  const now = Date.now() / 1000;
978
1065
  const exp = typeof decoded.exp === 'number' ? decoded.exp : 0;
@@ -469,7 +469,10 @@ export class MCPClient {
469
469
  this.sseConnection.onmessage = (event) => {
470
470
  try {
471
471
  const data = JSON.parse(event.data);
472
- console.log(chalk.blue('📡 Real-time update:'), data.type);
472
+ // Only show SSE updates in verbose mode to avoid interfering with interactive prompts
473
+ if (process.env.CLI_VERBOSE === 'true') {
474
+ console.log(chalk.blue('📡 Real-time update:'), data.type);
475
+ }
473
476
  }
474
477
  catch {
475
478
  // Ignore parse errors
@@ -526,15 +529,20 @@ export class MCPClient {
526
529
  this.wsConnection.on('message', (data) => {
527
530
  try {
528
531
  const message = JSON.parse(data.toString());
529
- const messageId = message.id ?? 'event';
530
- const messageType = message.method
531
- || (message.error ? 'error' : undefined)
532
- || (message.result ? 'result' : undefined)
533
- || 'response';
534
- console.log(chalk.blue('📡 MCP message:'), messageId, messageType);
532
+ // Only show WebSocket messages in verbose mode to avoid interfering with interactive prompts
533
+ if (process.env.CLI_VERBOSE === 'true') {
534
+ const messageId = message.id ?? 'event';
535
+ const messageType = message.method
536
+ || (message.error ? 'error' : undefined)
537
+ || (message.result ? 'result' : undefined)
538
+ || 'response';
539
+ console.log(chalk.blue('📡 MCP message:'), messageId, messageType);
540
+ }
535
541
  }
536
542
  catch (error) {
537
- console.error('Failed to parse WebSocket message:', error);
543
+ if (process.env.CLI_VERBOSE === 'true') {
544
+ console.error('Failed to parse WebSocket message:', error);
545
+ }
538
546
  }
539
547
  });
540
548
  this.wsConnection.on('error', (error) => {
@@ -47,6 +47,7 @@ export declare class TextInputHandlerImpl implements TextInputHandler {
47
47
  * Parse raw key event from buffer
48
48
  */
49
49
  private parseKeyEvent;
50
+ private resumeStdinIfSupported;
50
51
  /**
51
52
  * Check if a key event matches a key pattern
52
53
  */
@@ -46,46 +46,113 @@ export class TextInputHandlerImpl {
46
46
  options: mergedOptions,
47
47
  status: 'active',
48
48
  };
49
- return new Promise((resolve, reject) => {
49
+ return new Promise(async (resolve, reject) => {
50
50
  let handleKeypress = null;
51
+ let sigintHandler = null;
51
52
  const cleanup = () => {
52
53
  if (handleKeypress) {
53
54
  process.stdin.removeListener('data', handleKeypress);
55
+ handleKeypress = null;
56
+ }
57
+ if (sigintHandler) {
58
+ process.removeListener('SIGINT', sigintHandler);
59
+ sigintHandler = null;
54
60
  }
55
61
  this.disableRawMode();
62
+ // Restore terminal state
63
+ process.stdout.write('\x1b[?25h'); // Show cursor
64
+ };
65
+ // Set up completion handlers first
66
+ const complete = (result) => {
67
+ cleanup();
68
+ if (this.currentSession) {
69
+ this.currentSession.status = 'completed';
70
+ }
71
+ resolve(result);
72
+ };
73
+ const cancel = () => {
74
+ cleanup();
75
+ if (this.currentSession) {
76
+ this.currentSession.status = 'cancelled';
77
+ }
78
+ reject(new Error('Input cancelled by user'));
56
79
  };
80
+ // Store handlers for special key processing
81
+ this._completeHandler = complete;
82
+ this._cancelHandler = cancel;
57
83
  try {
84
+ // Check if we can use raw mode
85
+ if (!process.stdin.isTTY) {
86
+ // Fall back to simple readline for non-TTY environments
87
+ console.log('\nNote: Interactive text input not available. Using simple input mode.');
88
+ console.log('Enter your text (empty line to finish):');
89
+ const readline = require('readline');
90
+ const rl = readline.createInterface({
91
+ input: process.stdin,
92
+ output: process.stdout
93
+ });
94
+ const lines = [];
95
+ rl.on('line', (line) => {
96
+ if (line === '') {
97
+ rl.close();
98
+ complete(lines.join('\n'));
99
+ }
100
+ else {
101
+ lines.push(line);
102
+ }
103
+ });
104
+ rl.on('close', () => {
105
+ complete(lines.join('\n'));
106
+ });
107
+ return;
108
+ }
109
+ // Add SIGINT handler as fallback for Ctrl+C
110
+ sigintHandler = () => {
111
+ cancel();
112
+ };
113
+ process.on('SIGINT', sigintHandler);
114
+ // Safety: Add a way to escape via triple-ESC (sends 3 escape chars quickly)
115
+ let escapeCount = 0;
116
+ let escapeTimer = null;
117
+ // Try to claim stdin from any previous handlers
118
+ process.stdin.removeAllListeners('data');
119
+ process.stdin.removeAllListeners('readable');
120
+ process.stdin.removeAllListeners('end');
58
121
  this.enableRawMode();
122
+ // Verify raw mode is working
123
+ if (!this.isRawModeEnabled) {
124
+ console.log('\nWarning: Could not enable raw mode. Falling back to editor mode.');
125
+ cleanup();
126
+ const { content } = await (await import('inquirer')).default.prompt([
127
+ { type: 'editor', name: 'content', message: prompt, default: mergedOptions.defaultContent }
128
+ ]);
129
+ resolve(content);
130
+ return;
131
+ }
59
132
  this.displayInputPrompt(this.getCurrentContent());
60
133
  handleKeypress = (chunk) => {
61
- const key = this.parseKeyEvent(chunk);
62
- if (this.handleSpecialKeys(key)) {
63
- return;
64
- }
65
- // Handle regular character input
66
- if (key.sequence && this.currentSession) {
67
- this.addCharacterToInput(key.sequence);
68
- this.displayInputPrompt(this.getCurrentContent());
69
- }
70
- };
71
- // Set up completion handlers
72
- const complete = (result) => {
73
- cleanup();
74
- if (this.currentSession) {
75
- this.currentSession.status = 'completed';
134
+ try {
135
+ const key = this.parseKeyEvent(chunk);
136
+ if (this.handleSpecialKeys(key)) {
137
+ return;
138
+ }
139
+ // Handle regular character input
140
+ if (key.sequence && this.currentSession) {
141
+ // Filter out control characters that shouldn't be added as text
142
+ if (key.sequence.charCodeAt(0) >= 32 || key.sequence === '\t') {
143
+ this.addCharacterToInput(key.sequence);
144
+ this.displayInputPrompt(this.getCurrentContent());
145
+ }
146
+ }
76
147
  }
77
- resolve(result);
78
- };
79
- const cancel = () => {
80
- cleanup();
81
- if (this.currentSession) {
82
- this.currentSession.status = 'cancelled';
148
+ catch (err) {
149
+ // Don't let errors in key handling crash the input
150
+ console.error('Key handling error:', err);
83
151
  }
84
- reject(new Error('Input cancelled by user'));
85
152
  };
86
- // Store handlers for special key processing
87
- this._completeHandler = complete;
88
- this._cancelHandler = cancel;
153
+ // Ensure stdin is flowing before adding listener
154
+ // This is critical after inquirer prompts which may pause stdin
155
+ this.resumeStdinIfSupported();
89
156
  process.stdin.on('data', handleKeypress);
90
157
  }
91
158
  catch (error) {
@@ -101,8 +168,14 @@ export class TextInputHandlerImpl {
101
168
  if (!this.isRawModeEnabled && process.stdin.isTTY) {
102
169
  this.originalStdinMode = process.stdin.isRaw;
103
170
  process.stdin.setRawMode(true);
171
+ this.resumeStdinIfSupported(); // Ensure stdin is flowing to receive data events
104
172
  this.isRawModeEnabled = true;
105
173
  }
174
+ else if (!process.stdin.isTTY) {
175
+ // Non-TTY mode - can't use raw mode, fall back to line mode
176
+ console.error('Warning: Not a TTY, inline text input may not work correctly');
177
+ this.resumeStdinIfSupported();
178
+ }
106
179
  }
107
180
  /**
108
181
  * Disable raw mode and return to normal terminal behavior
@@ -112,6 +185,7 @@ export class TextInputHandlerImpl {
112
185
  process.stdin.setRawMode(this.originalStdinMode || false);
113
186
  this.isRawModeEnabled = false;
114
187
  }
188
+ // Don't pause stdin here as other handlers may need it
115
189
  }
116
190
  /**
117
191
  * Handle special keyboard events
@@ -228,6 +302,11 @@ export class TextInputHandlerImpl {
228
302
  }
229
303
  return key;
230
304
  }
305
+ resumeStdinIfSupported() {
306
+ if (typeof process.stdin.resume === 'function') {
307
+ process.stdin.resume();
308
+ }
309
+ }
231
310
  /**
232
311
  * Check if a key event matches a key pattern
233
312
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanonasis/cli",
3
- "version": "3.9.3",
3
+ "version": "3.9.5",
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",
@@ -88,7 +88,7 @@
88
88
  "build": "rimraf dist && tsc -p tsconfig.json",
89
89
  "prepublishOnly": "npm run build",
90
90
  "postinstall": "node scripts/postinstall.js",
91
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
91
+ "test": "[ -f node_modules/jest/bin/jest.js ] || bun install --no-save; node --experimental-vm-modules node_modules/jest/bin/jest.js",
92
92
  "test:watch": "npm test -- --watch",
93
93
  "test:coverage": "npm test -- --coverage"
94
94
  }