@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 +5 -0
- package/README.md +71 -11
- package/dist/commands/config.js +11 -0
- package/dist/commands/memory.js +276 -6
- package/dist/index-simple.js +16 -3
- package/dist/index.js +19 -3
- package/dist/mcp/schemas/tool-schemas.d.ts +8 -8
- package/dist/utils/api.d.ts +5 -0
- package/dist/utils/api.js +128 -11
- package/dist/utils/config.js +87 -0
- package/dist/utils/mcp-client.js +16 -8
- package/dist/ux/implementations/TextInputHandlerImpl.d.ts +1 -0
- package/dist/ux/implementations/TextInputHandlerImpl.js +105 -26
- package/package.json +2 -2
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.
|
|
1
|
+
# @lanonasis/cli v3.9.3 - Enterprise Security & Professional UX
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@lanonasis/cli)
|
|
4
4
|
[](https://www.npmjs.com/package/@lanonasis/cli)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://api.lanonasis.com/.well-known/onasis.json)
|
|
7
7
|
|
|
8
|
-
🎉 **NEW IN v3.9**:
|
|
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
|
-
|
|
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 --
|
|
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
|
|
231
|
-
onasis memory create
|
|
232
|
-
|
|
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" --
|
|
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>
|
|
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
|
-
|
package/dist/commands/config.js
CHANGED
|
@@ -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 => {
|
package/dist/commands/memory.js
CHANGED
|
@@ -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>',
|
|
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: [
|
|
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:
|
|
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>',
|
|
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: [
|
|
590
|
+
choices: [...MEMORY_TYPE_CHOICES],
|
|
321
591
|
default: currentMemory.memory_type,
|
|
322
592
|
},
|
|
323
593
|
{
|
package/dist/index-simple.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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: {
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -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 = !
|
|
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 (
|
|
33
|
-
//
|
|
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
|
-
|
|
55
|
-
|
|
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('
|
|
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
|
-
|
|
134
|
-
|
|
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}`);
|
package/dist/utils/config.js
CHANGED
|
@@ -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;
|
package/dist/utils/mcp-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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) => {
|
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
87
|
-
|
|
88
|
-
this.
|
|
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
|
+
"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
|
}
|