nyxora 26.6.21 → 26.6.22-1

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 (54) hide show
  1. package/bin/nyxora.mjs +14 -2
  2. package/dist/packages/core/src/agent/cronManager.js +107 -0
  3. package/dist/packages/core/src/agent/reasoning.js +85 -22
  4. package/dist/packages/core/src/agent/transactionManager.js +2 -2
  5. package/dist/packages/core/src/agent/updateIdentity.js +71 -0
  6. package/dist/packages/core/src/config/paths.js +5 -20
  7. package/dist/packages/core/src/gateway/chat.js +38 -0
  8. package/dist/packages/core/src/gateway/cli.js +20 -20
  9. package/dist/packages/core/src/gateway/server.js +43 -8
  10. package/dist/packages/core/src/gateway/telegram.js +43 -0
  11. package/dist/packages/core/src/gateway/tracker.js +58 -0
  12. package/dist/packages/core/src/memory/logger.js +1 -1
  13. package/dist/packages/core/src/system/skills/cancelTask.js +40 -0
  14. package/dist/packages/core/src/system/skills/editFile.js +5 -0
  15. package/dist/packages/core/src/system/skills/scheduleTask.js +39 -0
  16. package/dist/packages/core/src/system/skills/writeFile.js +5 -0
  17. package/dist/packages/core/src/web3/skills/getPrice.js +1 -1
  18. package/dist/packages/policy/src/server.js +1 -1
  19. package/package.json +4 -1
  20. package/packages/core/package.json +5 -1
  21. package/packages/core/src/agent/cronManager.ts +87 -0
  22. package/packages/core/src/agent/reasoning.ts +91 -21
  23. package/packages/core/src/agent/transactionManager.ts +2 -1
  24. package/packages/core/src/agent/updateIdentity.ts +68 -0
  25. package/packages/core/src/config/paths.ts +5 -23
  26. package/packages/core/src/gateway/chat.ts +40 -1
  27. package/packages/core/src/gateway/cli.ts +10 -10
  28. package/packages/core/src/gateway/server.ts +44 -7
  29. package/packages/core/src/gateway/telegram.ts +49 -0
  30. package/packages/core/src/gateway/tracker.ts +55 -0
  31. package/packages/core/src/memory/logger.ts +1 -1
  32. package/packages/core/src/system/skills/cancelTask.ts +38 -0
  33. package/packages/core/src/system/skills/editFile.ts +7 -0
  34. package/packages/core/src/system/skills/scheduleTask.ts +38 -0
  35. package/packages/core/src/system/skills/writeFile.ts +7 -0
  36. package/packages/core/src/web3/skills/getPrice.ts +1 -1
  37. package/packages/dashboard/dist/assets/index-CjZWf1Ei.css +1 -0
  38. package/packages/dashboard/dist/assets/index-CmWZofn_.js +16 -0
  39. package/packages/dashboard/dist/index.html +2 -2
  40. package/packages/dashboard/dist/routers/0x.png +0 -0
  41. package/packages/dashboard/dist/routers/1inch.png +0 -0
  42. package/packages/dashboard/dist/routers/cmc.png +0 -0
  43. package/packages/dashboard/dist/routers/kyberswap.png +0 -0
  44. package/packages/dashboard/dist/routers/lifi.png +0 -0
  45. package/packages/dashboard/dist/routers/openocean.png +0 -0
  46. package/packages/dashboard/dist/routers/relay.png +0 -0
  47. package/packages/dashboard/dist/routers/zerion.png +0 -0
  48. package/packages/dashboard/package.json +1 -1
  49. package/packages/mcp-server/package.json +1 -1
  50. package/packages/policy/package.json +1 -1
  51. package/packages/policy/src/server.ts +1 -1
  52. package/packages/signer/package.json +1 -1
  53. package/packages/dashboard/dist/assets/index-CQNHWZtN.css +0 -1
  54. package/packages/dashboard/dist/assets/index-Di9x08yk.js +0 -16
@@ -18,6 +18,7 @@ process.on('uncaughtException', (error) => {
18
18
  const path_1 = __importDefault(require("path"));
19
19
  const helmet_1 = __importDefault(require("helmet"));
20
20
  const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
21
+ const os_1 = __importDefault(require("os"));
21
22
  const paths_1 = require("../config/paths");
22
23
  const state_1 = require("../utils/state");
23
24
  const fs_1 = __importDefault(require("fs"));
@@ -29,6 +30,7 @@ const config_1 = require("../web3/config");
29
30
  const tokens_1 = require("../web3/utils/tokens");
30
31
  const tracker_1 = require("./tracker");
31
32
  const transactionManager_1 = require("../agent/transactionManager");
33
+ const multer_1 = __importDefault(require("multer"));
32
34
  const transfer_1 = require("../web3/skills/transfer");
33
35
  const swapToken_1 = require("../web3/skills/swapToken");
34
36
  const getBalance_1 = require("../web3/skills/getBalance");
@@ -64,6 +66,9 @@ const xManager_1 = require("../system/skills/xManager");
64
66
  const notionWorkspace_1 = require("../system/skills/notionWorkspace");
65
67
  const audioTranscribe_1 = require("../system/skills/audioTranscribe");
66
68
  const summarizeText_1 = require("../system/skills/summarizeText");
69
+ const scheduleTask_1 = require("../system/skills/scheduleTask");
70
+ const cancelTask_1 = require("../system/skills/cancelTask");
71
+ const cronManager_1 = require("../agent/cronManager");
67
72
  const updateSecurityPolicy_1 = require("../system/skills/updateSecurityPolicy");
68
73
  const writeFile_1 = require("../system/skills/writeFile");
69
74
  const generateExcel_1 = require("../system/skills/generateExcel");
@@ -141,14 +146,11 @@ app.use('/api', (req, res, next) => {
141
146
  next();
142
147
  });
143
148
  // Serve Static Dashboard
144
- let rootDir = __dirname;
145
- while (!fs_1.default.existsSync(path_1.default.join(rootDir, 'packages', 'dashboard'))) {
146
- const nextDir = path_1.default.dirname(rootDir);
147
- if (nextDir === rootDir || rootDir === '/' || rootDir === 'C:\\')
148
- break;
149
- rootDir = nextDir;
149
+ // __dirname is packages/core/dist/gateway (compiled) OR packages/core/src/gateway (dev)
150
+ let dashboardPath = path_1.default.join(__dirname, '..', '..', '..', 'dashboard', 'dist'); // Dev
151
+ if (!fs_1.default.existsSync(dashboardPath)) {
152
+ dashboardPath = path_1.default.join(__dirname, '..', '..', '..', '..', '..', 'packages', 'dashboard', 'dist'); // Compiled
150
153
  }
151
- const dashboardPath = path_1.default.join(rootDir, 'packages', 'dashboard', 'dist');
152
154
  app.use(express_1.default.static(dashboardPath));
153
155
  app.get('/', (req, res) => {
154
156
  res.sendFile(path_1.default.join(dashboardPath, 'index.html'));
@@ -159,6 +161,31 @@ app.get('/privacy', (req, res) => {
159
161
  app.get('/tos', (req, res) => {
160
162
  res.send((0, legalGenerator_1.generateTosHtml)());
161
163
  });
164
+ const storage = multer_1.default.diskStorage({
165
+ destination: function (req, file, cb) {
166
+ const docsDir = path_1.default.join(os_1.default.homedir(), '.nyxora', 'docs');
167
+ if (!fs_1.default.existsSync(docsDir))
168
+ fs_1.default.mkdirSync(docsDir, { recursive: true });
169
+ cb(null, docsDir);
170
+ },
171
+ filename: function (req, file, cb) {
172
+ const safeName = file.originalname.replace(/[^a-zA-Z0-9.\-_]/g, '_');
173
+ cb(null, `${Date.now()}-${safeName}`);
174
+ }
175
+ });
176
+ const upload = (0, multer_1.default)({ storage });
177
+ app.post('/api/upload', upload.single('file'), (req, res) => {
178
+ try {
179
+ if (!req.file) {
180
+ return res.status(400).json({ error: 'No file uploaded' });
181
+ }
182
+ return res.json({ filePath: req.file.path });
183
+ }
184
+ catch (err) {
185
+ console.error('[Upload] Error:', err);
186
+ return res.status(500).json({ error: 'Failed to save file' });
187
+ }
188
+ });
162
189
  app.post('/api/upload-google-credentials', (req, res) => {
163
190
  try {
164
191
  const credentials = req.body.credentials;
@@ -352,7 +379,9 @@ const systemSkills = [
352
379
  xManager_1.xManagerToolDefinition,
353
380
  notionWorkspace_1.notionWorkspaceToolDefinition,
354
381
  audioTranscribe_1.audioTranscribeToolDefinition,
355
- summarizeText_1.summarizeTextToolDefinition
382
+ summarizeText_1.summarizeTextToolDefinition,
383
+ scheduleTask_1.scheduleTaskDefinition,
384
+ cancelTask_1.cancelTaskDefinition
356
385
  ];
357
386
  app.get('/api/stats', (req, res) => {
358
387
  const stats = tracker_1.Tracker.getStats();
@@ -366,6 +395,12 @@ app.get('/api/stats', (req, res) => {
366
395
  app.get('/api/logs', (req, res) => {
367
396
  res.json(tracker_1.Tracker.getLogs());
368
397
  });
398
+ app.get('/api/cron', (req, res) => {
399
+ res.json({
400
+ activeJobs: cronManager_1.cronManager.getActiveJobsCount(),
401
+ jobs: cronManager_1.cronManager.getJobs()
402
+ });
403
+ });
369
404
  app.get('/api/skills', (req, res) => {
370
405
  const skillsWithStatus = allSkills.map(skill => ({
371
406
  ...skill,
@@ -19,6 +19,9 @@ const executeDefi_1 = require("../web3/skills/executeDefi");
19
19
  const revokeApprovals_1 = require("../web3/skills/revokeApprovals");
20
20
  const formatter_1 = require("../utils/formatter");
21
21
  const picocolors_1 = __importDefault(require("picocolors"));
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const os_1 = __importDefault(require("os"));
22
25
  let globalBotInstance = null;
23
26
  function formatToTelegramHTML(text) {
24
27
  if (!text)
@@ -162,6 +165,46 @@ function startTelegramBot() {
162
165
  await ctx.reply('❌ Sorry, I encountered an error while processing your message.');
163
166
  }
164
167
  });
168
+ bot.on('document', async (ctx) => {
169
+ const doc = ctx.message.document;
170
+ const caption = ctx.message.caption || '';
171
+ console.log(`[Telegram] Received document from ${ctx.from?.first_name || 'User'}: ${doc.file_name}`);
172
+ await ctx.sendChatAction('typing');
173
+ try {
174
+ const fileLink = await ctx.telegram.getFileLink(doc.file_id);
175
+ const docsDir = path_1.default.join(os_1.default.homedir(), '.nyxora', 'docs');
176
+ if (!fs_1.default.existsSync(docsDir))
177
+ fs_1.default.mkdirSync(docsDir, { recursive: true });
178
+ const safeName = (doc.file_name || 'telegram_doc').replace(/[^a-zA-Z0-9.\-_]/g, '_');
179
+ const localFilePath = path_1.default.join(docsDir, `${Date.now()}-${safeName}`);
180
+ const res = await fetch(fileLink.toString());
181
+ const buffer = await res.arrayBuffer();
182
+ fs_1.default.writeFileSync(localFilePath, Buffer.from(buffer));
183
+ const prompt = `Tolong analisis dokumen ini: ${localFilePath}\n\n${caption}`;
184
+ let progressMsgId = null;
185
+ const onProgress = async (progressText) => {
186
+ try {
187
+ if (!progressMsgId) {
188
+ const sent = await ctx.reply(`<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
189
+ progressMsgId = sent.message_id;
190
+ }
191
+ else {
192
+ await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, `<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
193
+ }
194
+ }
195
+ catch (e) { }
196
+ };
197
+ const response = await (0, reasoning_1.processUserInput)(prompt, 'user', onProgress, ctx.chat?.id.toString());
198
+ if (progressMsgId) {
199
+ await ctx.telegram.deleteMessage(ctx.chat.id, progressMsgId).catch(() => { });
200
+ }
201
+ await ctx.reply(formatToTelegramHTML(response), { parse_mode: 'HTML' });
202
+ }
203
+ catch (error) {
204
+ console.error('[Telegram] Error processing document:', error);
205
+ await ctx.reply('❌ Sorry, I failed to download or analyze the document.');
206
+ }
207
+ });
165
208
  // Handle callbacks
166
209
  bot.action(/^approve_(.+)$/, async (ctx) => {
167
210
  const txId = ctx.match[1];
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.Tracker = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const paths_1 = require("../config/paths");
4
9
  const stats = {
5
10
  cost: 0,
6
11
  tokens: 0,
@@ -9,6 +14,55 @@ const stats = {
9
14
  const eventLogs = [];
10
15
  const gatewayLogs = [];
11
16
  const MAX_LOGS = 100;
17
+ let trackerFile = '';
18
+ try {
19
+ trackerFile = (0, paths_1.getPath)('tracker.json');
20
+ }
21
+ catch (e) {
22
+ // Fallback
23
+ }
24
+ function loadState() {
25
+ if (!trackerFile)
26
+ return;
27
+ try {
28
+ if (fs_1.default.existsSync(trackerFile)) {
29
+ const data = JSON.parse(fs_1.default.readFileSync(trackerFile, 'utf8'));
30
+ if (data.stats)
31
+ Object.assign(stats, data.stats);
32
+ if (data.eventLogs && Array.isArray(data.eventLogs)) {
33
+ eventLogs.splice(0, eventLogs.length, ...data.eventLogs);
34
+ }
35
+ if (data.gatewayLogs && Array.isArray(data.gatewayLogs)) {
36
+ gatewayLogs.splice(0, gatewayLogs.length, ...data.gatewayLogs);
37
+ }
38
+ }
39
+ }
40
+ catch (e) { }
41
+ }
42
+ let savePending = false;
43
+ function saveState() {
44
+ if (!trackerFile)
45
+ return;
46
+ if (savePending)
47
+ return;
48
+ savePending = true;
49
+ setTimeout(() => {
50
+ flushState();
51
+ savePending = false;
52
+ }, 1000);
53
+ }
54
+ function flushState() {
55
+ if (!trackerFile)
56
+ return;
57
+ try {
58
+ fs_1.default.writeFileSync(trackerFile, JSON.stringify({ stats, eventLogs, gatewayLogs }));
59
+ }
60
+ catch (e) { }
61
+ }
62
+ process.on('exit', flushState);
63
+ process.on('SIGTERM', () => { flushState(); process.exit(0); });
64
+ process.on('SIGINT', () => { flushState(); process.exit(0); });
65
+ loadState();
12
66
  function formatTime() {
13
67
  const now = new Date();
14
68
  return now.toTimeString().split(' ')[0]; // Returns HH:MM:SS
@@ -23,9 +77,11 @@ exports.Tracker = {
23
77
  else if (provider === 'gemini')
24
78
  rate = 0.00001;
25
79
  stats.cost += (amount * rate);
80
+ saveState();
26
81
  },
27
82
  addMessage: () => {
28
83
  stats.messages += 1;
84
+ saveState();
29
85
  },
30
86
  getStats: () => {
31
87
  return { ...stats, cost: Number(stats.cost.toFixed(4)) };
@@ -34,11 +90,13 @@ exports.Tracker = {
34
90
  eventLogs.unshift({ timestamp: formatTime(), event, meta });
35
91
  if (eventLogs.length > MAX_LOGS)
36
92
  eventLogs.pop();
93
+ saveState();
37
94
  },
38
95
  addGatewayLog: (message, meta) => {
39
96
  gatewayLogs.unshift({ timestamp: formatTime(), message, meta });
40
97
  if (gatewayLogs.length > MAX_LOGS)
41
98
  gatewayLogs.pop();
99
+ saveState();
42
100
  },
43
101
  getLogs: () => {
44
102
  return {
@@ -169,7 +169,7 @@ class Logger {
169
169
  SELECT * FROM (
170
170
  SELECT role, content, name, tool_call_id, tool_calls, session_id, id
171
171
  FROM messages
172
- WHERE session_id IS NULL
172
+ WHERE session_id IS NULL
173
173
  ORDER BY id DESC LIMIT 40
174
174
  ) ORDER BY id ASC
175
175
  `).all();
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cancelTaskDefinition = void 0;
4
+ exports.executeCancelTask = executeCancelTask;
5
+ const cronManager_1 = require("../../agent/cronManager");
6
+ exports.cancelTaskDefinition = {
7
+ type: "function",
8
+ function: {
9
+ name: "cancel_task",
10
+ description: "Cancel a scheduled background task (cron job). Use this when the user asks you to stop monitoring or cancel a previously scheduled task.",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ jobId: {
15
+ type: "string",
16
+ description: "The unique Job ID of the scheduled task to cancel."
17
+ }
18
+ },
19
+ required: ["jobId"]
20
+ }
21
+ }
22
+ };
23
+ async function executeCancelTask(args) {
24
+ const { jobId } = args;
25
+ if (!jobId) {
26
+ return "Error: Missing required parameter jobId.";
27
+ }
28
+ try {
29
+ const success = cronManager_1.cronManager.removeJob(jobId);
30
+ if (success) {
31
+ return `Success! I have cancelled the scheduled background task with Job ID: ${jobId}.`;
32
+ }
33
+ else {
34
+ 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.`;
35
+ }
36
+ }
37
+ catch (error) {
38
+ return `Failed to cancel task: ${error.message}`;
39
+ }
40
+ }
@@ -10,6 +10,11 @@ const path_1 = __importDefault(require("path"));
10
10
  function editLocalFile(filePath, searchString, replacementString) {
11
11
  try {
12
12
  const absolutePath = path_1.default.resolve(filePath);
13
+ // Security Firewall: Block modification of core configuration files
14
+ const basename = path_1.default.basename(absolutePath);
15
+ if (['config.yaml', 'rpc_key.yaml', 'policy.yaml'].includes(basename)) {
16
+ 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.`;
17
+ }
13
18
  if (!fs_1.default.existsSync(absolutePath)) {
14
19
  return `Error: File not found at ${absolutePath}`;
15
20
  }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scheduleTaskDefinition = void 0;
4
+ exports.executeScheduleTask = executeScheduleTask;
5
+ const cronManager_1 = require("../../agent/cronManager");
6
+ exports.scheduleTaskDefinition = {
7
+ type: "function",
8
+ function: {
9
+ name: "schedule_task",
10
+ description: "Schedule a recurring background task for the AI to execute automatically using a cron expression. Use this when the user asks you to remind them or monitor something periodically (e.g. 'check price every hour', 'monitor my wallet every 5 minutes'). The AI will execute the provided prompt at the scheduled interval and send the result via Telegram notification.",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ cronExpression: {
15
+ type: "string",
16
+ description: "A standard 5-field cron expression (minute hour day month day-of-week). Examples: '*/5 * * * *' (every 5 mins), '0 * * * *' (every hour), '0 8 * * *' (every day at 8 AM)."
17
+ },
18
+ prompt: {
19
+ type: "string",
20
+ description: "The prompt/command that the AI should execute when the cron triggers. E.g., 'What is the current price of Ethereum?'"
21
+ }
22
+ },
23
+ required: ["cronExpression", "prompt"]
24
+ }
25
+ }
26
+ };
27
+ async function executeScheduleTask(args) {
28
+ const { cronExpression, prompt } = args;
29
+ if (!cronExpression || !prompt) {
30
+ return "Error: Missing required parameters cronExpression or prompt.";
31
+ }
32
+ try {
33
+ const jobId = cronManager_1.cronManager.addJob(cronExpression, prompt);
34
+ return `Success! I have scheduled the background task.\nJob ID: ${jobId}\nSchedule: ${cronExpression}\nPrompt to execute: "${prompt}"\n\nYou will receive a notification via Telegram every time this task completes.`;
35
+ }
36
+ catch (error) {
37
+ return `Failed to schedule task: ${error.message}. Please ensure the cron expression is valid.`;
38
+ }
39
+ }
@@ -10,6 +10,11 @@ const path_1 = __importDefault(require("path"));
10
10
  function writeLocalFile(filePath, content) {
11
11
  try {
12
12
  const absolutePath = path_1.default.resolve(filePath);
13
+ // Security Firewall: Block modification of core configuration files
14
+ const basename = path_1.default.basename(absolutePath);
15
+ if (['config.yaml', 'rpc_key.yaml', 'policy.yaml'].includes(basename)) {
16
+ 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.`;
17
+ }
13
18
  const dir = path_1.default.dirname(absolutePath);
14
19
  if (!fs_1.default.existsSync(dir)) {
15
20
  fs_1.default.mkdirSync(dir, { recursive: true });
@@ -7,7 +7,7 @@ exports.getPriceToolDefinition = {
7
7
  type: "function",
8
8
  function: {
9
9
  name: "get_price",
10
- description: "Fetches the current price of a cryptocurrency. Use ONLY when the user explicitly asks for a simple price check (e.g. 'harga', 'price'). Do NOT use this for 'analysis', 'market analysis', or 'analisis pasar'.",
10
+ description: "Fetches the current price of a cryptocurrency.",
11
11
  parameters: {
12
12
  type: "object",
13
13
  properties: {
@@ -237,7 +237,7 @@ app.post('/approve-tx/:id', (req, res) => {
237
237
  signerReq.write(requestPayload);
238
238
  signerReq.end();
239
239
  });
240
- const server = app.listen(PORT, '127.0.0.1', () => {
240
+ const server = app.listen(Number(PORT), '127.0.0.1', () => {
241
241
  console.log(`[Policy Engine] Listening on 127.0.0.1:${PORT} (Secured Local Loopback)`);
242
242
  });
243
243
  server.on('error', (e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nyxora",
3
- "version": "26.6.21",
3
+ "version": "26.6.22-1",
4
4
  "description": "Your Personal Web3 Assistant",
5
5
  "keywords": [
6
6
  "web3",
@@ -65,6 +65,8 @@
65
65
  "isolated-vm": "^6.1.2",
66
66
  "jsonwebtoken": "^9.0.2",
67
67
  "mammoth": "^1.6.0",
68
+ "multer": "^2.2.0",
69
+ "node-cron": "^4.4.1",
68
70
  "open": "^11.0.0",
69
71
  "openai": "^6.39.0",
70
72
  "pdf-parse": "^2.4.5",
@@ -97,6 +99,7 @@
97
99
  "devDependencies": {
98
100
  "@types/jsonwebtoken": "^9.0.5",
99
101
  "@types/node": "^25.9.1",
102
+ "@types/node-cron": "^3.0.11",
100
103
  "concurrently": "^10.0.3",
101
104
  "oxc-minify": "^0.135.0",
102
105
  "vitepress": "^2.0.0-alpha.17"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nyxora-agent-core",
3
- "version": "26.6.21",
3
+ "version": "26.6.22-1",
4
4
  "private": true,
5
5
  "main": "src/gateway/server.ts",
6
6
  "dependencies": {
@@ -14,6 +14,8 @@
14
14
  "express-rate-limit": "^7.5.0",
15
15
  "helmet": "^8.0.0",
16
16
  "mammoth": "^1.12.0",
17
+ "multer": "^2.2.0",
18
+ "node-cron": "^4.4.1",
17
19
  "open": "^11.0.0",
18
20
  "openai": "^6.39.0",
19
21
  "pdf-parse": "^2.4.5",
@@ -26,7 +28,9 @@
26
28
  "zod": "^3.25.76"
27
29
  },
28
30
  "devDependencies": {
31
+ "@types/multer": "^2.1.0",
29
32
  "@types/node": "^25.9.2",
33
+ "@types/node-cron": "^3.0.11",
30
34
  "@types/pdf-parse": "^1.1.5",
31
35
  "vitest": "^4.1.8"
32
36
  },
@@ -0,0 +1,87 @@
1
+ import * as cron from 'node-cron';
2
+ import { loadConfig } from '../config/parser';
3
+ import { sendPushNotification } from '../gateway/telegram';
4
+ import { randomUUID } from 'crypto';
5
+ import pc from 'picocolors';
6
+
7
+ export interface CronJob {
8
+ id: string;
9
+ expression: string;
10
+ prompt: string;
11
+ task: cron.ScheduledTask;
12
+ createdAt: number;
13
+ }
14
+
15
+ class CronManager {
16
+ private jobs: Map<string, CronJob> = new Map();
17
+
18
+ public addJob(expression: string, prompt: string): string {
19
+ const id = randomUUID();
20
+
21
+ // Validate expression
22
+ if (!cron.validate(expression)) {
23
+ throw new Error(`Invalid cron expression: ${expression}`);
24
+ }
25
+
26
+ const task = cron.schedule(expression, async () => {
27
+ console.log(pc.cyan(`[Cron] Executing job ${id}: "${prompt}"`));
28
+ try {
29
+ // Dynamically import processUserInput to avoid circular dependencies
30
+ const { processUserInput } = await import('./reasoning');
31
+
32
+ // Execute the prompt as a background system task
33
+ const response = await processUserInput(prompt, 'system', undefined, `cron-${id}`);
34
+
35
+ // Push notification to Telegram if configured
36
+ const config = loadConfig();
37
+ if (config.integrations?.telegram?.enabled && config.integrations?.telegram?.authorized_chat_id) {
38
+ const message = `🤖 *AI Scheduled Report*\n\n${response}`;
39
+ await sendPushNotification(config.integrations.telegram.authorized_chat_id, message);
40
+ }
41
+ } catch (err: any) {
42
+ console.error(pc.red(`[Cron] Failed to execute job ${id}:`), err);
43
+ const config = loadConfig();
44
+ if (config.integrations?.telegram?.enabled && config.integrations?.telegram?.authorized_chat_id) {
45
+ await sendPushNotification(config.integrations.telegram.authorized_chat_id, `⚠️ *Cron Job Error*\n\nPrompt: ${prompt}\nError: ${err.message}`);
46
+ }
47
+ }
48
+ });
49
+
50
+ this.jobs.set(id, {
51
+ id,
52
+ expression,
53
+ prompt,
54
+ task,
55
+ createdAt: Date.now()
56
+ });
57
+
58
+ console.log(pc.green(`[Cron] Scheduled new job ${id} with expression '${expression}'`));
59
+ return id;
60
+ }
61
+
62
+ public removeJob(id: string): boolean {
63
+ const job = this.jobs.get(id);
64
+ if (job) {
65
+ job.task.stop();
66
+ this.jobs.delete(id);
67
+ console.log(pc.yellow(`[Cron] Removed job ${id}`));
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ public getJobs(): Omit<CronJob, 'task'>[] {
74
+ return Array.from(this.jobs.values()).map(job => ({
75
+ id: job.id,
76
+ expression: job.expression,
77
+ prompt: job.prompt,
78
+ createdAt: job.createdAt
79
+ }));
80
+ }
81
+
82
+ public getActiveJobsCount(): number {
83
+ return this.jobs.size;
84
+ }
85
+ }
86
+
87
+ export const cronManager = new CronManager();