decharge-scout 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/.env.example +18 -0
- package/README.md +323 -0
- package/index.js +374 -0
- package/package.json +48 -0
- package/setup.js +389 -0
- package/src/energy-data.js +198 -0
- package/src/geolocation.js +150 -0
- package/src/optimizer.js +165 -0
- package/src/oracle.js +199 -0
- package/src/points.js +159 -0
- package/src/wallet.js +132 -0
- package/src/x402.js +245 -0
package/index.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DeCharge Scout - Energy Grid Data Scout CLI
|
|
5
|
+
*
|
|
6
|
+
* Main entry point for the CLI application that scouts energy grid data,
|
|
7
|
+
* performs optimizations, and submits results to Solana blockchain.
|
|
8
|
+
*
|
|
9
|
+
* Installation:
|
|
10
|
+
* npm install -g .
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* decharge-scout --wallet=<path> [--agent-name=<name>] [--location=<location>]
|
|
14
|
+
*
|
|
15
|
+
* Example:
|
|
16
|
+
* decharge-scout --wallet=./wallet.json --agent-name="MyAgent"
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Command } from 'commander';
|
|
20
|
+
import chalk from 'chalk';
|
|
21
|
+
import ora from 'ora';
|
|
22
|
+
import dotenv from 'dotenv';
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
24
|
+
import { createInterface } from 'readline';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { fileURLToPath } from 'url';
|
|
27
|
+
import { dirname } from 'path';
|
|
28
|
+
import { Keypair } from '@solana/web3.js';
|
|
29
|
+
|
|
30
|
+
// Load environment variables
|
|
31
|
+
dotenv.config();
|
|
32
|
+
|
|
33
|
+
// Import modules
|
|
34
|
+
import { loadWallet, stakeSOL, refundStake } from './src/wallet.js';
|
|
35
|
+
import { fetchEnergyData, fetchElectricityMapsData } from './src/energy-data.js';
|
|
36
|
+
import { findCheapestWindow, calculateSavings } from './src/optimizer.js';
|
|
37
|
+
import { submitToOracle } from './src/oracle.js';
|
|
38
|
+
import { initializePoints, awardPoints, getPoints, savePoints } from './src/points.js';
|
|
39
|
+
import { getLocation } from './src/geolocation.js';
|
|
40
|
+
import { purchasePremiumData } from './src/x402.js';
|
|
41
|
+
|
|
42
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
|
+
const __dirname = dirname(__filename);
|
|
44
|
+
|
|
45
|
+
// Configuration
|
|
46
|
+
const STAKE_AMOUNT = parseFloat(process.env.STAKE_AMOUNT || '0.01');
|
|
47
|
+
const CYCLE_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
48
|
+
const DEFAULT_WALLET_PATH = path.join(__dirname, 'wallet.json');
|
|
49
|
+
|
|
50
|
+
// Global state
|
|
51
|
+
let isRunning = true;
|
|
52
|
+
let totalRuns = 0;
|
|
53
|
+
let stakeTransactionSignature = null;
|
|
54
|
+
|
|
55
|
+
// Readline interface for prompts
|
|
56
|
+
const rl = createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate random agent name
|
|
65
|
+
*/
|
|
66
|
+
function generateAgentName() {
|
|
67
|
+
const randomId = Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
68
|
+
return `Agent-${randomId}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Auto-create wallet if missing
|
|
73
|
+
*/
|
|
74
|
+
async function ensureWallet(walletPath) {
|
|
75
|
+
if (existsSync(walletPath)) {
|
|
76
|
+
return walletPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(chalk.yellow(`\n⚠️ No wallet found at ${walletPath}`));
|
|
80
|
+
const answer = await question('Create a new wallet? (Y/n): ');
|
|
81
|
+
|
|
82
|
+
if (answer.toLowerCase() === 'n') {
|
|
83
|
+
console.log(chalk.blue('\nYou can create a wallet manually:'));
|
|
84
|
+
console.log(chalk.gray(' solana-keygen new --outfile ./wallet.json'));
|
|
85
|
+
console.log(chalk.gray(' Or run: node setup.js\n'));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(chalk.blue('Generating new wallet...'));
|
|
90
|
+
|
|
91
|
+
const keypair = Keypair.generate();
|
|
92
|
+
const secretKey = Array.from(keypair.secretKey);
|
|
93
|
+
|
|
94
|
+
writeFileSync(walletPath, JSON.stringify(secretKey));
|
|
95
|
+
|
|
96
|
+
console.log(chalk.green(`✓ Wallet created: ${keypair.publicKey.toBase58()}`));
|
|
97
|
+
console.log(chalk.yellow('⚠️ IMPORTANT: Backup this wallet file!'));
|
|
98
|
+
console.log(chalk.blue(`\nYou need devnet SOL. Get it from:`));
|
|
99
|
+
console.log(chalk.gray(' solana airdrop 1 ' + keypair.publicKey.toBase58() + ' --url devnet'));
|
|
100
|
+
console.log(chalk.gray(' Or visit: https://faucet.solana.com/\n'));
|
|
101
|
+
|
|
102
|
+
await question('Press Enter after funding your wallet...');
|
|
103
|
+
|
|
104
|
+
return walletPath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Auto-configure .env if needed
|
|
109
|
+
*/
|
|
110
|
+
async function ensureEnvironment() {
|
|
111
|
+
const envPath = path.join(__dirname, '.env');
|
|
112
|
+
|
|
113
|
+
// Create .env from example if missing
|
|
114
|
+
if (!existsSync(envPath)) {
|
|
115
|
+
const examplePath = path.join(__dirname, '.env.example');
|
|
116
|
+
if (existsSync(examplePath)) {
|
|
117
|
+
console.log(chalk.yellow('⚠️ .env file not found, creating from template...'));
|
|
118
|
+
const example = readFileSync(examplePath, 'utf-8');
|
|
119
|
+
writeFileSync(envPath, example);
|
|
120
|
+
console.log(chalk.green('✓ .env file created'));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check for EIA API key
|
|
125
|
+
if (!process.env.EIA_API_KEY || process.env.EIA_API_KEY === 'your_eia_api_key_here') {
|
|
126
|
+
console.log(chalk.yellow('\n⚠️ EIA_API_KEY not configured'));
|
|
127
|
+
console.log(chalk.blue('Get a free API key from: https://www.eia.gov/opendata/register.php'));
|
|
128
|
+
|
|
129
|
+
const answer = await question('Enter your EIA API key (or press Enter to skip): ');
|
|
130
|
+
|
|
131
|
+
if (answer.trim()) {
|
|
132
|
+
// Update .env file
|
|
133
|
+
let envContent = readFileSync(envPath, 'utf-8');
|
|
134
|
+
envContent = envContent.replace(/EIA_API_KEY=.*/g, `EIA_API_KEY=${answer.trim()}`);
|
|
135
|
+
writeFileSync(envPath, envContent);
|
|
136
|
+
|
|
137
|
+
// Update process.env
|
|
138
|
+
process.env.EIA_API_KEY = answer.trim();
|
|
139
|
+
|
|
140
|
+
console.log(chalk.green('✓ API key saved to .env'));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(chalk.yellow('⚠️ Running without EIA API key (will use fallback data sources)'));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Main CLI function
|
|
149
|
+
*/
|
|
150
|
+
async function main(options) {
|
|
151
|
+
console.log(chalk.cyan.bold('\n🔋 DeCharge Scout - Energy Grid Data Scout\n'));
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Auto-configure environment
|
|
155
|
+
await ensureEnvironment();
|
|
156
|
+
|
|
157
|
+
// Auto-handle wallet
|
|
158
|
+
const walletPath = options.wallet || DEFAULT_WALLET_PATH;
|
|
159
|
+
const confirmedWalletPath = await ensureWallet(walletPath);
|
|
160
|
+
|
|
161
|
+
// Load wallet
|
|
162
|
+
const spinner = ora('Loading wallet...').start();
|
|
163
|
+
const wallet = await loadWallet(confirmedWalletPath);
|
|
164
|
+
spinner.succeed(chalk.green(`Wallet loaded: ${wallet.publicKey.toBase58()}`));
|
|
165
|
+
|
|
166
|
+
// Set agent name
|
|
167
|
+
const agentName = options.agentName || generateAgentName();
|
|
168
|
+
console.log(chalk.blue(`🤖 Agent Name: ${agentName}`));
|
|
169
|
+
|
|
170
|
+
// Get location
|
|
171
|
+
const locationSpinner = ora('Detecting location...').start();
|
|
172
|
+
let location = options.location;
|
|
173
|
+
if (!location) {
|
|
174
|
+
location = await getLocation();
|
|
175
|
+
}
|
|
176
|
+
locationSpinner.succeed(chalk.green(`📍 Location: ${location}`));
|
|
177
|
+
|
|
178
|
+
// Initialize points system
|
|
179
|
+
initializePoints(wallet.publicKey.toBase58());
|
|
180
|
+
const currentPoints = getPoints(wallet.publicKey.toBase58());
|
|
181
|
+
console.log(chalk.magenta(`⭐ Current Points: ${currentPoints}`));
|
|
182
|
+
|
|
183
|
+
// Stake SOL
|
|
184
|
+
console.log(chalk.yellow(`\n💰 Staking ${STAKE_AMOUNT} SOL for anti-spam/gas...`));
|
|
185
|
+
const stakeSpinner = ora('Submitting stake transaction...').start();
|
|
186
|
+
stakeTransactionSignature = await stakeSOL(wallet, STAKE_AMOUNT);
|
|
187
|
+
stakeSpinner.succeed(chalk.green(`Stake successful! TX: ${stakeTransactionSignature}`));
|
|
188
|
+
|
|
189
|
+
console.log(chalk.cyan('\n🔄 Starting query cycle (runs every 15 minutes)...'));
|
|
190
|
+
console.log(chalk.gray('Press Ctrl+C to stop and refund stake\n'));
|
|
191
|
+
|
|
192
|
+
// Handle graceful shutdown
|
|
193
|
+
process.on('SIGINT', async () => {
|
|
194
|
+
console.log(chalk.yellow('\n\n⏹️ Stopping scout...'));
|
|
195
|
+
isRunning = false;
|
|
196
|
+
|
|
197
|
+
if (totalRuns > 0) {
|
|
198
|
+
console.log(chalk.blue('💸 Refunding stake...'));
|
|
199
|
+
try {
|
|
200
|
+
const refundTx = await refundStake(wallet, STAKE_AMOUNT);
|
|
201
|
+
console.log(chalk.green(`Refund successful! TX: ${refundTx}`));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error(chalk.red(`Refund failed: ${error.message}`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const finalPoints = getPoints(wallet.publicKey.toBase58());
|
|
208
|
+
console.log(chalk.magenta(`\n⭐ Final Points: ${finalPoints}`));
|
|
209
|
+
console.log(chalk.cyan(`📊 Total Runs: ${totalRuns}\n`));
|
|
210
|
+
|
|
211
|
+
rl.close();
|
|
212
|
+
process.exit(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Main query cycle
|
|
216
|
+
await runQueryCycle(wallet, agentName, location, options);
|
|
217
|
+
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}`));
|
|
220
|
+
console.error(chalk.gray(error.stack));
|
|
221
|
+
rl.close();
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Run the main query cycle
|
|
228
|
+
*/
|
|
229
|
+
async function runQueryCycle(wallet, agentName, location, options) {
|
|
230
|
+
while (isRunning) {
|
|
231
|
+
try {
|
|
232
|
+
totalRuns++;
|
|
233
|
+
console.log(chalk.cyan(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
234
|
+
console.log(chalk.cyan.bold(`🔍 Run #${totalRuns} - ${new Date().toLocaleString()}`));
|
|
235
|
+
console.log(chalk.cyan(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`));
|
|
236
|
+
|
|
237
|
+
// Fetch energy data
|
|
238
|
+
const dataSpinner = ora('Fetching energy grid data...').start();
|
|
239
|
+
let energyData;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
energyData = await fetchEnergyData();
|
|
243
|
+
dataSpinner.succeed(chalk.green(`Fetched ${energyData.length} data points from EIA (ERCOT)`));
|
|
244
|
+
} catch (error) {
|
|
245
|
+
dataSpinner.warn(chalk.yellow(`EIA API failed, trying Electricity Maps...`));
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
energyData = await fetchElectricityMapsData();
|
|
249
|
+
dataSpinner.succeed(chalk.green(`Fetched forecast data from Electricity Maps`));
|
|
250
|
+
} catch (fallbackError) {
|
|
251
|
+
dataSpinner.fail(chalk.red('All data sources failed'));
|
|
252
|
+
throw new Error('Unable to fetch energy data from any source');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Run optimization
|
|
257
|
+
const optSpinner = ora('Running optimization...').start();
|
|
258
|
+
const cheapestWindow = findCheapestWindow(energyData);
|
|
259
|
+
const savings = calculateSavings(energyData, cheapestWindow);
|
|
260
|
+
optSpinner.succeed(chalk.green('Optimization complete'));
|
|
261
|
+
|
|
262
|
+
// Display results
|
|
263
|
+
console.log(chalk.green.bold('\n✨ Optimization Results:'));
|
|
264
|
+
console.log(chalk.white(` Cheapest charge window: ${cheapestWindow.timeWindow}`));
|
|
265
|
+
console.log(chalk.white(` Price: $${cheapestWindow.price.toFixed(4)}/kWh`));
|
|
266
|
+
console.log(chalk.white(` Savings: ${savings.toFixed(1)}%`));
|
|
267
|
+
|
|
268
|
+
// Prepare submission data
|
|
269
|
+
const submissionData = {
|
|
270
|
+
agent_name: agentName,
|
|
271
|
+
location: location,
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
results: {
|
|
274
|
+
cheapest_window: cheapestWindow.timeWindow,
|
|
275
|
+
price: cheapestWindow.price,
|
|
276
|
+
savings: savings,
|
|
277
|
+
data_points: energyData.length
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Submit to oracle
|
|
282
|
+
const submitSpinner = ora('Submitting to DeCharge oracle...').start();
|
|
283
|
+
const txSignature = await submitToOracle(wallet, submissionData);
|
|
284
|
+
submitSpinner.succeed(chalk.green(`Submitted to oracle! TX: ${txSignature}`));
|
|
285
|
+
|
|
286
|
+
// Log dashboard data structure
|
|
287
|
+
console.log(chalk.blue('\n📊 Dashboard Data Structure:'));
|
|
288
|
+
console.log(chalk.gray(JSON.stringify(submissionData, null, 2)));
|
|
289
|
+
|
|
290
|
+
// Optional: Submit to mock dashboard API
|
|
291
|
+
if (process.env.DASHBOARD_API_URL) {
|
|
292
|
+
try {
|
|
293
|
+
const dashboardSpinner = ora('Submitting to dashboard API...').start();
|
|
294
|
+
const response = await fetch(process.env.DASHBOARD_API_URL, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify(submissionData)
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (response.ok) {
|
|
301
|
+
dashboardSpinner.succeed(chalk.green('Dashboard submission successful'));
|
|
302
|
+
} else {
|
|
303
|
+
dashboardSpinner.warn(chalk.yellow(`Dashboard API returned: ${response.status}`));
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
// Silent fail for dashboard - it's optional
|
|
307
|
+
console.log(chalk.gray(`ℹ️ Dashboard API not available (${error.message})`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Award points
|
|
312
|
+
const basePoints = Math.floor(Math.random() * 5) + 1; // 1-5 points
|
|
313
|
+
const bonusPoints = savings > 15 ? 2 : 0; // Bonus for good savings
|
|
314
|
+
const totalPointsEarned = basePoints + bonusPoints;
|
|
315
|
+
|
|
316
|
+
awardPoints(wallet.publicKey.toBase58(), totalPointsEarned);
|
|
317
|
+
const currentPoints = getPoints(wallet.publicKey.toBase58());
|
|
318
|
+
|
|
319
|
+
console.log(chalk.magenta(`\n⭐ Earned ${totalPointsEarned} points! (${basePoints} base${bonusPoints > 0 ? ` + ${bonusPoints} bonus` : ''})`));
|
|
320
|
+
console.log(chalk.magenta(`⭐ Total Points: ${currentPoints}`));
|
|
321
|
+
|
|
322
|
+
// Premium upgrade option
|
|
323
|
+
if (options.premium && totalRuns % 3 === 0) {
|
|
324
|
+
console.log(chalk.yellow('\n🔒 Premium Feature Available!'));
|
|
325
|
+
try {
|
|
326
|
+
const premiumData = await purchasePremiumData(wallet);
|
|
327
|
+
console.log(chalk.green(`Premium forecast data: ${JSON.stringify(premiumData)}`));
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.log(chalk.red(`Premium purchase failed: ${error.message}`));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Save points
|
|
334
|
+
savePoints();
|
|
335
|
+
|
|
336
|
+
// Wait for next cycle
|
|
337
|
+
if (isRunning) {
|
|
338
|
+
const waitMinutes = CYCLE_INTERVAL_MS / 60000;
|
|
339
|
+
console.log(chalk.gray(`\n⏳ Next run in ${waitMinutes} minutes...\n`));
|
|
340
|
+
await sleep(CYCLE_INTERVAL_MS);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error(chalk.red(`\n❌ Cycle error: ${error.message}`));
|
|
345
|
+
console.log(chalk.yellow('Retrying in 5 minutes...'));
|
|
346
|
+
|
|
347
|
+
if (isRunning) {
|
|
348
|
+
await sleep(5 * 60 * 1000);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Sleep utility
|
|
356
|
+
*/
|
|
357
|
+
function sleep(ms) {
|
|
358
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// CLI Setup
|
|
362
|
+
const program = new Command();
|
|
363
|
+
|
|
364
|
+
program
|
|
365
|
+
.name('decharge-scout')
|
|
366
|
+
.description('AI-powered energy grid data scout with Solana integration')
|
|
367
|
+
.version('1.0.0')
|
|
368
|
+
.option('-w, --wallet <path>', 'Path to Solana wallet JSON keypair file (default: ./wallet.json)')
|
|
369
|
+
.option('-a, --agent-name <name>', 'Custom agent name (default: auto-generated)')
|
|
370
|
+
.option('-l, --location <location>', 'Manual location override (default: auto-detect via IP)')
|
|
371
|
+
.option('-p, --premium', 'Enable premium features (x402 micropayments)')
|
|
372
|
+
.action(main);
|
|
373
|
+
|
|
374
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "decharge-scout",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered energy grid data scout with Solana integration",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"decharge-scout": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"setup": "node setup.js",
|
|
12
|
+
"start": "node index.js",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"solana",
|
|
17
|
+
"energy",
|
|
18
|
+
"cli",
|
|
19
|
+
"decharge",
|
|
20
|
+
"blockchain",
|
|
21
|
+
"ev-charging",
|
|
22
|
+
"energy-optimization",
|
|
23
|
+
"ercot",
|
|
24
|
+
"web3"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/sentinelcore/agentone.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/sentinelcore/agentone/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/sentinelcore/agentone#readme",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@solana/web3.js": "^1.95.8",
|
|
38
|
+
"@solana/spl-token": "^0.4.9",
|
|
39
|
+
"commander": "^12.1.0",
|
|
40
|
+
"node-fetch": "^3.3.2",
|
|
41
|
+
"chalk": "^5.3.0",
|
|
42
|
+
"ora": "^8.1.1",
|
|
43
|
+
"dotenv": "^16.4.5"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|