nyxora 26.6.20 → 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 (80) hide show
  1. package/README.md +18 -1
  2. package/bin/nyxora.mjs +32 -0
  3. package/dist/packages/core/src/agent/reasoning.js +10 -0
  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 +100 -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/manageCustomTokens.js +18 -32
  19. package/dist/packages/core/src/web3/skills/marketAnalysis.js +3 -1
  20. package/dist/packages/core/src/web3/skills/mintNft.js +2 -0
  21. package/dist/packages/core/src/web3/skills/provideLiquidity.js +2 -0
  22. package/dist/packages/core/src/web3/skills/revokeApprovals.js +2 -0
  23. package/dist/packages/core/src/web3/skills/swapToken.js +4 -2
  24. package/dist/packages/core/src/web3/skills/transfer.js +2 -0
  25. package/dist/packages/core/src/web3/skills/yieldVault.js +2 -0
  26. package/dist/packages/core/src/web3/utils/tokens.js +9 -1
  27. package/package.json +2 -1
  28. package/packages/core/package.json +1 -1
  29. package/packages/core/src/agent/reasoning.ts +11 -0
  30. package/packages/core/src/config/parser.ts +119 -9
  31. package/packages/core/src/gateway/chat.ts +85 -0
  32. package/packages/core/src/gateway/cli.ts +63 -0
  33. package/packages/core/src/gateway/server.ts +115 -60
  34. package/packages/core/src/gateway/setup.ts +39 -27
  35. package/packages/core/src/utils/formatter.test.ts +41 -0
  36. package/packages/core/src/utils/skillManager.ts +98 -0
  37. package/packages/core/src/utils/userWhitelistManager.ts +48 -39
  38. package/packages/core/src/web3/aggregator/aggregatorMainnet.ts +2 -2
  39. package/packages/core/src/web3/aggregator/defiRouter.ts +4 -0
  40. package/packages/core/src/web3/skills/bridgeToken.ts +3 -0
  41. package/packages/core/src/web3/skills/checkRegistryStatus.ts +13 -0
  42. package/packages/core/src/web3/skills/customTx.ts +1 -0
  43. package/packages/core/src/web3/skills/defiLending.ts +1 -0
  44. package/packages/core/src/web3/skills/manageCustomTokens.ts +18 -29
  45. package/packages/core/src/web3/skills/marketAnalysis.ts +2 -1
  46. package/packages/core/src/web3/skills/mintNft.ts +1 -0
  47. package/packages/core/src/web3/skills/provideLiquidity.ts +1 -0
  48. package/packages/core/src/web3/skills/revokeApprovals.ts +1 -0
  49. package/packages/core/src/web3/skills/swapToken.ts +3 -2
  50. package/packages/core/src/web3/skills/transfer.ts +1 -0
  51. package/packages/core/src/web3/skills/yieldVault.ts +1 -0
  52. package/packages/core/src/web3/utils/tokens.ts +9 -1
  53. package/packages/dashboard/dist/assets/index-Di9x08yk.js +16 -0
  54. package/packages/dashboard/dist/index.html +1 -1
  55. package/packages/dashboard/package.json +1 -1
  56. package/packages/mcp-server/package.json +1 -1
  57. package/packages/policy/package.json +1 -1
  58. package/packages/signer/package.json +1 -1
  59. package/dist/packages/core/src/agent/limitOrderManager.js +0 -124
  60. package/dist/packages/core/src/system/pluginManager.js +0 -91
  61. package/dist/packages/core/src/system/skills/installSkill.js +0 -52
  62. package/dist/packages/core/src/test-all-routers.js +0 -81
  63. package/dist/packages/core/src/test-router.js +0 -38
  64. package/dist/packages/core/src/web3/skills/autonomousDefi.js +0 -191
  65. package/dist/packages/core/src/web3/skills/createWallet.js +0 -34
  66. package/dist/packages/core/src/web3/skills/limitOrder.js +0 -106
  67. package/dist/packages/core/src/web3/utils/protocolRegistry.js +0 -46
  68. package/dist/tsconfig.tsbuildinfo +0 -1
  69. package/packages/core/src/__tests__/reasoning.test.ts +0 -81
  70. package/packages/core/src/__tests__/tokens.test.ts +0 -55
  71. package/packages/core/src/__tests__/web3.test.ts +0 -50
  72. package/packages/core/src/agent/reasoning.d.ts.map +0 -1
  73. package/packages/core/src/config/parser.d.ts.map +0 -1
  74. package/packages/core/src/gateway/cli.d.ts.map +0 -1
  75. package/packages/core/src/memory/logger.d.ts.map +0 -1
  76. package/packages/core/src/test-all-routers.ts +0 -59
  77. package/packages/core/src/test-router.ts +0 -49
  78. package/packages/core/src/web3/config.d.ts.map +0 -1
  79. package/packages/core/src/web3/skills/getBalance.d.ts.map +0 -1
  80. package/packages/dashboard/dist/assets/index-O2m42q4p.js +0 -16
@@ -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();
@@ -642,13 +696,8 @@ app.get('/api/wallet', async (req, res) => {
642
696
  app.get('/api/portfolio', async (req, res) => {
643
697
  try {
644
698
  const userAddress = await getAddress();
645
- const customTokensPath = path.join(getPath('custom_tokens.json'));
646
- let customTokens: Record<string, any> = {};
647
- if (fs.existsSync(customTokensPath)) {
648
- try {
649
- customTokens = JSON.parse(fs.readFileSync(customTokensPath, 'utf8'));
650
- } catch (e) {}
651
- }
699
+ const whitelist = getUserWhitelist();
700
+ const userCustomTokens = whitelist[userAddress.toLowerCase()] || [];
652
701
 
653
702
  const portfolio: Record<string, any[]> = {};
654
703
 
@@ -669,11 +718,15 @@ app.get('/api/portfolio', async (req, res) => {
669
718
  });
670
719
  }
671
720
 
672
- // 2. Combine TOKEN_MAP and customTokens for this chain
721
+ // 2. Combine TOKEN_MAP and YAML whitelist for this chain
673
722
  const tokensToQuery = { ...((TOKEN_MAP as any)[chainName] || {}) };
674
- if (customTokens[chainName]) {
675
- Object.assign(tokensToQuery, customTokens[chainName]);
676
- }
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
+ });
677
730
 
678
731
  // 3. Query all ERC-20 balances in parallel
679
732
  await Promise.all(Object.entries(tokensToQuery).map(async ([symbol, address]) => {
@@ -692,7 +745,9 @@ app.get('/api/portfolio', async (req, res) => {
692
745
  } as any);
693
746
 
694
747
  const [bal, decimals] = await Promise.all([balPromise, decPromise]) as [bigint, number];
695
- if (bal > 0n) {
748
+ const isCustom = userCustomTokens.some(t => t.chainName === chainName && t.address === address);
749
+
750
+ if (bal > 0n || isCustom) {
696
751
  portfolio[chainName].push({
697
752
  symbol,
698
753
  address,
@@ -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
+ });
@@ -1,8 +1,54 @@
1
1
  import fs from 'fs';
2
2
  import { getPath } from '../config/paths';
3
+ import { loadConfig, saveConfig } from '../config/parser';
3
4
 
4
5
  let disabledSkillsCache: string[] | null = null;
5
6
 
7
+ const reverseSkillMapping: Record<string, { category: 'os' | 'web3', name: string }> = {
8
+ // OS Skills
9
+ 'read_local_file': { category: 'os', name: 'readFile' },
10
+ 'write_local_file': { category: 'os', name: 'writeFile' },
11
+ 'edit_local_file': { category: 'os', name: 'editFile' },
12
+ 'generate_excel_file': { category: 'os', name: 'generateExcel' },
13
+ 'analyze_document': { category: 'os', name: 'analyzeDocument' },
14
+ 'run_terminal_command': { category: 'os', name: 'executeShell' },
15
+ 'browse_website': { category: 'os', name: 'browseWeb' },
16
+ 'search_web': { category: 'os', name: 'searchWeb' },
17
+ 'read_gmail_inbox': { category: 'os', name: 'readGmail' },
18
+ 'list_calendar_events': { category: 'os', name: 'listCalendar' },
19
+ 'append_row_to_sheets': { category: 'os', name: 'appendSheets' },
20
+ 'read_google_docs': { category: 'os', name: 'readDocs' },
21
+ 'read_google_form_responses': { category: 'os', name: 'readForms' },
22
+ 'execute_git_command': { category: 'os', name: 'gitManager' },
23
+ 'manage_twitter': { category: 'os', name: 'xManager' },
24
+ 'manage_notion': { category: 'os', name: 'notionWorkspace' },
25
+ 'transcribe_audio': { category: 'os', name: 'audioTranscribe' },
26
+ 'summarize_text': { category: 'os', name: 'summarizeText' },
27
+ 'update_security_policy': { category: 'os', name: 'updateSecurityPolicy' },
28
+
29
+ // Web3 Skills
30
+ 'transfer_token': { category: 'web3', name: 'transfer' },
31
+ 'transfer_native': { category: 'web3', name: 'transfer' },
32
+ 'swap_token': { category: 'web3', name: 'swap' },
33
+ 'bridge_token': { category: 'web3', name: 'bridge' },
34
+ 'mint_nft': { category: 'web3', name: 'mintNft' },
35
+ 'custom_tx': { category: 'web3', name: 'customTx' },
36
+ 'check_address': { category: 'web3', name: 'checkAddress' },
37
+ 'get_my_address': { category: 'web3', name: 'getMyAddress' },
38
+ 'check_token_security': { category: 'web3', name: 'checkSecurity' },
39
+ 'check_portfolio': { category: 'web3', name: 'checkPortfolio' },
40
+ 'analyze_market': { category: 'web3', name: 'marketAnalysis' },
41
+ 'manage_custom_tokens': { category: 'web3', name: 'manageCustomTokens' },
42
+ 'get_price': { category: 'web3', name: 'getPrice' },
43
+ 'supply_aave': { category: 'web3', name: 'aaveSupply' },
44
+ 'revoke_approval': { category: 'web3', name: 'revokeApproval' },
45
+ 'deposit_yield_vault': { category: 'web3', name: 'vaultDeposit' },
46
+ 'provide_liquidity_v3': { category: 'web3', name: 'provideLiquidity' },
47
+ 'get_tx_history': { category: 'web3', name: 'getTxHistory' },
48
+ 'check_registry_status': { category: 'web3', name: 'checkRegistryStatus' },
49
+ 'create_limit_order': { category: 'web3', name: 'createLimitOrder' }
50
+ };
51
+
6
52
  function getDisabledSkillsFile(): string {
7
53
  return getPath('disabled_skills.json');
8
54
  }
@@ -36,9 +82,61 @@ export function toggleSkill(skillName: string, active: boolean): void {
36
82
  }
37
83
  disabledSkillsCache = Array.from(current);
38
84
  fs.writeFileSync(getDisabledSkillsFile(), JSON.stringify(disabledSkillsCache, null, 2));
85
+
86
+ // Sync to config.yaml
87
+ const mapping = reverseSkillMapping[skillName];
88
+ if (mapping) {
89
+ const config = loadConfig();
90
+ if (!config.skills) config.skills = { web3: [], os: [] } as any;
91
+
92
+ const categoryArray = mapping.category === 'web3' ? config.skills!.web3 : config.skills!.os;
93
+
94
+ if (active && !categoryArray.includes(mapping.name)) {
95
+ categoryArray.push(mapping.name);
96
+ } else if (!active) {
97
+ const index = categoryArray.indexOf(mapping.name);
98
+ if (index !== -1) {
99
+ categoryArray.splice(index, 1);
100
+ }
101
+ }
102
+
103
+ saveConfig(config);
104
+ }
39
105
  }
40
106
 
41
107
  export function isSkillActive(skillName: string): boolean {
42
108
  const disabled = getDisabledSkills();
43
109
  return !disabled.includes(skillName);
44
110
  }
111
+
112
+ export function syncAllSkillsToConfig(): void {
113
+ const config = loadConfig();
114
+ if (!config.skills) config.skills = { web3: [], os: [] } as any;
115
+
116
+ const activeWeb3 = new Set<string>();
117
+ const activeOs = new Set<string>();
118
+
119
+ for (const [skillName, mapping] of Object.entries(reverseSkillMapping)) {
120
+ if (isSkillActive(skillName)) {
121
+ if (mapping.category === 'web3') {
122
+ activeWeb3.add(mapping.name);
123
+ } else {
124
+ activeOs.add(mapping.name);
125
+ }
126
+ }
127
+ }
128
+
129
+ // Check if arrays are different to avoid unnecessary saves
130
+ const currentWeb3 = config.skills!.web3 || [];
131
+ const currentOs = config.skills!.os || [];
132
+
133
+ const web3Changed = currentWeb3.length !== activeWeb3.size || !currentWeb3.every(s => activeWeb3.has(s));
134
+ const osChanged = currentOs.length !== activeOs.size || !currentOs.every(s => activeOs.has(s));
135
+
136
+ if (web3Changed || osChanged) {
137
+ config.skills!.web3 = Array.from(activeWeb3);
138
+ config.skills!.os = Array.from(activeOs);
139
+ saveConfig(config);
140
+ console.log('[SkillManager] Automatically synchronized active skills to config.yaml');
141
+ }
142
+ }
@@ -1,12 +1,17 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import yaml from 'yaml';
5
+ import { getPublicClient, ChainName } from '../web3/config';
6
+ import { getTokenMetadata } from '../web3/utils/tokens';
4
7
 
5
- const WHITELIST_FILE_PATH = path.join(os.homedir(), '.nyxora', 'user_whitelist.json');
8
+ const WHITELIST_FILE_PATH = path.join(os.homedir(), '.nyxora', 'user_whitelist.yaml');
6
9
 
7
10
  export interface WhitelistedToken {
8
11
  chainName: string;
9
12
  address: string;
13
+ symbol?: string;
14
+ decimals?: number;
10
15
  source: 'manual' | 'explorer' | 'swap';
11
16
  lastSeen: number;
12
17
  }
@@ -18,70 +23,74 @@ export interface UserWhitelist {
18
23
  export function getUserWhitelist(): UserWhitelist {
19
24
  try {
20
25
  if (!fs.existsSync(WHITELIST_FILE_PATH)) {
26
+ // Try migrating from JSON if YAML doesn't exist yet
27
+ const oldJsonPath = path.join(os.homedir(), '.nyxora', 'user_whitelist.json');
28
+ if (fs.existsSync(oldJsonPath)) {
29
+ const data = fs.readFileSync(oldJsonPath, 'utf-8');
30
+ const parsed = JSON.parse(data);
31
+ fs.writeFileSync(WHITELIST_FILE_PATH, yaml.stringify(parsed), 'utf-8');
32
+ // Rename old file so it doesn't get used again
33
+ fs.renameSync(oldJsonPath, `${oldJsonPath}.bak`);
34
+ return parsed;
35
+ }
21
36
  return {};
22
37
  }
23
38
  const data = fs.readFileSync(WHITELIST_FILE_PATH, 'utf-8');
24
- const parsed = JSON.parse(data);
25
-
26
- // Auto-migrate legacy format
27
- let migrated = false;
28
- for (const addr in parsed) {
29
- if (parsed[addr] && !Array.isArray(parsed[addr])) {
30
- const newArray: WhitelistedToken[] = [];
31
- for (const chain in parsed[addr]) {
32
- const tokens = parsed[addr][chain];
33
- if (Array.isArray(tokens)) {
34
- for (const t of tokens) {
35
- newArray.push({
36
- chainName: chain,
37
- address: typeof t === 'string' ? t.toLowerCase() : (t.address || '').toLowerCase(),
38
- source: 'manual',
39
- lastSeen: Date.now()
40
- });
41
- }
42
- }
43
- }
44
- parsed[addr] = newArray;
45
- migrated = true;
46
- }
47
- }
48
-
49
- if (migrated) {
50
- fs.writeFileSync(WHITELIST_FILE_PATH, JSON.stringify(parsed, null, 2), 'utf-8');
51
- }
52
-
53
- return parsed;
39
+ return yaml.parse(data) || {};
54
40
  } catch (err) {
55
- console.error('[Whitelist] Error reading user_whitelist.json', err);
41
+ console.error('[Whitelist] Error reading user_whitelist.yaml', err);
56
42
  return {};
57
43
  }
58
44
  }
59
45
 
60
- export function saveTokenToWhitelist(walletAddress: string, chainName: string, tokenAddress: string, source: 'manual' | 'explorer' | 'swap' = 'manual') {
46
+ export async function saveTokenToWhitelist(
47
+ walletAddress: string,
48
+ chainName: ChainName,
49
+ tokenAddress: string,
50
+ source: 'manual' | 'explorer' | 'swap' = 'manual',
51
+ symbol?: string,
52
+ decimals?: number
53
+ ) {
61
54
  try {
62
55
  const whitelist = getUserWhitelist();
63
56
  const addr = walletAddress.toLowerCase();
64
57
  const tokenAddr = tokenAddress.toLowerCase();
65
58
 
59
+ // Auto-fetch metadata if not provided
60
+ if (!symbol || decimals === undefined) {
61
+ try {
62
+ const client = getPublicClient(chainName);
63
+ const metadata = await getTokenMetadata(client, tokenAddr as `0x${string}`);
64
+ symbol = metadata.symbol;
65
+ decimals = metadata.decimals;
66
+ } catch (err) {
67
+ console.warn(`[Whitelist] Could not fetch metadata for ${tokenAddr} on ${chainName}`, err);
68
+ }
69
+ }
70
+
66
71
  if (!whitelist[addr]) whitelist[addr] = [];
67
72
 
68
73
  const existingIndex = whitelist[addr].findIndex(t => t.chainName === chainName && t.address === tokenAddr);
69
74
 
70
75
  if (existingIndex >= 0) {
71
76
  whitelist[addr][existingIndex].lastSeen = Date.now();
77
+ if (symbol) whitelist[addr][existingIndex].symbol = symbol;
78
+ if (decimals !== undefined) whitelist[addr][existingIndex].decimals = decimals;
72
79
  } else {
73
80
  whitelist[addr].push({
74
81
  chainName,
75
82
  address: tokenAddr,
83
+ symbol,
84
+ decimals,
76
85
  source,
77
86
  lastSeen: Date.now()
78
87
  });
79
- console.log(`[Whitelist] Added ${tokenAddr} to ${chainName} for ${addr} via ${source}`);
88
+ console.log(`[Whitelist] Added ${symbol || tokenAddr} to ${chainName} for ${addr} via ${source}`);
80
89
  }
81
90
 
82
- fs.writeFileSync(WHITELIST_FILE_PATH, JSON.stringify(whitelist, null, 2), 'utf-8');
91
+ fs.writeFileSync(WHITELIST_FILE_PATH, yaml.stringify(whitelist), 'utf-8');
83
92
  } catch (err) {
84
- console.error('[Whitelist] Error saving token to user_whitelist.json', err);
93
+ console.error('[Whitelist] Error saving token to user_whitelist.yaml', err);
85
94
  }
86
95
  }
87
96
 
@@ -97,11 +106,11 @@ export function removeTokenFromWhitelist(walletAddress: string, chainName: strin
97
106
  whitelist[addr] = whitelist[addr].filter(t => !(t.chainName === chainName && t.address === tokenAddr));
98
107
 
99
108
  if (whitelist[addr].length !== initialLength) {
100
- fs.writeFileSync(WHITELIST_FILE_PATH, JSON.stringify(whitelist, null, 2), 'utf-8');
101
- console.log(`[Whitelist] Removed garbage token ${tokenAddr} on ${chainName} for ${addr}`);
109
+ fs.writeFileSync(WHITELIST_FILE_PATH, yaml.stringify(whitelist), 'utf-8');
110
+ console.log(`[Whitelist] Removed token ${tokenAddr} on ${chainName} for ${addr}`);
102
111
  }
103
112
  } catch (err) {
104
- console.error('[Whitelist] Error removing token from user_whitelist.json', err);
113
+ console.error('[Whitelist] Error removing token from user_whitelist.yaml', err);
105
114
  }
106
115
  }
107
116