nyxora 26.6.19 → 26.6.21

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.
Files changed (83) hide show
  1. package/README.md +18 -1
  2. package/bin/nyxora.mjs +32 -0
  3. package/dist/packages/core/src/agent/reasoning.js +11 -1
  4. package/dist/packages/core/src/config/parser.js +121 -7
  5. package/dist/packages/core/src/gateway/chat.js +82 -0
  6. package/dist/packages/core/src/gateway/cli.js +63 -0
  7. package/dist/packages/core/src/gateway/server.js +117 -56
  8. package/dist/packages/core/src/gateway/setup.js +39 -22
  9. package/dist/packages/core/src/utils/formatter.test.js +40 -0
  10. package/dist/packages/core/src/utils/skillManager.js +91 -0
  11. package/dist/packages/core/src/utils/userWhitelistManager.js +41 -36
  12. package/dist/packages/core/src/web3/aggregator/aggregatorMainnet.js +2 -2
  13. package/dist/packages/core/src/web3/aggregator/defiRouter.js +3 -0
  14. package/dist/packages/core/src/web3/skills/bridgeToken.js +4 -0
  15. package/dist/packages/core/src/web3/skills/checkRegistryStatus.js +13 -0
  16. package/dist/packages/core/src/web3/skills/customTx.js +2 -0
  17. package/dist/packages/core/src/web3/skills/defiLending.js +2 -0
  18. package/dist/packages/core/src/web3/skills/getPrice.js +11 -6
  19. package/dist/packages/core/src/web3/skills/manageCustomTokens.js +18 -32
  20. package/dist/packages/core/src/web3/skills/marketAnalysis.js +3 -1
  21. package/dist/packages/core/src/web3/skills/mintNft.js +2 -0
  22. package/dist/packages/core/src/web3/skills/provideLiquidity.js +2 -0
  23. package/dist/packages/core/src/web3/skills/revokeApprovals.js +2 -0
  24. package/dist/packages/core/src/web3/skills/swapToken.js +4 -2
  25. package/dist/packages/core/src/web3/skills/transfer.js +2 -0
  26. package/dist/packages/core/src/web3/skills/yieldVault.js +2 -0
  27. package/dist/packages/core/src/web3/utils/tokens.js +9 -1
  28. package/package.json +2 -1
  29. package/packages/core/package.json +1 -1
  30. package/packages/core/src/agent/reasoning.ts +12 -1
  31. package/packages/core/src/config/parser.ts +119 -9
  32. package/packages/core/src/gateway/chat.ts +85 -0
  33. package/packages/core/src/gateway/cli.ts +63 -0
  34. package/packages/core/src/gateway/server.ts +132 -60
  35. package/packages/core/src/gateway/setup.ts +39 -27
  36. package/packages/core/src/utils/formatter.test.ts +41 -0
  37. package/packages/core/src/utils/skillManager.ts +98 -0
  38. package/packages/core/src/utils/userWhitelistManager.ts +48 -39
  39. package/packages/core/src/web3/aggregator/aggregatorMainnet.ts +2 -2
  40. package/packages/core/src/web3/aggregator/defiRouter.ts +4 -0
  41. package/packages/core/src/web3/skills/bridgeToken.ts +3 -0
  42. package/packages/core/src/web3/skills/checkRegistryStatus.ts +13 -0
  43. package/packages/core/src/web3/skills/customTx.ts +1 -0
  44. package/packages/core/src/web3/skills/defiLending.ts +1 -0
  45. package/packages/core/src/web3/skills/getPrice.ts +11 -6
  46. package/packages/core/src/web3/skills/manageCustomTokens.ts +18 -29
  47. package/packages/core/src/web3/skills/marketAnalysis.ts +2 -1
  48. package/packages/core/src/web3/skills/mintNft.ts +1 -0
  49. package/packages/core/src/web3/skills/provideLiquidity.ts +1 -0
  50. package/packages/core/src/web3/skills/revokeApprovals.ts +1 -0
  51. package/packages/core/src/web3/skills/swapToken.ts +3 -2
  52. package/packages/core/src/web3/skills/transfer.ts +1 -0
  53. package/packages/core/src/web3/skills/yieldVault.ts +1 -0
  54. package/packages/core/src/web3/utils/tokens.ts +9 -1
  55. package/packages/dashboard/dist/assets/{index-DnQrbB4c.css → index-CQNHWZtN.css} +1 -1
  56. package/packages/dashboard/dist/assets/index-Di9x08yk.js +16 -0
  57. package/packages/dashboard/dist/index.html +2 -2
  58. package/packages/dashboard/package.json +1 -1
  59. package/packages/mcp-server/package.json +1 -1
  60. package/packages/policy/package.json +1 -1
  61. package/packages/signer/package.json +1 -1
  62. package/dist/packages/core/src/agent/limitOrderManager.js +0 -124
  63. package/dist/packages/core/src/system/pluginManager.js +0 -91
  64. package/dist/packages/core/src/system/skills/installSkill.js +0 -52
  65. package/dist/packages/core/src/test-all-routers.js +0 -81
  66. package/dist/packages/core/src/test-router.js +0 -38
  67. package/dist/packages/core/src/web3/skills/autonomousDefi.js +0 -191
  68. package/dist/packages/core/src/web3/skills/createWallet.js +0 -34
  69. package/dist/packages/core/src/web3/skills/limitOrder.js +0 -106
  70. package/dist/packages/core/src/web3/utils/protocolRegistry.js +0 -46
  71. package/dist/tsconfig.tsbuildinfo +0 -1
  72. package/packages/core/src/__tests__/reasoning.test.ts +0 -81
  73. package/packages/core/src/__tests__/tokens.test.ts +0 -55
  74. package/packages/core/src/__tests__/web3.test.ts +0 -50
  75. package/packages/core/src/agent/reasoning.d.ts.map +0 -1
  76. package/packages/core/src/config/parser.d.ts.map +0 -1
  77. package/packages/core/src/gateway/cli.d.ts.map +0 -1
  78. package/packages/core/src/memory/logger.d.ts.map +0 -1
  79. package/packages/core/src/test-all-routers.ts +0 -59
  80. package/packages/core/src/test-router.ts +0 -49
  81. package/packages/core/src/web3/config.d.ts.map +0 -1
  82. package/packages/core/src/web3/skills/getBalance.d.ts.map +0 -1
  83. package/packages/dashboard/dist/assets/index-BAXifdMN.js +0 -16
@@ -0,0 +1,85 @@
1
+ import { intro, text, spinner, isCancel, cancel } from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import fs from 'fs';
4
+ import { getPath } from '../config/paths';
5
+
6
+ export async function chatInteractive() {
7
+ const tokenFile = getPath('auth.token');
8
+ if (!fs.existsSync(tokenFile)) {
9
+ console.log(pc.red('❌ Nyxora daemon is not running. Please start it with `nyxora start`.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ let token = fs.readFileSync(tokenFile, 'utf8').trim();
14
+ if (token.startsWith('{')) {
15
+ try {
16
+ const parsed = JSON.parse(token);
17
+ token = parsed.token;
18
+ } catch (e) {}
19
+ }
20
+
21
+ const logo = `
22
+ ███╗ ██╗██╗ ██╗██╗ ██╗ ██████╗ ██████╗ █████╗
23
+ ████╗ ██║╚██╗ ██╔╝╚██╗██╔╝██╔═══██╗██╔══██╗██╔══██╗
24
+ ██╔██╗ ██║ ╚████╔╝ ╚███╔╝ ██║ ██║██████╔╝███████║
25
+ ██║╚██╗██║ ╚██╔╝ ██╔██╗ ██║ ██║██╔══██╗██╔══██║
26
+ ██║ ╚████║ ██║ ██╔╝ ██╗╚██████╔╝██║ ██║██║ ██║
27
+ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
28
+ `;
29
+
30
+ console.log(pc.cyan(logo));
31
+ intro(pc.inverse(' Nyxora Interactive Shell '));
32
+ console.log(pc.gray('Type your message and press Enter. Type "exit" or press Ctrl+C to quit.\n'));
33
+
34
+ while (true) {
35
+ const input = await text({
36
+ message: pc.cyan('You:'),
37
+ placeholder: 'Send a message...',
38
+ });
39
+
40
+ if (isCancel(input) || input.toString().trim().toLowerCase() === 'exit' || input.toString().trim().toLowerCase() === 'quit') {
41
+ cancel('Chat session ended.');
42
+ process.exit(0);
43
+ }
44
+
45
+ const messageStr = input.toString().trim();
46
+ if (!messageStr) continue;
47
+
48
+ const s = spinner();
49
+ s.start('Thinking...');
50
+
51
+ try {
52
+ const response = await fetch('http://localhost:3000/api/chat', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'x-nyxora-token': token,
57
+ },
58
+ body: JSON.stringify({ message: messageStr, session_id: 'cli-chat' })
59
+ });
60
+
61
+ if (!response.ok) {
62
+ s.stop(pc.red('API Error.'));
63
+ if (response.status === 401) {
64
+ console.log(pc.red('Unauthorized: Token is invalid. Please restart the daemon.'));
65
+ process.exit(1);
66
+ } else {
67
+ console.log(pc.red(`Gateway returned status ${response.status}`));
68
+ }
69
+ continue;
70
+ }
71
+
72
+ const data = await response.json();
73
+ s.stop(pc.green('Nyxora:'));
74
+
75
+ let finalReply = data.response || '';
76
+ // Strip <think> tags for clean UI
77
+ finalReply = finalReply.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
78
+
79
+ console.log(finalReply + '\n');
80
+ } catch (error) {
81
+ s.stop(pc.red('Connection failed.'));
82
+ console.log(pc.red(`Is the daemon running? (http://localhost:3000)`));
83
+ }
84
+ }
85
+ }
@@ -128,6 +128,69 @@ console.log(`================================`);
128
128
  }
129
129
  }
130
130
 
131
+ // Check for chat command
132
+ if (process.argv.includes('chat')) {
133
+ const { chatInteractive } = await import('./chat');
134
+ await chatInteractive();
135
+ process.exit(0);
136
+ }
137
+
138
+ // Check for uninstall command
139
+ if (process.argv.includes('uninstall')) {
140
+ console.log(pc.cyan('\n🗑️ Nyxora Uninstallation Wizard'));
141
+
142
+ let shouldProceed = process.argv.includes('--force') || process.argv.includes('-y');
143
+
144
+ if (!shouldProceed) {
145
+ const proceed = await confirm({
146
+ message: pc.bgRed(pc.white(' ⚠️ WARNING ')) + pc.yellow(' This will PERMANENTLY WIPE the AI\'s local memory, securely delete your Private Key and Master Key from the OS Keyring, and remove all configuration.\n\nAre you absolutely sure you want to proceed?'),
147
+ });
148
+ if (isCancel(proceed) || !proceed) process.exit(0);
149
+ }
150
+
151
+ console.log(pc.gray('\nStarting cleanup process...'));
152
+
153
+ // 1. Wipe AI Memory
154
+ try {
155
+ const { Logger } = require('../memory/logger');
156
+ const logger = new Logger();
157
+ logger.clear();
158
+ console.log(pc.green('✅ AI memory wiped successfully.'));
159
+ } catch (e: any) {
160
+ console.log(pc.gray('⚠️ Could not clear AI memory (may not exist).'));
161
+ }
162
+
163
+ // 2. Delete OS Keyring entries
164
+ try {
165
+ const { Entry } = await import('@napi-rs/keyring');
166
+ const walletEntry = new Entry('nyxora', 'wallet');
167
+ try { await walletEntry.deletePassword(); } catch(e) {}
168
+ console.log(pc.green('✅ Wallet key removed from OS Keyring.'));
169
+
170
+ const masterEntry = new Entry('nyxora', 'config_master');
171
+ try { await masterEntry.deletePassword(); } catch(e) {}
172
+ console.log(pc.green('✅ Master key removed from OS Keyring.'));
173
+ } catch (e: any) {
174
+ console.log(pc.gray('⚠️ Could not access OS Keyring.'));
175
+ }
176
+
177
+ // 3. Delete ~/.nyxora directory
178
+ try {
179
+ const targetDir = path.join(os.homedir(), '.nyxora');
180
+ if (fs.existsSync(targetDir)) {
181
+ fs.rmSync(targetDir, { recursive: true, force: true });
182
+ console.log(pc.green('✅ Configuration directory (~/.nyxora) deleted.'));
183
+ }
184
+ } catch (e: any) {
185
+ console.log(pc.red(`❌ Failed to delete ~/.nyxora: ${e.message}`));
186
+ }
187
+
188
+ console.log(pc.cyan('\n✨ Nyxora data has been completely removed.'));
189
+ console.log(pc.white('To complete the uninstallation, run:'));
190
+ console.log(pc.green(' npm uninstall -g nyxora\n'));
191
+ process.exit(0);
192
+ }
193
+
131
194
  // 2. Setup boilerplate files if in global mode and they don't exist
132
195
  let isFirstBoot = false;
133
196
  if (isGlobalMode) {
@@ -42,7 +42,7 @@ import { checkPortfolioToolDefinition } from '../web3/skills/checkPortfolio';
42
42
  import { marketAnalysisToolDefinition } from '../web3/skills/marketAnalysis';
43
43
  import { executeApprove, executeAaveSupply, executeVaultDeposit, executeUniv3Mint } from '../web3/skills/executeDefi';
44
44
  import { executeRevokeApproval } from '../web3/skills/revokeApprovals';
45
- import { isSkillActive, toggleSkill } from '../utils/skillManager';
45
+ import { isSkillActive, toggleSkill, syncAllSkillsToConfig } from '../utils/skillManager';
46
46
  import { executeBridge, bridgeTokenToolDefinition } from '../web3/skills/bridgeToken';
47
47
  import { executeMintNft, mintNftToolDefinition } from '../web3/skills/mintNft';
48
48
  import { executeCustomTx, customTxToolDefinition } from '../web3/skills/customTx';
@@ -51,8 +51,11 @@ import { revokeApprovalToolDefinition } from '../web3/skills/revokeApprovals';
51
51
  import { vaultDepositToolDefinition } from '../web3/skills/yieldVault';
52
52
  import { provideLiquidityToolDefinition } from '../web3/skills/provideLiquidity';
53
53
  import { getTxHistoryToolDefinition } from '../web3/skills/getTxHistory';
54
- import { checkRegistryStatus } from '../web3/skills/checkRegistryStatus';
54
+ import { checkRegistryStatus, checkRegistryStatusToolDefinition } from '../web3/skills/checkRegistryStatus';
55
55
  import { createLimitOrderToolDefinition } from '../web3/skills/createLimitOrder';
56
+ import { getUserWhitelist, saveTokenToWhitelist, removeTokenFromWhitelist } from '../utils/userWhitelistManager';
57
+ import { getTokenMetadata } from '../web3/utils/tokens';
58
+ import { ChainName } from '../web3/config';
56
59
 
57
60
  // System Skills
58
61
  import { browseWebsiteToolDefinition } from '../system/skills/browseWeb';
@@ -84,6 +87,9 @@ import { ReflectionEngine } from '../memory/reflection';
84
87
  // Initialize Google Auth
85
88
  initGoogleAuth();
86
89
 
90
+ // Synchronize all active skills to config.yaml on startup
91
+ syncAllSkillsToConfig();
92
+
87
93
  import util from 'util';
88
94
 
89
95
  // Intercept console.log and console.error
@@ -331,8 +337,63 @@ app.post('/api/defi-keys', (req, res) => {
331
337
  }
332
338
  });
333
339
 
340
+ const allSkills = [
341
+ getBalanceToolDefinition,
342
+ transferToolDefinition,
343
+ getPriceToolDefinition,
344
+ swapTokenToolDefinition,
345
+ bridgeTokenToolDefinition,
346
+ mintNftToolDefinition,
347
+ customTxToolDefinition,
348
+ checkAddressToolDefinition,
349
+ getMyAddressToolDefinition,
350
+ checkSecurityToolDefinition,
351
+ checkPortfolioToolDefinition,
352
+ marketAnalysisToolDefinition,
353
+ manageCustomTokensDefinition,
354
+ aaveSupplyToolDefinition,
355
+ revokeApprovalToolDefinition,
356
+ vaultDepositToolDefinition,
357
+ provideLiquidityToolDefinition,
358
+ getTxHistoryToolDefinition,
359
+ createLimitOrderToolDefinition,
360
+ checkRegistryStatusToolDefinition
361
+ ];
362
+
363
+ const systemSkills = [
364
+ runTerminalCommandToolDefinition,
365
+ readLocalFileToolDefinition,
366
+ writeLocalFileToolDefinition,
367
+ generateExcelToolDefinition,
368
+ browseWebsiteToolDefinition,
369
+ updateSecurityPolicyToolDefinition,
370
+
371
+ analyzeDocumentToolDefinition,
372
+ searchWebToolDefinition,
373
+ readGmailInboxToolDefinition,
374
+ listCalendarEventsToolDefinition,
375
+ appendRowToSheetsToolDefinition,
376
+ readGoogleDocsToolDefinition,
377
+ readGoogleFormResponsesToolDefinition,
378
+ editLocalFileToolDefinition,
379
+ gitManagerToolDefinition,
380
+ xManagerToolDefinition,
381
+ notionWorkspaceToolDefinition,
382
+ audioTranscribeToolDefinition,
383
+ summarizeTextToolDefinition
384
+ ];
385
+
334
386
  app.get('/api/stats', (req, res) => {
335
- res.json(Tracker.getStats());
387
+ const stats = Tracker.getStats();
388
+ const dbPath = getPath('memory.db');
389
+
390
+ const activeWeb3 = allSkills.filter(s => isSkillActive(s.function.name)).length;
391
+ const activeSystem = systemSkills.filter(s => isSkillActive(s.function.name)).length;
392
+
393
+ const totalSkills = allSkills.length + systemSkills.length;
394
+ const activeSkills = activeWeb3 + activeSystem;
395
+
396
+ res.json({ ...stats, memoryPath: dbPath, totalSkills, activeSkills });
336
397
  });
337
398
 
338
399
  app.get('/api/logs', (req, res) => {
@@ -340,28 +401,6 @@ app.get('/api/logs', (req, res) => {
340
401
  });
341
402
 
342
403
  app.get('/api/skills', (req, res) => {
343
- const allSkills = [
344
- getBalanceToolDefinition,
345
- transferToolDefinition,
346
- getPriceToolDefinition,
347
- swapTokenToolDefinition,
348
- bridgeTokenToolDefinition,
349
- mintNftToolDefinition,
350
- customTxToolDefinition,
351
- checkAddressToolDefinition,
352
- getMyAddressToolDefinition,
353
- checkSecurityToolDefinition,
354
- checkPortfolioToolDefinition,
355
- marketAnalysisToolDefinition,
356
- manageCustomTokensDefinition,
357
- aaveSupplyToolDefinition,
358
- revokeApprovalToolDefinition,
359
- vaultDepositToolDefinition,
360
- provideLiquidityToolDefinition,
361
- getTxHistoryToolDefinition,
362
- createLimitOrderToolDefinition
363
- ];
364
-
365
404
  const skillsWithStatus = allSkills.map(skill => ({
366
405
  ...skill,
367
406
  isActive: isSkillActive(skill.function.name)
@@ -371,29 +410,6 @@ app.get('/api/skills', (req, res) => {
371
410
  });
372
411
 
373
412
  app.get('/api/skills/system', (req, res) => {
374
- const systemSkills = [
375
- runTerminalCommandToolDefinition,
376
- readLocalFileToolDefinition,
377
- writeLocalFileToolDefinition,
378
- generateExcelToolDefinition,
379
- browseWebsiteToolDefinition,
380
- updateSecurityPolicyToolDefinition,
381
-
382
- analyzeDocumentToolDefinition,
383
- searchWebToolDefinition,
384
- readGmailInboxToolDefinition,
385
- listCalendarEventsToolDefinition,
386
- appendRowToSheetsToolDefinition,
387
- readGoogleDocsToolDefinition,
388
- readGoogleFormResponsesToolDefinition,
389
- editLocalFileToolDefinition,
390
- gitManagerToolDefinition,
391
- xManagerToolDefinition,
392
- notionWorkspaceToolDefinition,
393
- audioTranscribeToolDefinition,
394
- summarizeTextToolDefinition
395
- ];
396
-
397
413
  const skillsWithStatus = systemSkills.map(skill => ({
398
414
  ...skill,
399
415
  isActive: isSkillActive(skill.function.name)
@@ -411,6 +427,44 @@ app.post('/api/skills/toggle', (req, res) => {
411
427
  res.json({ success: true, skillName, active });
412
428
  });
413
429
 
430
+ // Portfolio Whitelist Routes
431
+ app.get('/api/portfolio/whitelist', async (req, res) => {
432
+ const whitelist = getUserWhitelist();
433
+ res.json(whitelist);
434
+ });
435
+
436
+ app.post('/api/portfolio/whitelist', async (req, res) => {
437
+ const { walletAddress, chainName, tokenAddress, symbol, decimals } = req.body;
438
+ if (!walletAddress || !chainName || !tokenAddress) {
439
+ return res.status(400).json({ error: 'Missing required fields' });
440
+ }
441
+ await saveTokenToWhitelist(walletAddress, chainName, tokenAddress, 'manual', symbol, decimals);
442
+ res.json({ success: true });
443
+ });
444
+
445
+ app.delete('/api/portfolio/whitelist', (req, res) => {
446
+ const { walletAddress, chainName, tokenAddress } = req.body;
447
+ if (!walletAddress || !chainName || !tokenAddress) {
448
+ return res.status(400).json({ error: 'Missing required fields' });
449
+ }
450
+ removeTokenFromWhitelist(walletAddress, chainName, tokenAddress);
451
+ res.json({ success: true });
452
+ });
453
+
454
+ app.get('/api/portfolio/token-metadata', async (req, res) => {
455
+ const { chain, address } = req.query;
456
+ if (!chain || !address || typeof address !== 'string') {
457
+ return res.status(400).json({ error: 'Missing chain or address' });
458
+ }
459
+ try {
460
+ const client = getPublicClient(chain as ChainName);
461
+ const metadata = await getTokenMetadata(client, address as `0x${string}`);
462
+ res.json(metadata);
463
+ } catch (err: any) {
464
+ res.status(500).json({ error: err.message });
465
+ }
466
+ });
467
+
414
468
  // Google Workspace Auth Routes
415
469
  app.get('/api/auth/google/url', (req, res) => {
416
470
  const url = getAuthUrl();
@@ -603,6 +657,7 @@ let cachedTrending: string[] | null = null;
603
657
  let lastTrendingFetch = 0;
604
658
 
605
659
  let cachedPrices: Record<string, number> = {};
660
+ let cachedPriceChanges: Record<string, number> = {};
606
661
  let lastPricesFetch = 0;
607
662
 
608
663
  app.get('/api/trending', async (req, res) => {
@@ -629,16 +684,20 @@ app.get('/api/trending', async (req, res) => {
629
684
  }
630
685
  });
631
686
 
687
+ app.get('/api/wallet', async (req, res) => {
688
+ try {
689
+ const userAddress = await getAddress();
690
+ res.json({ address: userAddress });
691
+ } catch (error: any) {
692
+ res.status(500).json({ error: error.message });
693
+ }
694
+ });
695
+
632
696
  app.get('/api/portfolio', async (req, res) => {
633
697
  try {
634
698
  const userAddress = await getAddress();
635
- const customTokensPath = path.join(getPath('custom_tokens.json'));
636
- let customTokens: Record<string, any> = {};
637
- if (fs.existsSync(customTokensPath)) {
638
- try {
639
- customTokens = JSON.parse(fs.readFileSync(customTokensPath, 'utf8'));
640
- } catch (e) {}
641
- }
699
+ const whitelist = getUserWhitelist();
700
+ const userCustomTokens = whitelist[userAddress.toLowerCase()] || [];
642
701
 
643
702
  const portfolio: Record<string, any[]> = {};
644
703
 
@@ -659,11 +718,15 @@ app.get('/api/portfolio', async (req, res) => {
659
718
  });
660
719
  }
661
720
 
662
- // 2. Combine TOKEN_MAP and customTokens for this chain
721
+ // 2. Combine TOKEN_MAP and YAML whitelist for this chain
663
722
  const tokensToQuery = { ...((TOKEN_MAP as any)[chainName] || {}) };
664
- if (customTokens[chainName]) {
665
- Object.assign(tokensToQuery, customTokens[chainName]);
666
- }
723
+
724
+ // Inject whitelisted tokens
725
+ userCustomTokens.forEach(t => {
726
+ if (t.chainName === chainName && t.symbol && t.address) {
727
+ tokensToQuery[t.symbol.toUpperCase()] = t.address;
728
+ }
729
+ });
667
730
 
668
731
  // 3. Query all ERC-20 balances in parallel
669
732
  await Promise.all(Object.entries(tokensToQuery).map(async ([symbol, address]) => {
@@ -682,7 +745,9 @@ app.get('/api/portfolio', async (req, res) => {
682
745
  } as any);
683
746
 
684
747
  const [bal, decimals] = await Promise.all([balPromise, decPromise]) as [bigint, number];
685
- if (bal > 0n) {
748
+ const isCustom = userCustomTokens.some(t => t.chainName === chainName && t.address === address);
749
+
750
+ if (bal > 0n || isCustom) {
686
751
  portfolio[chainName].push({
687
752
  symbol,
688
753
  address,
@@ -722,10 +787,12 @@ app.get('/api/portfolio', async (req, res) => {
722
787
  const uniqueAddrs = Array.from(addressesToFetch);
723
788
  const now = Date.now();
724
789
  let priceMap = cachedPrices;
790
+ let changeMap = cachedPriceChanges;
725
791
 
726
792
  if (uniqueAddrs.length > 0 && now - lastPricesFetch > 2 * 60 * 1000) {
727
793
  try {
728
794
  const newPrices: Record<string, number> = {};
795
+ const newChanges: Record<string, number> = {};
729
796
 
730
797
  await Promise.all(uniqueAddrs.map(async (addr) => {
731
798
  try {
@@ -751,8 +818,10 @@ app.get('/api/portfolio', async (req, res) => {
751
818
 
752
819
  if (baseAddr === addr) {
753
820
  newPrices[addr] = parseFloat(bestPair.priceUsd);
821
+ newChanges[addr] = bestPair.priceChange?.h24 || 0;
754
822
  } else if (quoteAddr === addr && bestPair.priceNative && parseFloat(bestPair.priceNative) > 0) {
755
823
  newPrices[addr] = parseFloat(bestPair.priceUsd) / parseFloat(bestPair.priceNative);
824
+ newChanges[addr] = bestPair.priceChange?.h24 || 0;
756
825
  }
757
826
  }
758
827
  }
@@ -765,7 +834,9 @@ app.get('/api/portfolio', async (req, res) => {
765
834
  console.log('DexScreener Fetched Prices:', newPrices);
766
835
 
767
836
  cachedPrices = { ...cachedPrices, ...newPrices };
837
+ cachedPriceChanges = { ...cachedPriceChanges, ...newChanges };
768
838
  priceMap = cachedPrices;
839
+ changeMap = cachedPriceChanges;
769
840
  lastPricesFetch = now;
770
841
  } catch (e) {
771
842
  console.error('DexScreener fetch error:', e);
@@ -780,6 +851,7 @@ app.get('/api/portfolio', async (req, res) => {
780
851
  lookupAddr = (((TOKEN_MAP as any)[chain]?.[wToken]) || '').toLowerCase();
781
852
  }
782
853
  t.priceUsd = priceMap[lookupAddr] || 0;
854
+ t.priceChange24h = changeMap[lookupAddr] || 0;
783
855
  }
784
856
  }
785
857
 
@@ -7,24 +7,7 @@ import { getAppDir, getPath } from '../config/paths';
7
7
  import { loadConfig, saveConfig, saveApiKeys, saveRpcConfig } from '../config/parser';
8
8
  import crypto from 'crypto';
9
9
 
10
- function encryptKey(privateKey: string, password: string) {
11
- const salt = crypto.randomBytes(16);
12
- const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
13
- const iv = crypto.randomBytes(12);
14
-
15
- const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
16
- let encrypted = cipher.update(privateKey, 'utf8', 'hex');
17
- encrypted += cipher.final('hex');
18
-
19
- const authTag = cipher.getAuthTag().toString('hex');
20
-
21
- return {
22
- salt: salt.toString('hex'),
23
- iv: iv.toString('hex'),
24
- authTag,
25
- encryptedData: encrypted
26
- };
27
- }
10
+
28
11
  import { generatePrivateKey, privateKeyToAccount, generateMnemonic, mnemonicToAccount, english } from 'viem/accounts';
29
12
 
30
13
  export async function runSetupWizard() {
@@ -286,9 +269,9 @@ Provider: ${config.llm.provider}`;
286
269
  privateKey = '0x' + Buffer.from(account.getHdKey().privateKey!).toString('hex');
287
270
  log.success('New Wallet Generated!');
288
271
  log.info(`Address: ${account.address}`);
289
- log.info(`Private Key: ${privateKey}`);
272
+ log.info(`Private Key: [REDACTED - Saved securely to vault]`);
290
273
  log.info(`Seed Phrase (Mnemonic): ${seedPhrase}`);
291
- log.warn('IMPORTANT: Write down these 12 words (or the Private Key) NOW! This is your ONLY backup. The credentials have been securely injected into your local OS vault.');
274
+ log.warn('IMPORTANT: Write down these 12 words NOW! This is your ONLY backup. The credentials have been securely injected into your local OS vault.');
292
275
  }
293
276
  }
294
277
 
@@ -356,19 +339,32 @@ Provider: ${config.llm.provider}`;
356
339
  })) as string;
357
340
  if (isCancel(telegramToken)) return process.exit(0);
358
341
 
359
- if (telegramToken && !authorizedChatId) {
342
+ if (telegramToken && telegramToken.trim() !== '') {
343
+ authorizedChatId = undefined;
344
+ }
345
+
346
+ const activeToken = telegramToken || config.integrations?.telegram?.bot_token;
347
+
348
+ if (activeToken && !authorizedChatId) {
360
349
  const s = spinner();
361
350
  const pin = Math.floor(100000 + Math.random() * 900000).toString();
362
351
 
363
352
  note(pc.cyan(`1. Open Telegram and search for your Bot.\n2. Send this exact message to your bot:\n\n /auth ${pin}\n\nWaiting for your message...`), 'Telegram Pairing Required');
364
353
  s.start(`Waiting for /auth ${pin} on Telegram...`);
365
354
 
355
+ let bot: any = null;
366
356
  try {
367
357
  const { Telegraf } = require('telegraf');
368
- const bot = new Telegraf(telegramToken);
358
+ bot = new Telegraf(activeToken);
369
359
  let paired = false;
370
360
 
361
+ let failedAttempts: Record<string, number> = {};
371
362
  bot.command('auth', (ctx: any) => {
363
+ const chatId = ctx.chat.id.toString();
364
+ if (failedAttempts[chatId] >= 5) {
365
+ return ctx.reply('❌ Too many failed attempts. You are locked out.');
366
+ }
367
+
372
368
  const text = ctx.message.text.split(' ');
373
369
  if (text[1] === pin) {
374
370
  authorizedChatId = ctx.chat.id;
@@ -376,6 +372,7 @@ Provider: ${config.llm.provider}`;
376
372
  ctx.reply('✅ Bot successfully paired with Nyxora!');
377
373
  bot.stop();
378
374
  } else {
375
+ failedAttempts[chatId] = (failedAttempts[chatId] || 0) + 1;
379
376
  ctx.reply('❌ Invalid PIN.');
380
377
  }
381
378
  });
@@ -383,12 +380,25 @@ Provider: ${config.llm.provider}`;
383
380
  bot.launch();
384
381
 
385
382
  // Wait until paired
386
- while (!paired) {
383
+ let attempts = 0;
384
+ while (!paired && attempts < 120) {
387
385
  await new Promise(r => setTimeout(r, 1000));
386
+ attempts++;
387
+ }
388
+ if (!paired) {
389
+ s.stop('Timeout waiting for Telegram pairing. Setup will continue, but Telegram integration is disabled.');
390
+ try { bot.stop(); } catch(e) {}
391
+ authorizedChatId = undefined;
392
+ telegramToken = '';
393
+ } else {
394
+ s.stop(`Bot successfully paired with Chat ID: ${authorizedChatId}`);
388
395
  }
389
- s.stop(`Bot successfully paired with Chat ID: ${authorizedChatId}`);
390
396
  } catch (err: any) {
391
397
  s.stop(`Failed to start bot listener: ${err.message}. You can pair it later.`);
398
+ // Try to stop the bot if it was initialized before the error (L17)
399
+ try {
400
+ if (bot) bot.stop();
401
+ } catch(e) {}
392
402
  }
393
403
  }
394
404
  }
@@ -403,11 +413,11 @@ Provider: ${config.llm.provider}`;
403
413
  config.agent.default_chain = defaultChain as string;
404
414
 
405
415
  if (!config.skills) config.skills = { web3: [], os: [] } as any;
406
- config.skills.web3 = activeWeb3Skills;
407
- config.skills.os = activeOsSkills;
416
+ config.skills!.web3 = activeWeb3Skills;
417
+ config.skills!.os = activeOsSkills;
408
418
 
409
419
  if (!config.channels) config.channels = { active: [] } as any;
410
- config.channels.active = activeChannels;
420
+ config.channels!.active = activeChannels;
411
421
 
412
422
  const newApiKeys: Record<string, string> = {};
413
423
  if (apiKey) {
@@ -442,6 +452,8 @@ Provider: ${config.llm.provider}`;
442
452
 
443
453
  if (authorizedChatId) {
444
454
  config.integrations.telegram.authorized_chat_id = authorizedChatId;
455
+ } else if (config.integrations.telegram) {
456
+ delete config.integrations.telegram.authorized_chat_id;
445
457
  }
446
458
 
447
459
  saveConfig(config);
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatTransactionSuccess, formatTransactionError } from './formatter';
3
+ import { PendingTransaction } from '../agent/transactionManager';
4
+
5
+ describe('Formatter Utilities', () => {
6
+ it('should format transaction success correctly for swap', () => {
7
+ const tx: PendingTransaction = {
8
+ id: 'test-1',
9
+ type: 'swap',
10
+ chainName: 'ethereum',
11
+ status: 'pending',
12
+ nonce: "0",
13
+ details: {
14
+ fromToken: 'eth',
15
+ toToken: 'usdc',
16
+ amountStr: '1'
17
+ },
18
+ createdAt: Date.now()
19
+ };
20
+ const rawResult = '{"txHash":"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}';
21
+ const output = formatTransactionSuccess(tx, rawResult);
22
+ expect(output).toContain('Network:** Ethereum');
23
+ expect(output).toContain('Action:** Swapped 1 ETH to USDC');
24
+ expect(output).toContain('Tx Hash:** `0x1234...cdef`');
25
+ });
26
+
27
+ it('should format transaction error correctly', () => {
28
+ const tx: PendingTransaction = {
29
+ id: 'test-2',
30
+ type: 'bridge',
31
+ chainName: 'base_sepolia',
32
+ status: 'failed',
33
+ nonce: "0",
34
+ details: {},
35
+ createdAt: Date.now()
36
+ };
37
+ const output = formatTransactionError(tx, 'Insufficient funds');
38
+ expect(output).toContain('Transaction Failed (Base Sepolia)');
39
+ expect(output).toContain('Error: Insufficient funds');
40
+ });
41
+ });