nyxora 26.6.21 → 26.6.23

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 (64) hide show
  1. package/README.md +3 -4
  2. package/bin/nyxora.mjs +14 -2
  3. package/dist/packages/core/src/agent/cronManager.js +107 -0
  4. package/dist/packages/core/src/agent/reasoning.js +85 -22
  5. package/dist/packages/core/src/agent/transactionManager.js +2 -2
  6. package/dist/packages/core/src/agent/updateIdentity.js +71 -0
  7. package/dist/packages/core/src/config/paths.js +5 -20
  8. package/dist/packages/core/src/gateway/WebSocketManager.js +98 -0
  9. package/dist/packages/core/src/gateway/chat.js +38 -0
  10. package/dist/packages/core/src/gateway/cli.js +20 -20
  11. package/dist/packages/core/src/gateway/server.js +63 -8
  12. package/dist/packages/core/src/gateway/setup.js +9 -6
  13. package/dist/packages/core/src/gateway/telegram.js +43 -0
  14. package/dist/packages/core/src/gateway/tracker.js +63 -0
  15. package/dist/packages/core/src/memory/logger.js +1 -1
  16. package/dist/packages/core/src/system/skills/cancelTask.js +40 -0
  17. package/dist/packages/core/src/system/skills/editFile.js +5 -0
  18. package/dist/packages/core/src/system/skills/scheduleTask.js +39 -0
  19. package/dist/packages/core/src/system/skills/searchWeb.js +61 -8
  20. package/dist/packages/core/src/system/skills/writeFile.js +5 -0
  21. package/dist/packages/core/src/web3/skills/getPrice.js +1 -1
  22. package/dist/packages/core/src/web3/utils/vaultClient.js +79 -29
  23. package/dist/packages/policy/src/server.js +29 -1
  24. package/package.json +7 -1
  25. package/packages/core/package.json +8 -1
  26. package/packages/core/src/agent/cronManager.ts +87 -0
  27. package/packages/core/src/agent/reasoning.ts +91 -21
  28. package/packages/core/src/agent/transactionManager.ts +2 -1
  29. package/packages/core/src/agent/updateIdentity.ts +68 -0
  30. package/packages/core/src/config/parser.ts +1 -1
  31. package/packages/core/src/config/paths.ts +5 -23
  32. package/packages/core/src/gateway/WebSocketManager.ts +114 -0
  33. package/packages/core/src/gateway/chat.ts +40 -1
  34. package/packages/core/src/gateway/cli.ts +10 -10
  35. package/packages/core/src/gateway/server.ts +66 -7
  36. package/packages/core/src/gateway/setup.ts +8 -5
  37. package/packages/core/src/gateway/telegram.ts +49 -0
  38. package/packages/core/src/gateway/tracker.ts +61 -0
  39. package/packages/core/src/memory/logger.ts +1 -1
  40. package/packages/core/src/system/skills/cancelTask.ts +38 -0
  41. package/packages/core/src/system/skills/editFile.ts +7 -0
  42. package/packages/core/src/system/skills/scheduleTask.ts +38 -0
  43. package/packages/core/src/system/skills/searchWeb.ts +56 -8
  44. package/packages/core/src/system/skills/writeFile.ts +7 -0
  45. package/packages/core/src/web3/skills/getPrice.ts +1 -1
  46. package/packages/core/src/web3/utils/vaultClient.ts +86 -26
  47. package/packages/dashboard/dist/assets/index-CjZWf1Ei.css +1 -0
  48. package/packages/dashboard/dist/assets/index-CmWZofn_.js +16 -0
  49. package/packages/dashboard/dist/index.html +2 -2
  50. package/packages/dashboard/dist/routers/0x.png +0 -0
  51. package/packages/dashboard/dist/routers/1inch.png +0 -0
  52. package/packages/dashboard/dist/routers/cmc.png +0 -0
  53. package/packages/dashboard/dist/routers/kyberswap.png +0 -0
  54. package/packages/dashboard/dist/routers/lifi.png +0 -0
  55. package/packages/dashboard/dist/routers/openocean.png +0 -0
  56. package/packages/dashboard/dist/routers/relay.png +0 -0
  57. package/packages/dashboard/dist/routers/zerion.png +0 -0
  58. package/packages/dashboard/package.json +1 -1
  59. package/packages/mcp-server/package.json +1 -1
  60. package/packages/policy/package.json +4 -3
  61. package/packages/policy/src/server.ts +29 -1
  62. package/packages/signer/package.json +1 -1
  63. package/packages/dashboard/dist/assets/index-CQNHWZtN.css +0 -1
  64. package/packages/dashboard/dist/assets/index-Di9x08yk.js +0 -16
@@ -2,30 +2,12 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
 
5
- let isGlobalModeCache: boolean | null = null;
6
-
7
5
  export function getAppDir(): string {
8
- // Check if .env or config.yaml exists in current working directory
9
- if (isGlobalModeCache === null) {
10
- const localEnv = path.join(process.cwd(), '.env');
11
- const localConfig = path.join(process.cwd(), 'config.yaml');
12
-
13
- if (fs.existsSync(localEnv) || fs.existsSync(localConfig)) {
14
- isGlobalModeCache = false; // Local manual mode
15
- } else {
16
- isGlobalModeCache = true; // Global CLI mode
17
- }
18
- }
19
-
20
- if (isGlobalModeCache) {
21
- const globalDir = path.join(os.homedir(), '.nyxora');
22
- if (!fs.existsSync(globalDir)) {
23
- fs.mkdirSync(globalDir, { recursive: true });
24
- }
25
- return globalDir;
6
+ const globalDir = path.join(os.homedir(), '.nyxora');
7
+ if (!fs.existsSync(globalDir)) {
8
+ fs.mkdirSync(globalDir, { recursive: true });
26
9
  }
27
-
28
- return process.cwd();
10
+ return globalDir;
29
11
  }
30
12
 
31
13
  function ensureDir(dir: string) {
@@ -47,7 +29,7 @@ export function getPath(filename: string): string {
47
29
  subDir = 'config';
48
30
  } else if (lowerFile.endsWith('.token') || lowerFile.includes('vault') || lowerFile.includes('credentials')) {
49
31
  subDir = 'auth';
50
- } else if (lowerFile.endsWith('.log') || lowerFile.includes('pid')) {
32
+ } else if (lowerFile.endsWith('.log') || lowerFile.includes('pid') || lowerFile.includes('tracker')) {
51
33
  subDir = 'run';
52
34
  }
53
35
 
@@ -0,0 +1,114 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import http from 'http';
3
+ import { validateToken } from '../utils/state';
4
+ import { IncomingMessage } from 'http';
5
+
6
+ interface BufferedLog {
7
+ timestamp: number;
8
+ message: string;
9
+ level: string;
10
+ }
11
+
12
+ export class WebSocketManager {
13
+ private wss: WebSocketServer;
14
+ private clients: Map<string, WebSocket> = new Map();
15
+ private ringBuffer: Map<string, { logs: BufferedLog[]; timeout: NodeJS.Timeout }> = new Map();
16
+
17
+ constructor(server: http.Server) {
18
+ this.wss = new WebSocketServer({ noServer: true });
19
+
20
+ server.on('upgrade', async (request: IncomingMessage, socket, head) => {
21
+ try {
22
+ const url = new URL(request.url || '', `http://${request.headers.host}`);
23
+ if (url.pathname !== '/ws/stream') {
24
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
25
+ socket.destroy();
26
+ return;
27
+ }
28
+
29
+ const traceId = url.searchParams.get('traceId');
30
+ const token = url.searchParams.get('token');
31
+
32
+ if (!traceId || !token) {
33
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
34
+ socket.destroy();
35
+ return;
36
+ }
37
+
38
+ // Validate Auth Token during Upgrade Handshake
39
+ if (!validateToken(token)) {
40
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
41
+ socket.destroy();
42
+ return;
43
+ }
44
+
45
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
46
+ this.wss.emit('connection', ws, request, traceId);
47
+ });
48
+ } catch (e) {
49
+ socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
50
+ socket.destroy();
51
+ }
52
+ });
53
+
54
+ this.wss.on('connection', (ws: WebSocket, request: IncomingMessage, traceId: string) => {
55
+ this.clients.set(traceId, ws);
56
+
57
+ ws.on('close', () => {
58
+ this.clients.delete(traceId);
59
+ });
60
+
61
+ // Flush Ring Buffer if it exists
62
+ const buffer = this.ringBuffer.get(traceId);
63
+ if (buffer) {
64
+ clearTimeout(buffer.timeout);
65
+ for (const log of buffer.logs) {
66
+ ws.send(JSON.stringify(log));
67
+ }
68
+ this.ringBuffer.delete(traceId);
69
+ }
70
+ });
71
+ }
72
+
73
+ public broadcast(traceId: string, message: string, level: string = 'info') {
74
+ const payload = { timestamp: Date.now(), message, level };
75
+ const ws = this.clients.get(traceId);
76
+
77
+ if (ws && ws.readyState === WebSocket.OPEN) {
78
+ // Client is connected, send directly
79
+ ws.send(JSON.stringify(payload));
80
+ } else {
81
+ // Client not connected yet, buffer the log (Anti-Race Condition)
82
+ let buffer = this.ringBuffer.get(traceId);
83
+ if (!buffer) {
84
+ buffer = {
85
+ logs: [],
86
+ timeout: setTimeout(() => {
87
+ // Drop buffer after 5 seconds if client never connects
88
+ this.ringBuffer.delete(traceId);
89
+ }, 5000)
90
+ };
91
+ this.ringBuffer.set(traceId, buffer);
92
+ }
93
+ buffer.logs.push(payload);
94
+ }
95
+ }
96
+
97
+ public broadcastAll(message: string, level: string = 'info') {
98
+ const payload = { timestamp: Date.now(), message, level };
99
+ const payloadStr = JSON.stringify(payload);
100
+ for (const [_, ws] of this.clients) {
101
+ if (ws.readyState === WebSocket.OPEN) {
102
+ ws.send(payloadStr);
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ // Global instance to be initialized in server.ts
109
+ export let wsManager: WebSocketManager | null = null;
110
+
111
+ export function initWebSocket(server: http.Server) {
112
+ wsManager = new WebSocketManager(server);
113
+ return wsManager;
114
+ }
@@ -1,4 +1,4 @@
1
- import { intro, text, spinner, isCancel, cancel } from '@clack/prompts';
1
+ import { intro, text, spinner, isCancel, cancel, confirm } from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import fs from 'fs';
4
4
  import { getPath } from '../config/paths';
@@ -77,6 +77,45 @@ export async function chatInteractive() {
77
77
  finalReply = finalReply.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
78
78
 
79
79
  console.log(finalReply + '\n');
80
+
81
+ // Check for pending transactions
82
+ try {
83
+ const txRes = await fetch('http://localhost:3000/api/transactions', {
84
+ headers: { 'x-nyxora-token': token }
85
+ });
86
+ if (txRes.ok) {
87
+ const txs = await txRes.json();
88
+ for (const tx of txs) {
89
+ const isApproved = await confirm({
90
+ message: pc.yellow(`Approve Transaction [${tx.type.toUpperCase()}] on ${tx.chainName.toUpperCase()}?`),
91
+ });
92
+
93
+ if (isCancel(isApproved) || !isApproved) {
94
+ await fetch(`http://localhost:3000/api/transactions/${tx.id}/reject`, {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json', 'x-nyxora-token': token },
97
+ body: JSON.stringify({ nonce: tx.nonce, sessionId: 'cli-chat' })
98
+ });
99
+ console.log(pc.red(`Transaction rejected.\n`));
100
+ continue;
101
+ }
102
+
103
+ const appRes = await fetch(`http://localhost:3000/api/transactions/${tx.id}/approve`, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json', 'x-nyxora-token': token },
106
+ body: JSON.stringify({ nonce: tx.nonce, sessionId: 'cli-chat' })
107
+ });
108
+ const appData = await appRes.json();
109
+ if (appData.success) {
110
+ console.log(pc.green(`Transaction approved! Processing in background...\n`));
111
+ } else {
112
+ console.log(pc.red(`Failed to approve: ${appData.error}\n`));
113
+ }
114
+ }
115
+ }
116
+ } catch (e) {
117
+ // silently ignore fetch errors for tx polling
118
+ }
80
119
  } catch (error) {
81
120
  s.stop(pc.red('Connection failed.'));
82
121
  console.log(pc.red(`Is the daemon running? (http://localhost:3000)`));
@@ -18,7 +18,6 @@ import { saveApiKeys } from '../config/parser';
18
18
  async function main() {
19
19
  // 1. Determine configuration directory
20
20
  const appDir = getAppDir();
21
- const isGlobalMode = appDir !== process.cwd();
22
21
 
23
22
  console.log(`================================`);
24
23
  console.log(`🤖 Nyxora CLI Agent Booting Up...`);
@@ -191,9 +190,8 @@ console.log(`================================`);
191
190
  process.exit(0);
192
191
  }
193
192
 
194
- // 2. Setup boilerplate files if in global mode and they don't exist
193
+ // 2. Setup boilerplate files since we enforce global mode
195
194
  let isFirstBoot = false;
196
- if (isGlobalMode) {
197
195
  const globalConfigPath = getPath('config.yaml');
198
196
  const globalUserMdPath = getPath('user.md');
199
197
  const globalIdentityMdPath = getPath('IDENTITY.md');
@@ -201,7 +199,10 @@ console.log(`================================`);
201
199
  // Copy default config.yaml
202
200
  if (!fs.existsSync(globalConfigPath)) {
203
201
  isFirstBoot = true;
204
- const exampleConfigPath = path.resolve(__dirname, '../../../config.yaml');
202
+ let exampleConfigPath = path.resolve(__dirname, '../../../config.yaml'); // Dev
203
+ if (!fs.existsSync(exampleConfigPath)) {
204
+ exampleConfigPath = path.resolve(__dirname, '../../../../../config.yaml'); // Compiled
205
+ }
205
206
  if (fs.existsSync(exampleConfigPath)) {
206
207
  fs.copyFileSync(exampleConfigPath, globalConfigPath);
207
208
  } else {
@@ -209,13 +210,12 @@ console.log(`================================`);
209
210
  }
210
211
  }
211
212
 
212
- if (!fs.existsSync(globalUserMdPath)) {
213
- fs.writeFileSync(globalUserMdPath, 'Write custom instructions, special rules, user profiles, or the persona you want for Nyxora AI in this file.\n');
214
- }
213
+ if (!fs.existsSync(globalUserMdPath)) {
214
+ fs.writeFileSync(globalUserMdPath, 'Write custom instructions, special rules, user profiles, or the persona you want for Nyxora AI in this file.\n');
215
+ }
215
216
 
216
- if (!fs.existsSync(globalIdentityMdPath)) {
217
- fs.writeFileSync(globalIdentityMdPath, 'You are a Web3 AI assistant named Nyxora.\n');
218
- }
217
+ if (!fs.existsSync(globalIdentityMdPath)) {
218
+ fs.writeFileSync(globalIdentityMdPath, 'You are a Web3 AI assistant named Nyxora.\n');
219
219
  }
220
220
 
221
221
  if (isFirstBoot) {
@@ -17,9 +17,11 @@ import rateLimit from 'express-rate-limit';
17
17
  import http from 'http';
18
18
  import jwt from 'jsonwebtoken';
19
19
  import crypto from 'crypto';
20
+ import os from 'os';
20
21
  import { getPath } from '../config/paths';
21
22
  import { validateToken, getSessionToken } from '../utils/state';
22
23
 
24
+ import { initWebSocket } from './WebSocketManager';
23
25
  import fs from 'fs';
24
26
  import yaml from 'yaml';
25
27
  import { processUserInput, logger } from '../agent/reasoning';
@@ -29,6 +31,7 @@ import { getPublicClient, SUPPORTED_CHAIN_NAMES, getAddress } from '../web3/conf
29
31
  import { TOKEN_MAP, ERC20_ABI } from '../web3/utils/tokens';
30
32
  import { Tracker } from './tracker';
31
33
  import { txManager } from '../agent/transactionManager';
34
+ import multer from 'multer';
32
35
 
33
36
  import { executeTransfer, transferToolDefinition } from '../web3/skills/transfer';
34
37
  import { executeSwap, swapTokenToolDefinition } from '../web3/skills/swapToken';
@@ -68,6 +71,9 @@ import { xManagerToolDefinition } from '../system/skills/xManager';
68
71
  import { notionWorkspaceToolDefinition } from '../system/skills/notionWorkspace';
69
72
  import { audioTranscribeToolDefinition } from '../system/skills/audioTranscribe';
70
73
  import { summarizeTextToolDefinition } from '../system/skills/summarizeText';
74
+ import { scheduleTaskDefinition } from '../system/skills/scheduleTask';
75
+ import { cancelTaskDefinition } from '../system/skills/cancelTask';
76
+ import { cronManager } from '../agent/cronManager';
71
77
  import { updateSecurityPolicyToolDefinition } from '../system/skills/updateSecurityPolicy';
72
78
  import { writeLocalFileToolDefinition } from '../system/skills/writeFile';
73
79
  import { generateExcelToolDefinition } from '../system/skills/generateExcel';
@@ -160,13 +166,11 @@ app.use('/api', (req, res, next) => {
160
166
  });
161
167
 
162
168
  // Serve Static Dashboard
163
- let rootDir = __dirname;
164
- while (!fs.existsSync(path.join(rootDir, 'packages', 'dashboard'))) {
165
- const nextDir = path.dirname(rootDir);
166
- if (nextDir === rootDir || rootDir === '/' || rootDir === 'C:\\') break;
167
- rootDir = nextDir;
169
+ // __dirname is packages/core/dist/gateway (compiled) OR packages/core/src/gateway (dev)
170
+ let dashboardPath = path.join(__dirname, '..', '..', '..', 'dashboard', 'dist'); // Dev
171
+ if (!fs.existsSync(dashboardPath)) {
172
+ dashboardPath = path.join(__dirname, '..', '..', '..', '..', '..', 'packages', 'dashboard', 'dist'); // Compiled
168
173
  }
169
- const dashboardPath = path.join(rootDir, 'packages', 'dashboard', 'dist');
170
174
  app.use(express.static(dashboardPath));
171
175
 
172
176
  app.get('/', (req, res) => {
@@ -181,6 +185,31 @@ app.get('/tos', (req, res) => {
181
185
  res.send(generateTosHtml());
182
186
  });
183
187
 
188
+ const storage = multer.diskStorage({
189
+ destination: function (req, file, cb) {
190
+ const docsDir = path.join(os.homedir(), '.nyxora', 'docs');
191
+ if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });
192
+ cb(null, docsDir);
193
+ },
194
+ filename: function (req, file, cb) {
195
+ const safeName = file.originalname.replace(/[^a-zA-Z0-9.\-_]/g, '_');
196
+ cb(null, `${Date.now()}-${safeName}`);
197
+ }
198
+ });
199
+ const upload = multer({ storage });
200
+
201
+ app.post('/api/upload', upload.single('file'), (req, res) => {
202
+ try {
203
+ if (!req.file) {
204
+ return res.status(400).json({ error: 'No file uploaded' });
205
+ }
206
+ return res.json({ filePath: req.file.path });
207
+ } catch (err) {
208
+ console.error('[Upload] Error:', err);
209
+ return res.status(500).json({ error: 'Failed to save file' });
210
+ }
211
+ });
212
+
184
213
  app.post('/api/upload-google-credentials', (req, res) => {
185
214
  try {
186
215
  const credentials = req.body.credentials;
@@ -380,7 +409,9 @@ const systemSkills = [
380
409
  xManagerToolDefinition,
381
410
  notionWorkspaceToolDefinition,
382
411
  audioTranscribeToolDefinition,
383
- summarizeTextToolDefinition
412
+ summarizeTextToolDefinition,
413
+ scheduleTaskDefinition,
414
+ cancelTaskDefinition
384
415
  ];
385
416
 
386
417
  app.get('/api/stats', (req, res) => {
@@ -400,6 +431,13 @@ app.get('/api/logs', (req, res) => {
400
431
  res.json(Tracker.getLogs());
401
432
  });
402
433
 
434
+ app.get('/api/cron', (req, res) => {
435
+ res.json({
436
+ activeJobs: cronManager.getActiveJobsCount(),
437
+ jobs: cronManager.getJobs()
438
+ });
439
+ });
440
+
403
441
  app.get('/api/skills', (req, res) => {
404
442
  const skillsWithStatus = allSkills.map(skill => ({
405
443
  ...skill,
@@ -873,6 +911,24 @@ function resetIdleTimer() {
873
911
  }, 3 * 60 * 1000); // 3 minutes idle
874
912
  }
875
913
 
914
+ app.post('/api/v1/trade', async (req, res) => {
915
+ try {
916
+ const { message, session_id } = req.body;
917
+ if (!message) return res.status(400).json({ error: 'Message is required' });
918
+
919
+ const traceId = crypto.randomBytes(8).toString('hex');
920
+
921
+ // Asynchronous background execution
922
+ processUserInput(message, session_id || traceId).catch(err => {
923
+ console.error(`[TradeAPI] Error:`, err);
924
+ });
925
+
926
+ res.status(200).json({ status: 'processing', traceId });
927
+ } catch (error: any) {
928
+ res.status(500).json({ error: error.message });
929
+ }
930
+ });
931
+
876
932
  app.post('/api/chat', async (req, res) => {
877
933
  try {
878
934
  const { message, session_id } = req.body;
@@ -1032,6 +1088,9 @@ export function startServer() {
1032
1088
  const server = app.listen(PORT, '127.0.0.1', () => {
1033
1089
  console.log(`🤖 Nyxora API Server running on port ${PORT}`);
1034
1090
 
1091
+ // Initialize WebSocket Manager
1092
+ initWebSocket(server);
1093
+
1035
1094
  // Start the Telegram bot listener
1036
1095
  startTelegramBot();
1037
1096
 
@@ -288,7 +288,7 @@ Provider: ${config.llm.provider}`;
288
288
  { value: 'gitManager', label: 'Git Operations (Commit/Push/Pull)' },
289
289
  { value: 'updateSecurityPolicy', label: 'Update policy.yaml rules', hint: 'safeguard' },
290
290
  { value: 'browseWeb', label: 'Browse & Scrape Webpages' },
291
- { value: 'searchWeb', label: 'Smart Web Search (Tavily/Brave)', hint: 'Requires API Key' },
291
+ { value: 'searchWeb', label: 'Smart Web Search (Tavily/Brave/DuckDuckGo)', hint: 'Optional API Key' },
292
292
  { value: 'googleWorkspace', label: 'Google Workspace (Gmail, Docs, Sheets, Forms)', hint: 'Requires OAuth' },
293
293
  { value: 'notionWorkspace', label: 'Notion Integration' },
294
294
  { value: 'xManager', label: 'X/Twitter Management' },
@@ -320,14 +320,17 @@ Provider: ${config.llm.provider}`;
320
320
  options: [
321
321
  { value: 'tavily', label: 'Tavily Search (Built for AI - 1000 free/mo)' },
322
322
  { value: 'brave', label: 'Brave Search (Privacy focused - 2000 free/mo)' },
323
+ { value: 'duckduckgo', label: 'DuckDuckGo (Free & Built-in)' },
323
324
  ],
324
325
  });
325
326
  if (isCancel(searchProvider)) return process.exit(0);
326
327
 
327
- searchApiKey = (await password({
328
- message: `Enter API Key for ${searchProvider} (Get it free at ${searchProvider === 'tavily' ? 'tavily.com' : 'search.brave.com'}):`,
329
- })) as string;
330
- if (isCancel(searchApiKey)) return process.exit(0);
328
+ if (searchProvider !== 'duckduckgo') {
329
+ searchApiKey = (await password({
330
+ message: `Enter API Key for ${searchProvider} (Get it free at ${searchProvider === 'tavily' ? 'tavily.com' : 'search.brave.com'}):`,
331
+ })) as string;
332
+ if (isCancel(searchApiKey)) return process.exit(0);
333
+ }
331
334
  }
332
335
 
333
336
  const setupTelegram = activeChannels.includes('telegram');
@@ -12,6 +12,9 @@ import { executeApprove, executeAaveSupply, executeVaultDeposit, executeUniv3Min
12
12
  import { executeRevokeApproval } from '../web3/skills/revokeApprovals';
13
13
  import { formatTransactionSuccess, formatTransactionError } from '../utils/formatter';
14
14
  import pc from 'picocolors';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
15
18
 
16
19
  let globalBotInstance: Telegraf | null = null;
17
20
 
@@ -178,6 +181,52 @@ export function startTelegramBot() {
178
181
  }
179
182
  });
180
183
 
184
+ bot.on('document', async (ctx) => {
185
+ const doc = ctx.message.document;
186
+ const caption = ctx.message.caption || '';
187
+ console.log(`[Telegram] Received document from ${ctx.from?.first_name || 'User'}: ${doc.file_name}`);
188
+
189
+ await ctx.sendChatAction('typing');
190
+
191
+ try {
192
+ const fileLink = await ctx.telegram.getFileLink(doc.file_id);
193
+ const docsDir = path.join(os.homedir(), '.nyxora', 'docs');
194
+ if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });
195
+
196
+ const safeName = (doc.file_name || 'telegram_doc').replace(/[^a-zA-Z0-9.\-_]/g, '_');
197
+ const localFilePath = path.join(docsDir, `${Date.now()}-${safeName}`);
198
+
199
+ const res = await fetch(fileLink.toString());
200
+ const buffer = await res.arrayBuffer();
201
+ fs.writeFileSync(localFilePath, Buffer.from(buffer));
202
+
203
+ const prompt = `Tolong analisis dokumen ini: ${localFilePath}\n\n${caption}`;
204
+
205
+ let progressMsgId: number | null = null;
206
+ const onProgress = async (progressText: string) => {
207
+ try {
208
+ if (!progressMsgId) {
209
+ const sent = await ctx.reply(`<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
210
+ progressMsgId = sent.message_id;
211
+ } else {
212
+ await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, `<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
213
+ }
214
+ } catch (e) {}
215
+ };
216
+
217
+ const response = await processUserInput(prompt, 'user', onProgress, ctx.chat?.id.toString());
218
+
219
+ if (progressMsgId) {
220
+ await ctx.telegram.deleteMessage(ctx.chat.id, progressMsgId).catch(() => {});
221
+ }
222
+
223
+ await ctx.reply(formatToTelegramHTML(response), { parse_mode: 'HTML' });
224
+ } catch (error: any) {
225
+ console.error('[Telegram] Error processing document:', error);
226
+ await ctx.reply('❌ Sorry, I failed to download or analyze the document.');
227
+ }
228
+ });
229
+
181
230
  // Handle callbacks
182
231
  bot.action(/^approve_(.+)$/, async (ctx) => {
183
232
  const txId = ctx.match[1];
@@ -1,3 +1,8 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { wsManager } from './WebSocketManager';
4
+ import { getPath } from '../config/paths';
5
+
1
6
  interface Stats {
2
7
  cost: number;
3
8
  tokens: number;
@@ -26,6 +31,53 @@ const eventLogs: EventLog[] = [];
26
31
  const gatewayLogs: GatewayLog[] = [];
27
32
  const MAX_LOGS = 100;
28
33
 
34
+ let trackerFile = '';
35
+ try {
36
+ trackerFile = getPath('tracker.json');
37
+ } catch (e) {
38
+ // Fallback
39
+ }
40
+
41
+ function loadState() {
42
+ if (!trackerFile) return;
43
+ try {
44
+ if (fs.existsSync(trackerFile)) {
45
+ const data = JSON.parse(fs.readFileSync(trackerFile, 'utf8'));
46
+ if (data.stats) Object.assign(stats, data.stats);
47
+ if (data.eventLogs && Array.isArray(data.eventLogs)) {
48
+ eventLogs.splice(0, eventLogs.length, ...data.eventLogs);
49
+ }
50
+ if (data.gatewayLogs && Array.isArray(data.gatewayLogs)) {
51
+ gatewayLogs.splice(0, gatewayLogs.length, ...data.gatewayLogs);
52
+ }
53
+ }
54
+ } catch (e) {}
55
+ }
56
+
57
+ let savePending = false;
58
+ function saveState() {
59
+ if (!trackerFile) return;
60
+ if (savePending) return;
61
+ savePending = true;
62
+ setTimeout(() => {
63
+ flushState();
64
+ savePending = false;
65
+ }, 1000);
66
+ }
67
+
68
+ function flushState() {
69
+ if (!trackerFile) return;
70
+ try {
71
+ fs.writeFileSync(trackerFile, JSON.stringify({ stats, eventLogs, gatewayLogs }));
72
+ } catch (e) {}
73
+ }
74
+
75
+ process.on('exit', flushState);
76
+ process.on('SIGTERM', () => { flushState(); process.exit(0); });
77
+ process.on('SIGINT', () => { flushState(); process.exit(0); });
78
+
79
+ loadState();
80
+
29
81
  function formatTime(): string {
30
82
  const now = new Date();
31
83
  return now.toTimeString().split(' ')[0]; // Returns HH:MM:SS
@@ -41,10 +93,12 @@ export const Tracker = {
41
93
  else if (provider === 'gemini') rate = 0.00001;
42
94
 
43
95
  stats.cost += (amount * rate);
96
+ saveState();
44
97
  },
45
98
 
46
99
  addMessage: () => {
47
100
  stats.messages += 1;
101
+ saveState();
48
102
  },
49
103
 
50
104
  getStats: () => {
@@ -54,11 +108,18 @@ export const Tracker = {
54
108
  addEvent: (event: string, meta: any = {}) => {
55
109
  eventLogs.unshift({ timestamp: formatTime(), event, meta });
56
110
  if (eventLogs.length > MAX_LOGS) eventLogs.pop();
111
+ saveState();
57
112
  },
58
113
 
59
114
  addGatewayLog: (message: string, meta?: any) => {
60
115
  gatewayLogs.unshift({ timestamp: formatTime(), message, meta });
61
116
  if (gatewayLogs.length > MAX_LOGS) gatewayLogs.pop();
117
+ saveState();
118
+
119
+ // Broadcast terminal logs to Dashboard via WebSocket
120
+ if (wsManager) {
121
+ wsManager.broadcastAll(`[${formatTime()}] ${message}`, meta?.level || 'info');
122
+ }
62
123
  },
63
124
 
64
125
  getLogs: () => {
@@ -204,7 +204,7 @@ export class Logger {
204
204
  SELECT * FROM (
205
205
  SELECT role, content, name, tool_call_id, tool_calls, session_id, id
206
206
  FROM messages
207
- WHERE session_id IS NULL
207
+ WHERE session_id IS NULL
208
208
  ORDER BY id DESC LIMIT 40
209
209
  ) ORDER BY id ASC
210
210
  `).all();
@@ -0,0 +1,38 @@
1
+ import { cronManager } from '../../agent/cronManager';
2
+
3
+ export const cancelTaskDefinition = {
4
+ type: "function",
5
+ function: {
6
+ name: "cancel_task",
7
+ description: "Cancel a scheduled background task (cron job). Use this when the user asks you to stop monitoring or cancel a previously scheduled task.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ jobId: {
12
+ type: "string",
13
+ description: "The unique Job ID of the scheduled task to cancel."
14
+ }
15
+ },
16
+ required: ["jobId"]
17
+ }
18
+ }
19
+ };
20
+
21
+ export async function executeCancelTask(args: any): Promise<string> {
22
+ const { jobId } = args;
23
+
24
+ if (!jobId) {
25
+ return "Error: Missing required parameter jobId.";
26
+ }
27
+
28
+ try {
29
+ const success = cronManager.removeJob(jobId);
30
+ if (success) {
31
+ return `Success! I have cancelled the scheduled background task with Job ID: ${jobId}.`;
32
+ } else {
33
+ return `Failed to cancel task. No active task found with Job ID: ${jobId}. You can check active jobs by asking me what tasks are currently running.`;
34
+ }
35
+ } catch (error: any) {
36
+ return `Failed to cancel task: ${error.message}`;
37
+ }
38
+ }
@@ -4,6 +4,13 @@ import path from 'path';
4
4
  export function editLocalFile(filePath: string, searchString: string, replacementString: string): string {
5
5
  try {
6
6
  const absolutePath = path.resolve(filePath);
7
+
8
+ // Security Firewall: Block modification of core configuration files
9
+ const basename = path.basename(absolutePath);
10
+ if (['config.yaml', 'rpc_key.yaml', 'policy.yaml'].includes(basename)) {
11
+ return `Error: Access Denied. You are strictly forbidden from modifying core configuration files directly. If you need to update your agent name, use the update_identity tool instead.`;
12
+ }
13
+
7
14
  if (!fs.existsSync(absolutePath)) {
8
15
  return `Error: File not found at ${absolutePath}`;
9
16
  }