nyxora 1.6.6 → 1.6.7

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 (49) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -18
  4. package/package.json +2 -1
  5. package/packages/core/package.json +7 -1
  6. package/packages/core/src/agent/reasoning.ts +49 -4
  7. package/packages/core/src/gateway/cli.ts +6 -1
  8. package/packages/core/src/gateway/googleAuthModule.ts +181 -0
  9. package/packages/core/src/gateway/server.ts +110 -6
  10. package/packages/core/src/gateway/setup.ts +4 -4
  11. package/packages/core/src/system/skills/analyzeDocument.ts +64 -0
  12. package/packages/core/src/system/skills/googleWorkspace.ts +250 -0
  13. package/packages/core/src/system/skills/searchWeb.ts +46 -0
  14. package/packages/core/src/utils/dynamicTokenUpdater.ts +72 -0
  15. package/packages/core/src/utils/skillManager.ts +44 -0
  16. package/packages/core/src/utils/userWhitelistManager.ts +50 -0
  17. package/packages/core/src/web3/config.ts +29 -2
  18. package/packages/core/src/web3/skills/checkPortfolio.ts +51 -2
  19. package/packages/core/src/web3/skills/getBalance.ts +4 -0
  20. package/packages/core/src/web3/skills/swapToken.ts +9 -0
  21. package/packages/dashboard/dist/assets/index-24OeXn-k.css +1 -0
  22. package/packages/dashboard/dist/assets/index-BuYfTEKE.js +295 -0
  23. package/packages/dashboard/dist/favicon.svg +10 -1
  24. package/packages/dashboard/dist/index.html +2 -2
  25. package/packages/dashboard/package.json +1 -1
  26. package/packages/dashboard/public/favicon.svg +10 -1
  27. package/packages/dashboard/src/App.tsx +28 -24
  28. package/packages/dashboard/src/NetworkSelector.tsx +64 -0
  29. package/packages/dashboard/src/NyxoraLogo.tsx +25 -0
  30. package/packages/dashboard/src/OsSkills.tsx +352 -0
  31. package/packages/dashboard/src/Overview.tsx +3 -3
  32. package/packages/dashboard/src/PendingTransactions.tsx +2 -2
  33. package/packages/dashboard/src/Settings.tsx +114 -61
  34. package/packages/dashboard/src/Skills.tsx +135 -20
  35. package/packages/dashboard/src/components/PillSelect.tsx +65 -0
  36. package/packages/dashboard/src/index.css +205 -18
  37. package/packages/dashboard/src/utils/api.ts +8 -1
  38. package/packages/mcp-server/package.json +1 -1
  39. package/packages/policy/package.json +1 -1
  40. package/packages/signer/package.json +1 -1
  41. package/test-address.ts +11 -0
  42. package/test-all-chains.ts +19 -0
  43. package/test-portfolio.ts +14 -0
  44. package/IDENTITY.md +0 -17
  45. package/nyxora-1.5.2.tgz +0 -0
  46. package/packages/dashboard/dist/assets/index-CfIids2e.js +0 -170
  47. package/packages/dashboard/dist/assets/index-POJM-7Fd.css +0 -1
  48. package/security_policy.md +0 -2
  49. package/user.md +0 -9
@@ -26,11 +26,28 @@ import { checkPortfolioToolDefinition } from '../web3/skills/checkPortfolio';
26
26
  import { marketAnalysisToolDefinition } from '../web3/skills/marketAnalysis';
27
27
  import { createWalletToolDefinition } from '../web3/skills/createWallet';
28
28
  import { createLimitOrderToolDefinition, listLimitOrdersToolDefinition, cancelLimitOrderToolDefinition } from '../agent/limitOrderManager';
29
+ import { isSkillActive, toggleSkill } from '../utils/skillManager';
29
30
  import { executeBridge, bridgeTokenToolDefinition } from '../web3/skills/bridgeToken';
30
31
  import { executeMintNft, mintNftToolDefinition } from '../web3/skills/mintNft';
31
32
  import { executeCustomTx, customTxToolDefinition } from '../web3/skills/customTx';
33
+
34
+ // System Skills
35
+ import { browseWebsiteToolDefinition } from '../system/skills/browseWeb';
36
+ import { runTerminalCommandToolDefinition } from '../system/skills/executeShell';
37
+ import { installExternalSkillToolDefinition } from '../system/skills/installSkill';
38
+ import { readLocalFileToolDefinition } from '../system/skills/readFile';
39
+ import { updateSecurityPolicyToolDefinition } from '../system/skills/updateSecurityPolicy';
40
+ import { writeLocalFileToolDefinition } from '../system/skills/writeFile';
41
+ import { analyzeDocumentToolDefinition } from '../system/skills/analyzeDocument';
42
+ import { searchWebToolDefinition } from '../system/skills/searchWeb';
43
+ import { readGmailInboxToolDefinition, listCalendarEventsToolDefinition, appendRowToSheetsToolDefinition, readGoogleDocsToolDefinition, readGoogleFormResponsesToolDefinition } from '../system/skills/googleWorkspace';
44
+
32
45
  import { startTelegramBot } from './telegram';
33
46
  import { formatTransactionSuccess, formatTransactionError } from '../utils/formatter';
47
+ import { initGoogleAuth, getAuthUrl, processCallback, isAuthenticated } from './googleAuthModule';
48
+
49
+ // Initialize Google Auth
50
+ initGoogleAuth();
34
51
 
35
52
  import util from 'util';
36
53
 
@@ -52,7 +69,16 @@ console.error = function (...args) {
52
69
 
53
70
  const app = express();
54
71
  app.use(helmet());
55
- app.use(cors({ origin: ['http://localhost:3000', 'http://localhost:5173'] }));
72
+ app.use(cors({
73
+ origin: function (origin, callback) {
74
+ if (!origin || /^(http:\/\/(localhost|127\.0\.0\.1):\d+)$/.test(origin)) {
75
+ callback(null, true);
76
+ } else {
77
+ callback(new Error('Not allowed by CORS'));
78
+ }
79
+ },
80
+ allowedHeaders: ['Content-Type', 'Authorization', 'x-nyxora-token']
81
+ }));
56
82
  app.use(express.json());
57
83
 
58
84
  const apiLimiter = rateLimit({
@@ -64,9 +90,15 @@ app.use('/api/', apiLimiter);
64
90
 
65
91
  // API Auth Middleware
66
92
  app.use('/api', (req, res, next) => {
93
+ // Bypass auth for Google OAuth callback and URLs since they are handled externally or by the browser
94
+ if (req.path.startsWith('/auth/google')) {
95
+ return next();
96
+ }
97
+
67
98
  const token = req.headers['x-nyxora-token'];
68
99
  if (token !== getSessionToken()) {
69
- return res.status(401).json({ error: `Unauthorized: Invalid or missing token. Expected: ${getSessionToken()}, Received: ${token}` });
100
+ console.error(`[Auth] Rejected ${req.method} ${req.originalUrl} - Expected: ${getSessionToken().substring(0,8)}... Received: ${token ? token.toString().substring(0,8) + '...' : 'undefined'}`);
101
+ return res.status(401).json({ error: `Unauthorized: Invalid or missing token.` });
70
102
  }
71
103
  next();
72
104
  });
@@ -170,7 +202,7 @@ app.get('/api/logs', (req, res) => {
170
202
  });
171
203
 
172
204
  app.get('/api/skills', (req, res) => {
173
- res.json([
205
+ const allSkills = [
174
206
  getBalanceToolDefinition,
175
207
  transferToolDefinition,
176
208
  getPriceToolDefinition,
@@ -187,7 +219,79 @@ app.get('/api/skills', (req, res) => {
187
219
  createLimitOrderToolDefinition,
188
220
  listLimitOrdersToolDefinition,
189
221
  cancelLimitOrderToolDefinition
190
- ]);
222
+ ];
223
+
224
+ const skillsWithStatus = allSkills.map(skill => ({
225
+ ...skill,
226
+ isActive: isSkillActive(skill.function.name)
227
+ }));
228
+
229
+ res.json(skillsWithStatus);
230
+ });
231
+
232
+ app.get('/api/skills/system', (req, res) => {
233
+ const systemSkills = [
234
+ runTerminalCommandToolDefinition,
235
+ readLocalFileToolDefinition,
236
+ writeLocalFileToolDefinition,
237
+ browseWebsiteToolDefinition,
238
+ updateSecurityPolicyToolDefinition,
239
+ installExternalSkillToolDefinition,
240
+ analyzeDocumentToolDefinition,
241
+ searchWebToolDefinition,
242
+ readGmailInboxToolDefinition,
243
+ listCalendarEventsToolDefinition,
244
+ appendRowToSheetsToolDefinition,
245
+ readGoogleDocsToolDefinition,
246
+ readGoogleFormResponsesToolDefinition
247
+ ];
248
+
249
+ const skillsWithStatus = systemSkills.map(skill => ({
250
+ ...skill,
251
+ isActive: isSkillActive(skill.function.name)
252
+ }));
253
+
254
+ res.json(skillsWithStatus);
255
+ });
256
+
257
+ app.post('/api/skills/toggle', (req, res) => {
258
+ const { skillName, active } = req.body;
259
+ if (!skillName || typeof active !== 'boolean') {
260
+ return res.status(400).json({ error: 'Invalid payload' });
261
+ }
262
+ toggleSkill(skillName, active);
263
+ res.json({ success: true, skillName, active });
264
+ });
265
+
266
+ // Google Workspace Auth Routes
267
+ app.get('/api/auth/google/url', (req, res) => {
268
+ const url = getAuthUrl();
269
+ if (!url) return res.status(500).json({ error: 'Google Auth not configured' });
270
+ res.json({ url });
271
+ });
272
+
273
+ app.get('/api/auth/google/callback', async (req, res) => {
274
+ const code = req.query.code as string;
275
+ if (!code) return res.status(400).send('No code provided');
276
+
277
+ const success = await processCallback(code);
278
+ if (success) {
279
+ res.send(`
280
+ <html><body>
281
+ <h2>Authentication Successful!</h2>
282
+ <p>You can close this window and return to the dashboard.</p>
283
+ <script>
284
+ setTimeout(() => window.close(), 2000);
285
+ </script>
286
+ </body></html>
287
+ `);
288
+ } else {
289
+ res.status(500).send('Authentication failed');
290
+ }
291
+ });
292
+
293
+ app.get('/api/auth/google/status', async (req, res) => {
294
+ res.json({ connected: await isAuthenticated() });
191
295
  });
192
296
 
193
297
  app.get('/api/transactions', (req, res) => {
@@ -300,8 +404,8 @@ export function startServer() {
300
404
  });
301
405
  limitOrderManager.startMonitor();
302
406
 
303
- const PORT = process.env.PORT || 3000;
304
- app.listen(PORT, () => {
407
+ const PORT = Number(process.env.PORT || 3000);
408
+ app.listen(PORT, '0.0.0.0', () => {
305
409
  console.log(`🤖 Nyxora API Server running on port ${PORT}`);
306
410
 
307
411
  // Start the Telegram bot listener
@@ -166,12 +166,12 @@ Provider: ${config.llm.provider}`;
166
166
  message: 'Select Default Chain:',
167
167
  initialValue: config.agent.default_chain,
168
168
  options: [
169
- { value: 'sepolia', label: 'Sepolia (Testnet)' },
170
- { value: 'base', label: 'Base' },
171
- { value: 'bsc', label: 'BSC' },
172
169
  { value: 'ethereum', label: 'Ethereum Mainnet' },
173
- { value: 'arbitrum', label: 'Arbitrum' },
170
+ { value: 'bsc', label: 'BSC' },
171
+ { value: 'base', label: 'Base' },
174
172
  { value: 'optimism', label: 'Optimism' },
173
+ { value: 'arbitrum', label: 'Arbitrum' },
174
+ { value: 'sepolia', label: 'Sepolia (Testnet)' },
175
175
  ],
176
176
  });
177
177
  if (isCancel(defaultChain)) return process.exit(0);
@@ -0,0 +1,64 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import pdfParse = require('pdf-parse');
4
+ // @ts-ignore
5
+ import mammoth from 'mammoth';
6
+
7
+ export async function analyzeDocument(filePath: string): Promise<string> {
8
+ try {
9
+ const absolutePath = path.resolve(filePath);
10
+ if (!fs.existsSync(absolutePath)) {
11
+ return `Error: File not found at ${absolutePath}`;
12
+ }
13
+
14
+ const ext = path.extname(absolutePath).toLowerCase();
15
+
16
+ if (ext === '.pdf') {
17
+ const dataBuffer = fs.readFileSync(absolutePath);
18
+ // @ts-ignore
19
+ const data = await pdfParse(dataBuffer);
20
+ let text = data.text.trim();
21
+ if (text.length > 20000) {
22
+ text = text.substring(0, 20000) + "... [Content Truncated]";
23
+ }
24
+ return text;
25
+ }
26
+
27
+ if (ext === '.docx') {
28
+ const result = await mammoth.extractRawText({ path: absolutePath });
29
+ let text = result.value.trim();
30
+ if (text.length > 20000) {
31
+ text = text.substring(0, 20000) + "... [Content Truncated]";
32
+ }
33
+ return text;
34
+ }
35
+
36
+ // Fallback for TXT, MD, CSV, etc.
37
+ let content = fs.readFileSync(absolutePath, 'utf8');
38
+ if (content.length > 20000) {
39
+ content = content.substring(0, 20000) + "... [Content Truncated]";
40
+ }
41
+ return content;
42
+
43
+ } catch (error: any) {
44
+ return `Failed to analyze document: ${error.message}`;
45
+ }
46
+ }
47
+
48
+ export const analyzeDocumentToolDefinition = {
49
+ type: "function",
50
+ function: {
51
+ name: "analyze_document",
52
+ description: "Extracts textual content from documents (PDF, DOCX) and plain text files (TXT, MD, CSV). Useful for reading reports, contracts, and data dumps.",
53
+ parameters: {
54
+ type: "object",
55
+ properties: {
56
+ filePath: {
57
+ type: "string",
58
+ description: "The absolute or relative path to the document file.",
59
+ }
60
+ },
61
+ required: ["filePath"],
62
+ },
63
+ },
64
+ };
@@ -0,0 +1,250 @@
1
+ import { getAccessToken, isAuthenticated } from '../../gateway/googleAuthModule';
2
+
3
+ export async function readGmailInbox(maxResults: number = 5): Promise<string> {
4
+ const isAuth = await isAuthenticated();
5
+ if (!isAuth) {
6
+ return "Google Auth not configured. Please link your Google account in the dashboard.";
7
+ }
8
+
9
+ const token = await getAccessToken();
10
+ if (!token) return "Failed to retrieve access token.";
11
+
12
+ try {
13
+ const listRes = await fetch(`https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=${maxResults}&q=in:inbox`, {
14
+ headers: { Authorization: `Bearer ${token}` }
15
+ });
16
+ const listData = await listRes.json();
17
+
18
+ if (!listData.messages || listData.messages.length === 0) {
19
+ return 'No emails found in inbox.';
20
+ }
21
+
22
+ let output = `Top ${listData.messages.length} recent emails:\n\n`;
23
+
24
+ for (const msg of listData.messages) {
25
+ const msgRes = await fetch(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${msg.id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`, {
26
+ headers: { Authorization: `Bearer ${token}` }
27
+ });
28
+ const msgData = await msgRes.json();
29
+ const headers = msgData.payload?.headers;
30
+
31
+ if (headers) {
32
+ const subject = headers.find((h: any) => h.name === 'Subject')?.value || 'No Subject';
33
+ const from = headers.find((h: any) => h.name === 'From')?.value || 'Unknown Sender';
34
+ const date = headers.find((h: any) => h.name === 'Date')?.value || 'Unknown Date';
35
+ const snippet = msgData.snippet ? `\nIsi/Snippet: ${msgData.snippet}` : '';
36
+ output += `From: ${from}\nSubject: ${subject}\nDate: ${date}${snippet}\n---\n`;
37
+ }
38
+ }
39
+
40
+ return output.trim();
41
+ } catch (err: any) {
42
+ return `Error reading Gmail: ${err.message}`;
43
+ }
44
+ }
45
+
46
+ export async function listCalendarEvents(maxResults: number = 5): Promise<string> {
47
+ const isAuth = await isAuthenticated();
48
+ if (!isAuth) return "Google Auth not configured. Please link your Google account in the dashboard.";
49
+ const token = await getAccessToken();
50
+ if (!token) return "Failed to retrieve access token.";
51
+
52
+ try {
53
+ const timeMin = encodeURIComponent(new Date().toISOString());
54
+ const res = await fetch(`https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${timeMin}&maxResults=${maxResults}&singleEvents=true&orderBy=startTime`, {
55
+ headers: { Authorization: `Bearer ${token}` }
56
+ });
57
+ const data = await res.json();
58
+
59
+ const events = data.items;
60
+ if (!events || events.length === 0) {
61
+ return 'No upcoming events found.';
62
+ }
63
+
64
+ let output = `Upcoming ${events.length} events:\n\n`;
65
+ events.map((event: any, i: number) => {
66
+ const start = event.start?.dateTime || event.start?.date;
67
+ output += `${i + 1}. ${event.summary} (${start})\n`;
68
+ });
69
+
70
+ return output.trim();
71
+ } catch (err: any) {
72
+ return `Error reading Calendar: ${err.message}`;
73
+ }
74
+ }
75
+
76
+ export async function appendRowToSheets(spreadsheetId: string, range: string, values: any[]): Promise<string> {
77
+ const isAuth = await isAuthenticated();
78
+ if (!isAuth) return "Google Auth not configured. Please link your Google account in the dashboard.";
79
+ const token = await getAccessToken();
80
+ if (!token) return "Failed to retrieve access token.";
81
+
82
+ try {
83
+ const res = await fetch(`https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}:append?valueInputOption=USER_ENTERED`, {
84
+ method: 'POST',
85
+ headers: {
86
+ Authorization: `Bearer ${token}`,
87
+ 'Content-Type': 'application/json'
88
+ },
89
+ body: JSON.stringify({ values: [values] })
90
+ });
91
+
92
+ const data = await res.json();
93
+ if (!res.ok) {
94
+ return `Failed to append to Sheets: ${data.error?.message || JSON.stringify(data)}`;
95
+ }
96
+
97
+ return `Successfully appended row to ${spreadsheetId} at range ${data.updates?.updatedRange}.`;
98
+ } catch (err: any) {
99
+ return `Error appending to Sheets: ${err.message}`;
100
+ }
101
+ }
102
+
103
+ export async function readGoogleDocs(documentId: string): Promise<string> {
104
+ const isAuth = await isAuthenticated();
105
+ if (!isAuth) return "Google Auth not configured. Please link your Google account in the dashboard.";
106
+ const token = await getAccessToken();
107
+ if (!token) return "Failed to retrieve access token.";
108
+
109
+ try {
110
+ const res = await fetch(`https://docs.googleapis.com/v1/documents/${documentId}`, {
111
+ headers: { Authorization: `Bearer ${token}` }
112
+ });
113
+ const data = await res.json();
114
+ if (!res.ok) return `Failed to read document: ${data.error?.message || JSON.stringify(data)}`;
115
+
116
+ let text = '';
117
+ data.body?.content?.forEach((c: any) => {
118
+ if (c.paragraph) {
119
+ c.paragraph.elements?.forEach((e: any) => {
120
+ if (e.textRun) text += e.textRun.content;
121
+ });
122
+ }
123
+ });
124
+
125
+ return text ? text.substring(0, 2000) + (text.length > 2000 ? '... (truncated)' : '') : 'Document is empty.';
126
+ } catch (err: any) {
127
+ return `Error reading Google Docs: ${err.message}`;
128
+ }
129
+ }
130
+
131
+ export async function readGoogleFormResponses(formId: string): Promise<string> {
132
+ const isAuth = await isAuthenticated();
133
+ if (!isAuth) return "Google Auth not configured. Please link your Google account in the dashboard.";
134
+ const token = await getAccessToken();
135
+ if (!token) return "Failed to retrieve access token.";
136
+
137
+ try {
138
+ const res = await fetch(`https://forms.googleapis.com/v1/forms/${formId}/responses`, {
139
+ headers: { Authorization: `Bearer ${token}` }
140
+ });
141
+ const data = await res.json();
142
+ if (!res.ok) return `Failed to read form responses: ${data.error?.message || JSON.stringify(data)}`;
143
+
144
+ if (!data.responses || data.responses.length === 0) {
145
+ return 'No responses found for this form.';
146
+ }
147
+
148
+ let output = `Total ${data.responses.length} responses:\n\n`;
149
+
150
+ // Only return the last 10 responses to avoid overflowing context window
151
+ const recentResponses = data.responses.slice(-10);
152
+
153
+ recentResponses.forEach((response: any, i: number) => {
154
+ output += `Response ${i + 1} (Submitted at ${response.createTime}):\n`;
155
+ if (response.answers) {
156
+ for (const [questionId, answer] of Object.entries(response.answers)) {
157
+ const ans = answer as any;
158
+ const textAnswers = ans.textAnswers?.answers?.map((a: any) => a.value).join(', ') || 'No text answer';
159
+ output += `- Question ID ${questionId}: ${textAnswers}\n`;
160
+ }
161
+ }
162
+ output += '---\n';
163
+ });
164
+
165
+ return output.trim();
166
+ } catch (err: any) {
167
+ return `Error reading Google Forms: ${err.message}`;
168
+ }
169
+ }
170
+
171
+ export const readGmailInboxToolDefinition = {
172
+ type: "function",
173
+ function: {
174
+ name: "read_gmail_inbox",
175
+ description: "Reads the most recent emails from the user's Gmail inbox.",
176
+ parameters: {
177
+ type: "object",
178
+ properties: {
179
+ maxResults: { type: "number", description: "Number of emails to fetch (default: 5)." }
180
+ },
181
+ required: [],
182
+ },
183
+ },
184
+ };
185
+
186
+ export const listCalendarEventsToolDefinition = {
187
+ type: "function",
188
+ function: {
189
+ name: "list_calendar_events",
190
+ description: "Lists upcoming events from the user's Google Calendar.",
191
+ parameters: {
192
+ type: "object",
193
+ properties: {
194
+ maxResults: { type: "number", description: "Number of events to fetch (default: 5)." }
195
+ },
196
+ required: [],
197
+ },
198
+ },
199
+ };
200
+
201
+ export const appendRowToSheetsToolDefinition = {
202
+ type: "function",
203
+ function: {
204
+ name: "append_row_to_sheets",
205
+ description: "Appends a row of data to a Google Spreadsheet.",
206
+ parameters: {
207
+ type: "object",
208
+ properties: {
209
+ spreadsheetId: { type: "string", description: "The ID of the spreadsheet (found in the URL)." },
210
+ range: { type: "string", description: "The A1 notation of a range to search for a logical table of data, e.g. 'Sheet1!A:D'." },
211
+ values: {
212
+ type: "array",
213
+ items: { type: "string" },
214
+ description: "An array of strings representing the columns of the new row."
215
+ }
216
+ },
217
+ required: ["spreadsheetId", "range", "values"],
218
+ },
219
+ },
220
+ };
221
+
222
+ export const readGoogleDocsToolDefinition = {
223
+ type: "function",
224
+ function: {
225
+ name: "read_google_docs",
226
+ description: "Reads the text content of a Google Document.",
227
+ parameters: {
228
+ type: "object",
229
+ properties: {
230
+ documentId: { type: "string", description: "The ID of the document (found in the URL)." }
231
+ },
232
+ required: ["documentId"],
233
+ },
234
+ },
235
+ };
236
+
237
+ export const readGoogleFormResponsesToolDefinition = {
238
+ type: "function",
239
+ function: {
240
+ name: "read_google_form_responses",
241
+ description: "Reads the most recent responses from a Google Form.",
242
+ parameters: {
243
+ type: "object",
244
+ properties: {
245
+ formId: { type: "string", description: "The ID of the form (found in the URL)." }
246
+ },
247
+ required: ["formId"],
248
+ },
249
+ },
250
+ };
@@ -0,0 +1,46 @@
1
+ import { search, SafeSearchType } from 'duck-duck-scrape';
2
+
3
+ export async function searchWeb(query: string): Promise<string> {
4
+ try {
5
+ const searchResults = await search(query, {
6
+ safeSearch: SafeSearchType.MODERATE
7
+ });
8
+
9
+ if (!searchResults.results || searchResults.results.length === 0) {
10
+ return "No results found for your query.";
11
+ }
12
+
13
+ // Limit to top 8 results
14
+ const topResults = searchResults.results.slice(0, 8);
15
+
16
+ let responseText = `Search Results for "${query}":\n\n`;
17
+
18
+ topResults.forEach((result, index) => {
19
+ responseText += `${index + 1}. ${result.title}\n`;
20
+ responseText += `URL: ${result.url}\n`;
21
+ responseText += `Snippet: ${result.description}\n\n`;
22
+ });
23
+
24
+ return responseText.trim();
25
+ } catch (error: any) {
26
+ return `Failed to search the web: ${error.message}`;
27
+ }
28
+ }
29
+
30
+ export const searchWebToolDefinition = {
31
+ type: "function",
32
+ function: {
33
+ name: "search_web",
34
+ description: "Searches the internet for information using a search engine. Returns top titles, snippets, and URLs. Use this to find current events, documentation, or general facts.",
35
+ parameters: {
36
+ type: "object",
37
+ properties: {
38
+ query: {
39
+ type: "string",
40
+ description: "The search query to look up.",
41
+ }
42
+ },
43
+ required: ["query"],
44
+ },
45
+ },
46
+ };
@@ -0,0 +1,72 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const CACHE_FILE = path.join(os.homedir(), '.nyxora', 'dynamic_tokens.json');
6
+ const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
7
+
8
+ const TOKEN_LIST_URLS: Record<string, string> = {
9
+ ethereum: 'https://tokens.coingecko.com/uniswap/all.json',
10
+ base: 'https://raw.githubusercontent.com/ethereum-optimism/ethereum-optimism.github.io/master/optimism.tokenlist.json',
11
+ arbitrum: 'https://bridge.arbitrum.io/token-list-42161.json',
12
+ optimism: 'https://static.optimism.io/optimism.tokenlist.json',
13
+ bsc: 'https://tokens.pancakeswap.finance/pancakeswap-extended.json'
14
+ };
15
+
16
+ export interface DynamicTokens {
17
+ [chainName: string]: Array<{ symbol: string, address: string }>;
18
+ }
19
+
20
+ export async function fetchDynamicTokens(): Promise<DynamicTokens> {
21
+ // Check cache first
22
+ try {
23
+ if (fs.existsSync(CACHE_FILE)) {
24
+ const stats = fs.statSync(CACHE_FILE);
25
+ if (Date.now() - stats.mtimeMs < CACHE_TTL) {
26
+ return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
27
+ }
28
+ }
29
+ } catch (e) {
30
+ // Ignore cache error
31
+ }
32
+
33
+ const result: DynamicTokens = {};
34
+ console.log('[DynamicTokenUpdater] Fetching updated token lists...');
35
+
36
+ for (const [chain, url] of Object.entries(TOKEN_LIST_URLS)) {
37
+ try {
38
+ const res = await fetch(url);
39
+ const data = await res.json();
40
+
41
+ let tokens: Array<{symbol: string, address: string}> = [];
42
+
43
+ if (data && data.tokens && Array.isArray(data.tokens)) {
44
+ // Limit to top 50 to avoid massive multicall
45
+ const topTokens = data.tokens.slice(0, 50);
46
+ tokens = topTokens.map((t: any) => ({
47
+ symbol: t.symbol,
48
+ address: t.address
49
+ }));
50
+ }
51
+
52
+ result[chain] = tokens;
53
+ } catch (err) {
54
+ console.warn(`[DynamicTokenUpdater] Failed to fetch list for ${chain}:`, err);
55
+ result[chain] = [];
56
+ }
57
+ }
58
+
59
+ // Save to cache
60
+ try {
61
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2), 'utf-8');
62
+ } catch (e) {
63
+ console.error('[DynamicTokenUpdater] Failed to write cache', e);
64
+ }
65
+
66
+ return result;
67
+ }
68
+
69
+ export async function getDynamicTokensForChain(chainName: string): Promise<Array<{symbol: string, address: string}>> {
70
+ const allTokens = await fetchDynamicTokens();
71
+ return allTokens[chainName] || [];
72
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'fs';
2
+ import { getPath } from '../config/paths';
3
+
4
+ let disabledSkillsCache: string[] | null = null;
5
+
6
+ function getDisabledSkillsFile(): string {
7
+ return getPath('disabled_skills.json');
8
+ }
9
+
10
+ export function getDisabledSkills(): string[] {
11
+ if (disabledSkillsCache !== null) {
12
+ return disabledSkillsCache;
13
+ }
14
+ const filepath = getDisabledSkillsFile();
15
+ if (!fs.existsSync(filepath)) {
16
+ disabledSkillsCache = [];
17
+ return disabledSkillsCache;
18
+ }
19
+ try {
20
+ const data = fs.readFileSync(filepath, 'utf-8');
21
+ disabledSkillsCache = JSON.parse(data);
22
+ if (!Array.isArray(disabledSkillsCache)) disabledSkillsCache = [];
23
+ return disabledSkillsCache;
24
+ } catch (e) {
25
+ disabledSkillsCache = [];
26
+ return [];
27
+ }
28
+ }
29
+
30
+ export function toggleSkill(skillName: string, active: boolean): void {
31
+ const current = new Set(getDisabledSkills());
32
+ if (active) {
33
+ current.delete(skillName); // Remove from disabled list to activate
34
+ } else {
35
+ current.add(skillName); // Add to disabled list to deactivate
36
+ }
37
+ disabledSkillsCache = Array.from(current);
38
+ fs.writeFileSync(getDisabledSkillsFile(), JSON.stringify(disabledSkillsCache, null, 2));
39
+ }
40
+
41
+ export function isSkillActive(skillName: string): boolean {
42
+ const disabled = getDisabledSkills();
43
+ return !disabled.includes(skillName);
44
+ }