@emtai/xray-vision 1.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/bin/xray-vision.mjs +378 -0
- package/package.json +25 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* XRay-Vision CLI — One-time setup for Claude Code
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx @emtai/xray-vision init
|
|
8
|
+
* npx @emtai/xray-vision init --no-instructions
|
|
9
|
+
* npx @emtai/xray-vision init --no-browser
|
|
10
|
+
* npx @emtai/xray-vision status
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Constants
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
const MCP_SERVER_URL = 'https://mcp.emtailabs.com/mcp';
|
|
23
|
+
const AUTH_URL = 'https://mcp.emtailabs.com/authorize';
|
|
24
|
+
const DASHBOARD_URL = 'https://emtai-xray.emtailabs.com/dashboard';
|
|
25
|
+
|
|
26
|
+
const MCP_CONFIG = {
|
|
27
|
+
'xray-vision': {
|
|
28
|
+
type: 'http',
|
|
29
|
+
url: MCP_SERVER_URL,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// ANSI Colors (no dependencies)
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
const c = {
|
|
38
|
+
reset: '\x1b[0m',
|
|
39
|
+
bold: '\x1b[1m',
|
|
40
|
+
dim: '\x1b[2m',
|
|
41
|
+
green: '\x1b[32m',
|
|
42
|
+
blue: '\x1b[34m',
|
|
43
|
+
cyan: '\x1b[36m',
|
|
44
|
+
yellow: '\x1b[33m',
|
|
45
|
+
red: '\x1b[31m',
|
|
46
|
+
magenta: '\x1b[35m',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ok = (msg) => console.log(`${c.green}✓${c.reset} ${msg}`);
|
|
50
|
+
const info = (msg) => console.log(`${c.blue}ℹ${c.reset} ${msg}`);
|
|
51
|
+
const warn = (msg) => console.log(`${c.yellow}⚠${c.reset} ${msg}`);
|
|
52
|
+
const err = (msg) => console.log(`${c.red}✗${c.reset} ${msg}`);
|
|
53
|
+
const step = (n, msg) => console.log(`\n${c.cyan}[${n}]${c.reset} ${c.bold}${msg}${c.reset}`);
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Paths
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
const HOME = os.homedir();
|
|
60
|
+
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
61
|
+
const MCP_JSON_PATH = path.join(CLAUDE_DIR, 'mcp.json');
|
|
62
|
+
const INSTRUCTIONS_PATH = path.join(CLAUDE_DIR, 'instructions.md');
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// XRay Instructions (embedded — preloaded into Claude's context)
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
const XRAY_INSTRUCTIONS = `# XRay-Vision — AI Code Intelligence
|
|
69
|
+
|
|
70
|
+
You have access to XRay-Vision MCP tools for deep code analysis. Use them for ALL code-related work.
|
|
71
|
+
|
|
72
|
+
## Core Rules
|
|
73
|
+
|
|
74
|
+
1. **XRay-first search** — Use \`fnid_lookup\`, \`graph_query\`, \`fnid_source\` instead of Grep/Glob/Read for code searches
|
|
75
|
+
2. **Before code changes** — Run \`fnid_lookup\` to understand the function, \`graph_query\` to check callers and dependencies
|
|
76
|
+
3. **After code changes** — Run \`indexer_changed\` to update the index, then \`xray_full\` to verify no regressions
|
|
77
|
+
4. **Before commits** — Run \`xray_security\` to catch vulnerabilities
|
|
78
|
+
|
|
79
|
+
## Quick Reference
|
|
80
|
+
|
|
81
|
+
### Finding Code
|
|
82
|
+
| Task | XRay Tool | NOT This |
|
|
83
|
+
|------|-----------|----------|
|
|
84
|
+
| Find a function | \`fnid_lookup name="foo"\` | Grep for "function foo" |
|
|
85
|
+
| Read function source | \`fnid_source fnid="..."\` | Read entire file |
|
|
86
|
+
| Find callers | \`graph_query "MATCH (c)-[:CALLS]->(f {name:'foo'}) RETURN c"\` | Grep for "foo(" |
|
|
87
|
+
| Find dependencies | \`graph_query\` with CALLS/IMPORTS patterns | Manual file reading |
|
|
88
|
+
|
|
89
|
+
### Repository Setup
|
|
90
|
+
\`\`\`
|
|
91
|
+
github_clone url="https://github.com/owner/repo" # Clone
|
|
92
|
+
indexer_full repo="<repoId>" # Full index (first time)
|
|
93
|
+
indexer_changed repo="<repoId>" # Incremental (after changes)
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
### Analysis Tools
|
|
97
|
+
- \`xray_full\` — Comprehensive health check (run after every change)
|
|
98
|
+
- \`xray_security\` — Security vulnerabilities (run before every commit)
|
|
99
|
+
- \`xray_complexity\` — Cyclomatic/cognitive complexity hotspots
|
|
100
|
+
- \`xray_architecture\` — Circular deps, layer violations, modularity
|
|
101
|
+
- \`xray_debt\` — Technical debt estimation with quick wins
|
|
102
|
+
- \`xray_smells\` — God objects, long methods, anti-patterns
|
|
103
|
+
- \`xray_testing\` — Test coverage, fragile tests
|
|
104
|
+
- \`xray_hotspots\` — High-churn + high-complexity files
|
|
105
|
+
|
|
106
|
+
### Code Navigation
|
|
107
|
+
- \`fnid_lookup\` — Find function by name/file
|
|
108
|
+
- \`fnid_source\` — Get function source code
|
|
109
|
+
- \`graph_query\` — Custom Cypher queries (no LIMIT clause!)
|
|
110
|
+
- \`file_read_range\` — Read specific lines from a file
|
|
111
|
+
|
|
112
|
+
### Remediation
|
|
113
|
+
- \`remediate_dead_code\` — Remove dead code (creates backups)
|
|
114
|
+
- \`remediate_duplicate\` — Merge duplicate functions
|
|
115
|
+
- \`refactor_extract_function\` — Extract code to new function
|
|
116
|
+
- \`refactor_rename\` — Rename across codebase
|
|
117
|
+
|
|
118
|
+
### Memory
|
|
119
|
+
- \`remember\` — Store findings, patterns, decisions
|
|
120
|
+
- \`evoke\` — Search memories by query/category
|
|
121
|
+
|
|
122
|
+
## Workflow: Feature Development
|
|
123
|
+
1. \`fnid_lookup\` + \`graph_query\` → understand existing code
|
|
124
|
+
2. \`xray_architecture\` → verify where new code fits
|
|
125
|
+
3. Implement the feature
|
|
126
|
+
4. \`indexer_changed\` → update index
|
|
127
|
+
5. \`xray_full\` + \`xray_security\` → verify no regressions
|
|
128
|
+
6. \`remember\` → record patterns/decisions
|
|
129
|
+
|
|
130
|
+
## Graph Query Rules
|
|
131
|
+
- **NEVER** add \`LIMIT\` to queries — you WILL miss critical data
|
|
132
|
+
- Use \`WHERE\` to filter, \`ORDER BY\` to organize
|
|
133
|
+
- Always include \`tenantId\` in queries for multi-tenant isolation
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Helpers
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
function openBrowser(url) {
|
|
141
|
+
try {
|
|
142
|
+
const platform = process.platform;
|
|
143
|
+
if (platform === 'darwin') {
|
|
144
|
+
execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
145
|
+
} else if (platform === 'win32') {
|
|
146
|
+
execSync(`start "" "${url}"`, { stdio: 'ignore' });
|
|
147
|
+
} else {
|
|
148
|
+
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readJsonFile(filePath) {
|
|
157
|
+
try {
|
|
158
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
159
|
+
return JSON.parse(content);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeJsonFile(filePath, data) {
|
|
166
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Commands
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
function cmdInit(flags) {
|
|
174
|
+
const skipInstructions = flags.includes('--no-instructions');
|
|
175
|
+
const skipBrowser = flags.includes('--no-browser');
|
|
176
|
+
|
|
177
|
+
console.log(`\n${c.bold}${c.magenta} XRay-Vision${c.reset} ${c.dim}— One-time setup for Claude Code${c.reset}\n`);
|
|
178
|
+
|
|
179
|
+
// ── Step 1: Ensure ~/.claude/ exists ──
|
|
180
|
+
step(1, 'Checking Claude Code directory');
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
183
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
184
|
+
ok(`Created ${c.cyan}~/.claude/${c.reset}`);
|
|
185
|
+
} else {
|
|
186
|
+
ok(`Found ${c.cyan}~/.claude/${c.reset}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Step 2: Add MCP config ──
|
|
190
|
+
step(2, 'Configuring MCP server');
|
|
191
|
+
|
|
192
|
+
let mcpJson = readJsonFile(MCP_JSON_PATH);
|
|
193
|
+
let configChanged = false;
|
|
194
|
+
|
|
195
|
+
if (!mcpJson) {
|
|
196
|
+
mcpJson = { mcpServers: {} };
|
|
197
|
+
configChanged = true;
|
|
198
|
+
info('Creating new mcp.json');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!mcpJson.mcpServers) {
|
|
202
|
+
mcpJson.mcpServers = {};
|
|
203
|
+
configChanged = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const existing = mcpJson.mcpServers['xray-vision'];
|
|
207
|
+
if (existing) {
|
|
208
|
+
// Check if it's the old npx/stdio config or has Bearer headers
|
|
209
|
+
const isOldConfig =
|
|
210
|
+
existing.command ||
|
|
211
|
+
existing.args ||
|
|
212
|
+
existing.headers?.Authorization?.includes('Bearer xray-') ||
|
|
213
|
+
existing.url !== MCP_SERVER_URL;
|
|
214
|
+
|
|
215
|
+
if (isOldConfig) {
|
|
216
|
+
mcpJson.mcpServers['xray-vision'] = { ...MCP_CONFIG['xray-vision'] };
|
|
217
|
+
configChanged = true;
|
|
218
|
+
ok(`Updated xray-vision config ${c.dim}(replaced old config)${c.reset}`);
|
|
219
|
+
} else {
|
|
220
|
+
ok(`xray-vision already configured ${c.dim}(up to date)${c.reset}`);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
mcpJson.mcpServers['xray-vision'] = { ...MCP_CONFIG['xray-vision'] };
|
|
224
|
+
configChanged = true;
|
|
225
|
+
ok('Added xray-vision MCP server');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (configChanged) {
|
|
229
|
+
writeJsonFile(MCP_JSON_PATH, mcpJson);
|
|
230
|
+
ok(`Saved ${c.cyan}~/.claude/mcp.json${c.reset}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(`\n ${c.dim}Config:${c.reset}`);
|
|
234
|
+
console.log(` ${c.cyan}${JSON.stringify(MCP_CONFIG['xray-vision'], null, 2).split('\n').join('\n ')}${c.reset}`);
|
|
235
|
+
|
|
236
|
+
// ── Step 3: Install instructions ──
|
|
237
|
+
if (!skipInstructions) {
|
|
238
|
+
step(3, 'Installing XRay instructions');
|
|
239
|
+
|
|
240
|
+
const existingInstructions = fs.existsSync(INSTRUCTIONS_PATH)
|
|
241
|
+
? fs.readFileSync(INSTRUCTIONS_PATH, 'utf-8')
|
|
242
|
+
: '';
|
|
243
|
+
|
|
244
|
+
if (existingInstructions.includes('XRay-Vision')) {
|
|
245
|
+
ok(`Instructions already present ${c.dim}(~/.claude/instructions.md)${c.reset}`);
|
|
246
|
+
} else {
|
|
247
|
+
// Append to existing instructions or create new
|
|
248
|
+
const content = existingInstructions
|
|
249
|
+
? existingInstructions.trimEnd() + '\n\n' + XRAY_INSTRUCTIONS
|
|
250
|
+
: XRAY_INSTRUCTIONS;
|
|
251
|
+
|
|
252
|
+
fs.writeFileSync(INSTRUCTIONS_PATH, content, 'utf-8');
|
|
253
|
+
ok(`Installed XRay instructions → ${c.cyan}~/.claude/instructions.md${c.reset}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Step 4: Open browser for auth ──
|
|
258
|
+
const browserStep = skipInstructions ? 3 : 4;
|
|
259
|
+
|
|
260
|
+
if (!skipBrowser) {
|
|
261
|
+
step(browserStep, 'Opening browser for authentication');
|
|
262
|
+
|
|
263
|
+
info('Claude Code will authenticate via Cloudflare Access on first connect.');
|
|
264
|
+
info('Opening browser to verify your account is set up...\n');
|
|
265
|
+
|
|
266
|
+
const opened = openBrowser(DASHBOARD_URL);
|
|
267
|
+
if (opened) {
|
|
268
|
+
ok('Browser opened — sign in to verify your account');
|
|
269
|
+
} else {
|
|
270
|
+
warn(`Could not open browser. Visit manually:\n ${c.cyan}${DASHBOARD_URL}${c.reset}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Done ──
|
|
275
|
+
console.log(`\n${c.green}${c.bold} Setup complete!${c.reset}\n`);
|
|
276
|
+
console.log(` ${c.bold}Next steps:${c.reset}`);
|
|
277
|
+
console.log(` ${c.cyan}1.${c.reset} Restart Claude Code ${c.dim}(required for MCP config to load)${c.reset}`);
|
|
278
|
+
console.log(` ${c.cyan}2.${c.reset} Run ${c.cyan}/mcp${c.reset} to verify xray-vision is connected`);
|
|
279
|
+
console.log(` ${c.cyan}3.${c.reset} A browser window will open for Cloudflare auth on first connect`);
|
|
280
|
+
console.log(` ${c.cyan}4.${c.reset} Clone a repo: ${c.cyan}github_clone url="https://github.com/you/repo"${c.reset}`);
|
|
281
|
+
console.log(` ${c.cyan}5.${c.reset} Run full analysis: ${c.cyan}indexer_full${c.reset} then ${c.cyan}xray_full${c.reset}\n`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function cmdStatus() {
|
|
285
|
+
console.log(`\n${c.bold}${c.magenta} XRay-Vision${c.reset} ${c.dim}— Status check${c.reset}\n`);
|
|
286
|
+
|
|
287
|
+
// Check mcp.json
|
|
288
|
+
const mcpJson = readJsonFile(MCP_JSON_PATH);
|
|
289
|
+
const xrayConfig = mcpJson?.mcpServers?.['xray-vision'];
|
|
290
|
+
|
|
291
|
+
if (xrayConfig) {
|
|
292
|
+
if (xrayConfig.url === MCP_SERVER_URL && xrayConfig.type === 'http' && !xrayConfig.headers) {
|
|
293
|
+
ok(`MCP config: ${c.green}OAuth (current)${c.reset}`);
|
|
294
|
+
} else if (xrayConfig.headers?.Authorization) {
|
|
295
|
+
warn(`MCP config: ${c.yellow}API key auth (outdated)${c.reset} — run ${c.cyan}npx @emtai/xray-vision init${c.reset} to upgrade`);
|
|
296
|
+
} else if (xrayConfig.command) {
|
|
297
|
+
warn(`MCP config: ${c.yellow}npx bridge (outdated)${c.reset} — run ${c.cyan}npx @emtai/xray-vision init${c.reset} to upgrade`);
|
|
298
|
+
} else {
|
|
299
|
+
info(`MCP config: ${c.blue}unknown format${c.reset}`);
|
|
300
|
+
}
|
|
301
|
+
console.log(` ${c.dim}URL: ${xrayConfig.url || 'not set'}${c.reset}`);
|
|
302
|
+
console.log(` ${c.dim}Type: ${xrayConfig.type || 'not set'}${c.reset}`);
|
|
303
|
+
} else {
|
|
304
|
+
err(`MCP config: ${c.red}not found${c.reset} — run ${c.cyan}npx @emtai/xray-vision init${c.reset}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check instructions
|
|
308
|
+
if (fs.existsSync(INSTRUCTIONS_PATH)) {
|
|
309
|
+
const content = fs.readFileSync(INSTRUCTIONS_PATH, 'utf-8');
|
|
310
|
+
if (content.includes('XRay-Vision')) {
|
|
311
|
+
ok(`Instructions: ${c.green}installed${c.reset} ${c.dim}(~/.claude/instructions.md)${c.reset}`);
|
|
312
|
+
} else {
|
|
313
|
+
warn(`Instructions: ${c.yellow}file exists but no XRay content${c.reset}`);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
info(`Instructions: ${c.blue}not installed${c.reset} ${c.dim}(optional)${c.reset}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log('');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function cmdHelp() {
|
|
323
|
+
console.log(`
|
|
324
|
+
${c.bold}${c.magenta} XRay-Vision CLI${c.reset} — Setup tool for Claude Code
|
|
325
|
+
|
|
326
|
+
${c.bold} Usage:${c.reset}
|
|
327
|
+
npx @emtai/xray-vision ${c.cyan}init${c.reset} Set up XRay-Vision in Claude Code
|
|
328
|
+
npx @emtai/xray-vision ${c.cyan}status${c.reset} Check current configuration
|
|
329
|
+
npx @emtai/xray-vision ${c.cyan}help${c.reset} Show this help
|
|
330
|
+
|
|
331
|
+
${c.bold} Init Flags:${c.reset}
|
|
332
|
+
${c.dim}--no-instructions${c.reset} Skip installing XRay instructions
|
|
333
|
+
${c.dim}--no-browser${c.reset} Skip opening browser for authentication
|
|
334
|
+
|
|
335
|
+
${c.bold} What init does:${c.reset}
|
|
336
|
+
1. Adds xray-vision to ${c.cyan}~/.claude/mcp.json${c.reset}
|
|
337
|
+
2. Installs XRay workflow instructions to ${c.cyan}~/.claude/instructions.md${c.reset}
|
|
338
|
+
3. Opens browser for Cloudflare authentication
|
|
339
|
+
|
|
340
|
+
${c.bold} Learn more:${c.reset}
|
|
341
|
+
Dashboard: ${c.cyan}https://emtai-xray.emtailabs.com/dashboard${c.reset}
|
|
342
|
+
Pricing: ${c.cyan}https://emtai-xray.emtailabs.com/pricing${c.reset}
|
|
343
|
+
`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// Main
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
const args = process.argv.slice(2);
|
|
351
|
+
const command = args[0] || 'help';
|
|
352
|
+
const flags = args.slice(1);
|
|
353
|
+
|
|
354
|
+
switch (command) {
|
|
355
|
+
case 'init':
|
|
356
|
+
case 'setup':
|
|
357
|
+
case 'install':
|
|
358
|
+
cmdInit(flags);
|
|
359
|
+
break;
|
|
360
|
+
case 'status':
|
|
361
|
+
case 'check':
|
|
362
|
+
cmdStatus();
|
|
363
|
+
break;
|
|
364
|
+
case 'help':
|
|
365
|
+
case '--help':
|
|
366
|
+
case '-h':
|
|
367
|
+
cmdHelp();
|
|
368
|
+
break;
|
|
369
|
+
default:
|
|
370
|
+
// If no recognized command, default to init (for `npx @emtai/xray-vision` with no args)
|
|
371
|
+
if (command.startsWith('-')) {
|
|
372
|
+
cmdInit([command, ...flags]);
|
|
373
|
+
} else {
|
|
374
|
+
err(`Unknown command: ${command}`);
|
|
375
|
+
cmdHelp();
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emtai/xray-vision",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "One-time setup for XRay-Vision MCP in Claude Code — adds config, instructions, and authenticates",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xray-vision": "./bin/xray-vision.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/**/*"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"xray-vision",
|
|
14
|
+
"mcp",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"code-analysis",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"setup"
|
|
19
|
+
],
|
|
20
|
+
"author": "eMTAi Labs",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|