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/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
+ }