omni-agent-cli 2.0.0

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/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # ◈ OmniAgent — Universal AI CLI Agent
2
+
3
+ A powerful, beautiful CLI agent that works with **any AI provider** and has full filesystem + shell capabilities.
4
+
5
+ ```
6
+ ██████╗ ███╗ ███╗███╗ ██╗██╗ ███████╗ ██████╗ ███████╗███╗ ██╗████████╗
7
+ ██╔═══██╗████╗ ████║████╗ ██║██║ ██╔════╝██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
8
+ ██║ ██║██╔████╔██║██╔██╗ ██║██║ ███████╗██║ ███╗█████╗ ██╔██╗ ██║ ██║
9
+ ██║ ██║██║╚██╔╝██║██║╚██╗██║██║ ╚════██║██║ ██║██╔══╝ ██║╚██╗██║ ██║
10
+ ╚██████╔╝██║ ╚═╝ ██║██║ ╚████║██║ ███████║╚██████╔╝███████╗██║ ╚████║ ██║
11
+ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝
12
+ ```
13
+
14
+ ---
15
+
16
+ ## 🚀 Quick Start
17
+
18
+ ```bash
19
+ # Install dependencies
20
+ npm install
21
+
22
+ # First run — guided setup wizard
23
+ node index.js
24
+
25
+ # Or set up separately
26
+ node index.js --setup
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 🔌 Supported Providers
32
+
33
+ | Provider | Format | Notes |
34
+ |----------|--------|-------|
35
+ | **Anthropic** (Claude) | Native | claude-opus, sonnet, haiku |
36
+ | **OpenAI** | OpenAI | gpt-4o, o1, o3-mini |
37
+ | **Groq** | OpenAI | Ultra fast inference |
38
+ | **xAI** (Grok) | OpenAI | grok-2, grok-beta |
39
+ | **DeepSeek** | OpenAI | deepseek-chat, reasoner |
40
+ | **Mistral AI** | OpenAI | mistral-large |
41
+ | **Together AI** | OpenAI | Llama, Qwen, etc. |
42
+ | **OpenRouter** | OpenAI | All models via one key |
43
+ | **io.net** | OpenAI | Decentralized inference |
44
+ | **Ollama** | OpenAI | Local models |
45
+ | **Custom** | OpenAI | Any OpenAI-compatible API (kiai.ai, etc.) |
46
+
47
+ ---
48
+
49
+ ## 💻 Usage
50
+
51
+ ```bash
52
+ # Start in current directory
53
+ node index.js
54
+
55
+ # Start in specific directory
56
+ node index.js /path/to/project
57
+
58
+ # Global install (optional)
59
+ npm link
60
+ omni-agent /path/to/project
61
+ ```
62
+
63
+ ---
64
+
65
+ ## ⌨️ Commands
66
+
67
+ | Command | Description |
68
+ |---------|-------------|
69
+ | `/help` | Show all commands |
70
+ | `/clear` | Clear screen + conversation history |
71
+ | `/stats` | Show current session statistics |
72
+ | `/cd <dir>` | Change working directory |
73
+ | `/model <name>` | Switch model on the fly |
74
+ | `/setup` | Reconfigure provider/API key/model |
75
+ | `/reset` | Start new conversation (clear history) |
76
+ | `/history` | Show conversation history |
77
+ | `/exit` | Exit and show session statistics |
78
+
79
+ ---
80
+
81
+ ## 🛠️ Agent Tools
82
+
83
+ The agent has access to **13 filesystem and shell tools**:
84
+
85
+ | Tool | Description |
86
+ |------|-------------|
87
+ | `list_directory` | List files with metadata |
88
+ | `read_file` | Read file content |
89
+ | `write_file` | Create/overwrite files |
90
+ | `append_to_file` | Append to files |
91
+ | `patch_file` | Surgically edit text in files |
92
+ | `delete_path` | Delete files/directories |
93
+ | `copy_path` | Copy files |
94
+ | `move_path` | Move/rename files |
95
+ | `create_directory` | Create directories recursively |
96
+ | `get_file_info` | Get file metadata |
97
+ | `find_files` | Find files by glob pattern |
98
+ | `search_in_files` | Grep search in files |
99
+ | `execute_command` | Run any shell command |
100
+
101
+ ---
102
+
103
+ ## ⚡ Token Efficiency
104
+
105
+ OmniAgent uses **parallel tool calling** — when the AI needs multiple independent pieces of information, it fetches them ALL in a single API call instead of making sequential requests.
106
+
107
+ Example: "List the src/ folder and read package.json"
108
+ - ❌ Naive: 3 API calls (list → read → answer)
109
+ - ✅ OmniAgent: 2 API calls (list+read simultaneously → answer)
110
+
111
+ ---
112
+
113
+ ## 📊 Session Statistics
114
+
115
+ On exit (or `/stats`), OmniAgent shows:
116
+ - Session duration
117
+ - Messages sent/received
118
+ - API requests made
119
+ - Input/output token counts
120
+ - Estimated cost
121
+ - Tool usage breakdown with visual bars
122
+
123
+ ---
124
+
125
+ ## ⚙️ Config File
126
+
127
+ Config is saved to `~/.omni-agent.json`:
128
+
129
+ ```json
130
+ {
131
+ "providerKey": "groq",
132
+ "baseURL": "https://api.groq.com/openai/v1",
133
+ "apiKey": "your-key-here",
134
+ "model": "llama-3.3-70b-versatile",
135
+ "format": "openai",
136
+ "maxTokens": 8192,
137
+ "temperature": 0.3
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## 💡 Example Prompts
144
+
145
+ ```
146
+ ⟩ Refactor all the .js files in src/ to use async/await instead of callbacks
147
+
148
+ ⟩ Find all TODO comments in this project and create a TODO.md file
149
+
150
+ ⟩ Run the tests and fix any failures
151
+
152
+ ⟩ Create a REST API with Express for a simple todo app
153
+
154
+ ⟩ Analyze this codebase and suggest improvements
155
+
156
+ ⟩ Set up a Python virtual environment and install requirements.txt
157
+ ```
158
+
159
+ ---
160
+
161
+ *Built with Node.js · chalk · gradient-string · figlet · ora · inquirer · boxen*
package/index.js ADDED
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from 'readline';
4
+ import chalk from 'chalk';
5
+ import path from 'path';
6
+ import { existsSync } from 'fs';
7
+
8
+ import {
9
+ displayBanner, displayHelp, displayMessage,
10
+ displayToolCall, displayToolResult, displayError,
11
+ displayStats, createSpinner, displayThinking,
12
+ displayFilePreview, displayConfirmPrompt, C,
13
+ } from './src/ui.js';
14
+ import { loadConfig, saveConfig, setupWizard, PROVIDERS } from './src/config.js';
15
+ import { Agent } from './src/agent.js';
16
+ import { SessionStats } from './src/stats.js';
17
+ import { fetchProviderModels, fetchAllProviderModels, formatModelList, formatAllModels } from './src/model-fetcher.js';
18
+
19
+ // ─── Main ──────────────────────────────────────────────────────────────────────
20
+ async function main() {
21
+ await displayBanner();
22
+
23
+ // Config
24
+ let config = loadConfig();
25
+ const needsSetup = !config || process.argv.includes('--setup') || process.argv.includes('-s');
26
+ if (needsSetup) {
27
+ config = await setupWizard(config);
28
+ saveConfig(config);
29
+ if (process.argv.includes('--setup') || process.argv.includes('-s')) {
30
+ console.log(chalk.green('\n ✅ Setup complete. Run: node index.js\n'));
31
+ process.exit(0);
32
+ }
33
+ }
34
+
35
+ // Working directory
36
+ const argDir = process.argv.find((a) => !a.startsWith('-') && a !== process.argv[0] && a !== process.argv[1]);
37
+ let workdir = argDir ? path.resolve(argDir) : process.cwd();
38
+ if (argDir && !existsSync(workdir)) { displayError(`Dir not found: ${workdir}`); workdir = process.cwd(); }
39
+
40
+ // Agent + Stats
41
+ const stats = new SessionStats();
42
+ const agent = new Agent(config, stats);
43
+ agent.setWorkdir(workdir);
44
+
45
+ // Confirmation mode (can be toggled with /confirm off)
46
+ let confirmEnabled = true;
47
+ // Store last fetched model list for selection by number
48
+ let lastModelList = [];
49
+
50
+ // Info bar
51
+ console.log(
52
+ ` 📁 ${C.primary('Dir:')} ${C.dim(workdir)}\n` +
53
+ ` 🤖 ${C.secondary('Provider:')} ${chalk.magenta(config.providerKey)} ${C.dim('›')} ${C.accent(config.model)}\n` +
54
+ ` 💡 ${C.dim('Type')} ${chalk.white('/help')} ${C.dim('for commands, /exit to quit')}\n` +
55
+ C.dim(' ' + '─'.repeat(56) + '\n')
56
+ );
57
+
58
+ // ── Readline ──────────────────────────────────────────────────────────────────
59
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
60
+ let isProcessing = false;
61
+
62
+ // ── Readline helper to read a single key/line ─────────────────────────────────
63
+ const readLine = (prompt) => new Promise((resolve) => {
64
+ rl.question(prompt, resolve);
65
+ });
66
+
67
+ const readKey = () => new Promise((resolve) => {
68
+ process.stdin.setRawMode?.(true);
69
+ process.stdin.resume();
70
+ process.stdin.once('data', (buf) => {
71
+ process.stdin.setRawMode?.(false);
72
+ resolve(buf.toString());
73
+ });
74
+ });
75
+
76
+ // ── Exit ──────────────────────────────────────────────────────────────────────
77
+ const handleExit = async () => {
78
+ console.log('\n');
79
+ await displayStats(stats.getStats());
80
+ rl.close();
81
+ process.exit(0);
82
+ };
83
+ rl.on('close', async () => { if (!isProcessing) await displayStats(stats.getStats()); process.exit(0); });
84
+ process.on('SIGINT', handleExit);
85
+ process.on('SIGTERM', handleExit);
86
+
87
+ // Safety net — prevents any unhandled rejection from killing the process
88
+ process.on('unhandledRejection', (err) => {
89
+ try { displayError('Unhandled error: ' + (err?.message || err)); } catch {}
90
+ isProcessing = false;
91
+ showPrompt();
92
+ });
93
+ process.on('uncaughtException', (err) => {
94
+ try { displayError('Uncaught error: ' + (err?.message || err)); } catch {}
95
+ isProcessing = false;
96
+ showPrompt();
97
+ });
98
+
99
+ // ── Prompt ────────────────────────────────────────────────────────────────────
100
+ const showPrompt = () => {
101
+ const dirName = path.basename(workdir);
102
+ const confirmTag = confirmEnabled ? C.dim('') : C.warning(' [no-confirm]');
103
+ rl.question(`\n ${C.dim('[')}${C.primary(dirName)}${C.dim(']')}${confirmTag} ${C.secondary('⟩')} `, handleInput);
104
+ };
105
+
106
+ // ── Confirmation handler ──────────────────────────────────────────────────────
107
+ const handleConfirmation = async (toolName, input) => {
108
+ displayFilePreview(toolName, input);
109
+ await displayConfirmPrompt(toolName);
110
+
111
+ process.stdout.write('\n ' + C.dim('Waiting for your choice [1/2/3/4]... '));
112
+
113
+ return new Promise((resolve) => {
114
+ // Try raw mode first (single keypress)
115
+ if (process.stdin.setRawMode) {
116
+ process.stdin.setRawMode(true);
117
+ process.stdin.resume();
118
+ const onKey = (buf) => {
119
+ const key = buf.toString();
120
+ process.stdin.setRawMode(false);
121
+ process.stdin.removeListener('data', onKey);
122
+ if (key === '1' || key === '\r' || key === '\n') {
123
+ console.log(C.success('1') + C.dim(' — Yes, once'));
124
+ resolve('yes');
125
+ } else if (key === '2') {
126
+ console.log(C.success('2') + C.dim(' — Yes, allow always'));
127
+ resolve('always');
128
+ } else if (key === '3' || key === '\x1b') {
129
+ console.log(C.warning('3') + C.dim(' — Skipped'));
130
+ resolve('skip');
131
+ } else if (key === '4') {
132
+ console.log(C.error('4') + C.dim(' — Suggest changes'));
133
+ resolve('modify');
134
+ } else {
135
+ console.log(C.success('1') + C.dim(' — Yes (default)'));
136
+ resolve('yes');
137
+ }
138
+ };
139
+ process.stdin.on('data', onKey);
140
+ } else {
141
+ // Fallback: readline for Termux/environments without raw mode
142
+ rl.question('', (ans) => {
143
+ const n = ans.trim();
144
+ if (n === '2') resolve('always');
145
+ else if (n === '3') resolve('skip');
146
+ else if (n === '4') resolve('modify');
147
+ else resolve('yes');
148
+ });
149
+ }
150
+ });
151
+ };
152
+
153
+ // ── Input handler ─────────────────────────────────────────────────────────────
154
+ const handleInput = async (input) => {
155
+ const trimmed = input.trim();
156
+ if (!trimmed) { showPrompt(); return; }
157
+
158
+ // Bare number input after /models → select model by number
159
+ if (/^\d+$/.test(trimmed) && lastModelList.length > 0) {
160
+ const n = parseInt(trimmed, 10);
161
+ if (n >= 1 && n <= lastModelList.length) {
162
+ config.model = lastModelList[n - 1]; // full name, not truncated
163
+ agent.updateConfig(config); saveConfig(config);
164
+ console.log(C.success('\n Model: ' + C.accent(config.model) + '\n'));
165
+ } else {
166
+ console.log(C.warning('\n Number out of range (1-' + lastModelList.length + ')\n'));
167
+ }
168
+ showPrompt(); return;
169
+ }
170
+
171
+ // ── Commands ───────────────────────────────────────────────────────────────
172
+ if (trimmed === '/exit' || trimmed === '/quit' || trimmed === '/q') { await handleExit(); return; }
173
+ if (trimmed === '/help' || trimmed === '/h') { displayHelp(); showPrompt(); return; }
174
+ if (trimmed === '/clear' || trimmed === '/cls') {
175
+ await displayBanner(); agent.clearHistory();
176
+ console.log(C.dim(' 🔄 Cleared.\n')); showPrompt(); return;
177
+ }
178
+ if (trimmed === '/stats') { await displayStats(stats.getStats()); showPrompt(); return; }
179
+ if (trimmed === '/reset' || trimmed === '/new') {
180
+ agent.clearHistory();
181
+ console.log(C.success('\n ✅ New conversation.\n')); showPrompt(); return;
182
+ }
183
+ if (trimmed.startsWith('/cd ')) {
184
+ const newDir = trimmed.slice(4).trim();
185
+ const resolved = path.isAbsolute(newDir) ? newDir : path.resolve(workdir, newDir);
186
+ if (existsSync(resolved)) {
187
+ workdir = resolved; agent.setWorkdir(workdir); process.chdir(workdir);
188
+ console.log(C.success(`\n ✅ ${C.primary(workdir)}\n`));
189
+ } else { displayError(`Not found: ${resolved}`); }
190
+ showPrompt(); return;
191
+ }
192
+ if (trimmed === '/setup') {
193
+ config = await setupWizard(config); saveConfig(config); agent.updateConfig(config);
194
+ showPrompt(); return;
195
+ }
196
+ if (trimmed.startsWith('/model ')) {
197
+ const modelArg = trimmed.slice(7).trim();
198
+ // Support /model 3 (pick by number from last /models list)
199
+ const asNum = parseInt(modelArg, 10);
200
+ if (!isNaN(asNum) && asNum >= 1 && lastModelList.length >= asNum) {
201
+ config.model = lastModelList[asNum - 1];
202
+ } else {
203
+ config.model = modelArg;
204
+ }
205
+ agent.updateConfig(config); saveConfig(config);
206
+ console.log(C.success(`\n ✅ Model: ${C.accent(config.model)}\n`)); showPrompt(); return;
207
+ }
208
+ if (trimmed.startsWith('/confirm')) {
209
+ const val = trimmed.split(' ')[1];
210
+ if (val === 'off') { confirmEnabled = false; agent.allowAllTools = true; console.log(C.warning('\n ⚠ Confirmations OFF — agent will run freely\n')); }
211
+ else { confirmEnabled = true; agent.allowAllTools = false; console.log(C.success('\n ✅ Confirmations ON\n')); }
212
+ showPrompt(); return;
213
+ }
214
+ if (trimmed === '/models' || trimmed.startsWith('/models ')) {
215
+ const arg = trimmed.slice(7).trim();
216
+
217
+ if (arg === 'all') {
218
+ process.stdout.write(C.dim(' Fetching all providers...\n'));
219
+ const apiConfigs = { [config.providerKey]: { apiKey: config.apiKey, baseURL: config.baseURL } };
220
+ let allResults;
221
+ try { allResults = await fetchAllProviderModels(apiConfigs); }
222
+ catch(e) { displayError(e.message); showPrompt(); return; }
223
+ console.log('\n' + C.primary(' -- ALL PROVIDERS & MODELS --') + '\n');
224
+ console.log(formatAllModels(allResults));
225
+ lastModelList = Object.values(allResults).flatMap(r => r.models);
226
+ console.log('\n' + C.dim(' /model <number> to switch') + '\n');
227
+ showPrompt(); return;
228
+ }
229
+
230
+ const targetKey = arg || config.providerKey;
231
+ const provider = PROVIDERS[targetKey];
232
+ if (!provider) {
233
+ displayError('Unknown: ' + targetKey + ' | Available: ' + Object.keys(PROVIDERS).join(', '));
234
+ showPrompt(); return;
235
+ }
236
+ process.stdout.write(C.dim(' Fetching ' + provider.label + '...\n'));
237
+ let modResult;
238
+ try {
239
+ const isSame = (targetKey === config.providerKey);
240
+ modResult = await fetchProviderModels(
241
+ targetKey,
242
+ isSame ? config.apiKey : null,
243
+ isSame ? config.baseURL : provider.baseURL
244
+ );
245
+ } catch(e) { displayError(e.message); showPrompt(); return; }
246
+ console.log('\n' + formatModelList(targetKey, modResult) + '\n');
247
+ lastModelList = modResult.models;
248
+ if (modResult.models.length > 0) console.log(C.dim(' /model <number> to switch\n'));
249
+ showPrompt(); return;
250
+ }
251
+ if (trimmed === '/providers') {
252
+ console.log('\n');
253
+ for (const [key, p] of Object.entries(PROVIDERS)) {
254
+ const active = key === config.providerKey;
255
+ console.log(` ${active ? C.accent(key.padEnd(14)) : C.dim(key.padEnd(14))} ${p.label}${active ? C.success(' ◄') : ''}`);
256
+ }
257
+ console.log('\n' + C.dim(' /setup to switch\n')); showPrompt(); return;
258
+ }
259
+ if (trimmed === '/history') {
260
+ for (const m of agent.history) {
261
+ const preview = typeof m.content === 'string' ? m.content.slice(0, 80).replace(/\n/g,' ') : '[structured]';
262
+ console.log(` ${C.accent(m.role.padEnd(12))} ${C.dim(preview)}`);
263
+ }
264
+ console.log(''); showPrompt(); return;
265
+ }
266
+
267
+ // ── Send to agent ──────────────────────────────────────────────────────────
268
+ isProcessing = true;
269
+ stats.addMessage('user');
270
+
271
+ process.stdout.write(C.dim(' ⟳ Thinking...') );
272
+ let _dotTimer = setInterval(() => process.stdout.write(C.dim('.')), 1500);
273
+ let _stopped = false;
274
+ const spinner = { stop: () => { if(_stopped) return; _stopped=true; clearInterval(_dotTimer); process.stdout.write('\n'); }, text: '', start: () => {} };
275
+
276
+ try {
277
+ const finalContent = await agent.chat(trimmed, workdir, async (event) => {
278
+
279
+ // ── Thinking bullets ─────────────────────────────────────────────────
280
+ if (event.type === 'thinking') {
281
+ spinner.stop();
282
+ displayThinking(event.bullets);
283
+ spinner.start();
284
+ spinner.text = C.dim('Planning…');
285
+ return;
286
+ }
287
+
288
+ // ── Confirmation required ────────────────────────────────────────────
289
+ if (event.type === 'confirm_needed') {
290
+ spinner.stop();
291
+ if (confirmEnabled) {
292
+ const decision = await handleConfirmation(event.tool, event.input);
293
+ event.resolve(decision);
294
+ } else {
295
+ // Confirmations off — auto approve, but still show preview
296
+ displayFilePreview(event.tool, event.input);
297
+ console.log(C.dim(' ✓ Auto-approved (confirm off)\n'));
298
+ event.resolve('yes');
299
+ }
300
+ spinner.start();
301
+ spinner.text = C.dim('Executing…');
302
+ return;
303
+ }
304
+
305
+ // ── Feedback request (option 4) ──────────────────────────────────────
306
+ if (event.type === 'request_feedback') {
307
+ spinner.stop();
308
+ const fb = await readLine('\n ' + C.accent('What should be changed? ') + C.dim('→ '));
309
+ event.resolve(fb);
310
+ spinner.start();
311
+ return;
312
+ }
313
+
314
+ // ── Tool start (non-confirm tools) ───────────────────────────────────
315
+ if (event.type === 'tool_start') {
316
+ spinner.stop();
317
+ displayToolCall(event.tool, event.input);
318
+ spinner.start();
319
+ spinner.text = C.dim(`Running ${event.tool}…`);
320
+ return;
321
+ }
322
+
323
+ // ── Tool done ────────────────────────────────────────────────────────
324
+ if (event.type === 'tool_done') {
325
+ spinner.stop();
326
+ displayToolResult(event.tool, event.result, event.durationMs);
327
+ spinner.start();
328
+ spinner.text = C.dim('Thinking…');
329
+ return;
330
+ }
331
+
332
+ // ── Token counter ────────────────────────────────────────────────────
333
+ if (event.type === 'tokens') {
334
+ stats.addTokens(event.inputTokens || 0, event.outputTokens || 0);
335
+ const total = (event.inputTokens || 0) + (event.outputTokens || 0);
336
+ spinner.text = C.dim(`Thinking… `) + C.dim(`[${total.toLocaleString()} tok]`);
337
+ }
338
+ });
339
+
340
+ spinner.stop();
341
+ stats.addMessage('assistant');
342
+ displayMessage('assistant', finalContent);
343
+
344
+ } catch (err) {
345
+ spinner.stop();
346
+ stats.addError();
347
+ displayError(err.message || 'Unexpected error');
348
+ if (err.message?.includes('401') || err.message?.includes('Unauthorized'))
349
+ console.log(C.dim(`\n 💡 Check API key: /setup\n`));
350
+ else if (err.message?.includes('model'))
351
+ console.log(C.dim(`\n 💡 Try: /model <name>\n`));
352
+ }
353
+
354
+ isProcessing = false;
355
+ showPrompt();
356
+ };
357
+
358
+ showPrompt();
359
+ }
360
+
361
+ main().catch((err) => {
362
+ console.error(chalk.red('\n Fatal:'), err.message);
363
+ process.exit(1);
364
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "omni-agent-cli",
3
+ "version": "2.0.0",
4
+ "description": "Universal AI CLI Agent — any provider, any model, full filesystem power",
5
+ "type": "module",
6
+ "bin": {
7
+ "omni-agent": "./index.js",
8
+ "oa": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "setup": "node index.js --setup"
13
+ },
14
+ "dependencies": {
15
+ "axios": "^1.7.2",
16
+ "boxen": "^7.1.1",
17
+ "chalk": "^5.3.0",
18
+ "cli-spinners": "^2.9.2",
19
+ "glob": "^11.0.0",
20
+ "gradient-string": "^2.0.2",
21
+ "inquirer": "^9.3.7",
22
+ "mime-types": "^2.1.35",
23
+ "ora": "^8.1.1",
24
+ "strip-ansi": "^7.1.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "preferGlobal": true
30
+ }