nyxora 1.6.2 → 1.6.4
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.
- package/CHANGELOG.md +23 -0
- package/README.md +22 -12
- package/SECURITY.md +25 -21
- package/assets/raw-diagram.png +0 -0
- package/assets/security-flow.png +0 -0
- package/bin/nyxora.mjs +236 -0
- package/launcher.js +8 -3
- package/launcher.ts +28 -1
- package/package.json +11 -8
- package/packages/core/package.json +4 -4
- package/packages/core/src/agent/reasoning.ts +10 -8
- package/packages/core/src/config/parser.ts +2 -1
- package/packages/core/src/gateway/cli.ts +2 -64
- package/packages/core/src/gateway/server.ts +89 -8
- package/packages/core/src/gateway/setup-cli.ts +7 -0
- package/packages/core/src/gateway/setup.ts +52 -28
- package/packages/core/src/gateway/telegram.ts +147 -89
- package/packages/core/src/memory/logger.ts +83 -20
- package/packages/core/src/system/pluginManager.ts +48 -34
- package/packages/core/src/utils/state.ts +15 -2
- package/packages/core/src/web3/config.ts +18 -3
- package/packages/core/src/web3/skills/marketAnalysis.ts +43 -17
- package/packages/core/src/web3/skills/swapToken.ts +9 -1
- package/packages/dashboard/dist/assets/index-CfIids2e.js +170 -0
- package/packages/dashboard/dist/assets/index-POJM-7Fd.css +1 -0
- package/packages/dashboard/dist/favicon.svg +1 -1
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/package.json +7 -7
- package/packages/dashboard/public/favicon.svg +1 -1
- package/packages/dashboard/src/App.tsx +224 -167
- package/packages/dashboard/src/Settings.tsx +55 -0
- package/packages/dashboard/src/Skills.tsx +8 -1
- package/packages/dashboard/src/index.css +146 -35
- package/packages/policy/package.json +1 -1
- package/packages/policy/src/server.ts +21 -28
- package/packages/signer/package.json +1 -1
- package/packages/signer/src/server.ts +40 -13
- package/test-db.ts +3 -0
- package/bin/nyxora.js +0 -13
- package/packages/dashboard/dist/assets/index-BK4qmIy6.js +0 -200
- package/packages/dashboard/dist/assets/index-C1m4ohce.css +0 -1
- package/packages/dashboard/package-lock.json +0 -2748
- package/packages/dashboard/src/Memory.tsx +0 -110
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Telegraf, Markup } from 'telegraf';
|
|
2
2
|
import { processUserInput, logger } from '../agent/reasoning';
|
|
3
|
-
import { loadConfig } from '../config/parser';
|
|
3
|
+
import { loadConfig, saveConfig } from '../config/parser';
|
|
4
4
|
import { txManager } from '../agent/transactionManager';
|
|
5
5
|
import { executeTransfer } from '../web3/skills/transfer';
|
|
6
6
|
import { executeSwap } from '../web3/skills/swapToken';
|
|
@@ -14,143 +14,201 @@ export function startTelegramBot() {
|
|
|
14
14
|
const config = loadConfig();
|
|
15
15
|
const token = config.integrations?.telegram?.bot_token;
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
if (!token) {
|
|
19
18
|
console.log('[Telegram] No TELEGRAM_BOT_TOKEN found in config.yaml. Bot is disabled.');
|
|
20
19
|
return;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
try {
|
|
24
|
-
const bot = new
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
const bot = new Telegraf(token);
|
|
24
|
+
|
|
25
|
+
// Pairing state variables
|
|
26
|
+
const isPaired = !!config.integrations?.telegram?.authorized_chat_id;
|
|
27
|
+
let generatedPin = '';
|
|
28
|
+
let pinExpiry = 0;
|
|
29
|
+
|
|
30
|
+
if (!isPaired) {
|
|
31
|
+
generatedPin = Math.floor(100000 + Math.random() * 900000).toString();
|
|
32
|
+
pinExpiry = Date.now() + 5 * 60 * 1000; // 5 minutes TTL
|
|
33
|
+
|
|
34
|
+
console.log(pc.yellow('\n==================================================='));
|
|
35
|
+
console.log(pc.yellow('🔐 OTORISASI BOT TELEGRAM DIBUTUHKAN'));
|
|
36
|
+
console.log(pc.yellow('==================================================='));
|
|
37
|
+
console.log('Bot Telegram Anda saat ini terkunci demi keamanan.');
|
|
38
|
+
console.log('Buka Telegram Anda, dan kirimkan perintah berikut ke bot Anda:\n');
|
|
39
|
+
console.log(pc.cyan(` /auth ${generatedPin}\n`));
|
|
40
|
+
console.log(pc.gray('(Kode OTP ini akan kedaluwarsa dalam 5 menit)\n'));
|
|
41
|
+
console.log('⏳ Menunggu pesan masuk...');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Security Middleware (OTP & Authorization)
|
|
45
|
+
bot.use(async (ctx, next) => {
|
|
46
|
+
const currentConfig = loadConfig();
|
|
47
|
+
const authId = currentConfig.integrations?.telegram?.authorized_chat_id;
|
|
48
|
+
|
|
49
|
+
if (authId) {
|
|
50
|
+
// Paired mode: Reject unauthorized users silently
|
|
51
|
+
if (ctx.chat?.id !== authId) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
return next();
|
|
55
|
+
}
|
|
31
56
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
// Pairing mode: Listen for /auth command
|
|
58
|
+
if (ctx.message && 'text' in ctx.message) {
|
|
59
|
+
const text = ctx.message.text || '';
|
|
60
|
+
if (text.startsWith('/auth ')) {
|
|
61
|
+
const pin = text.split(' ')[1];
|
|
62
|
+
if (Date.now() > pinExpiry) {
|
|
63
|
+
await ctx.reply('❌ The pairing PIN has expired. Please restart the CLI to generate a new one.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (pin === generatedPin) {
|
|
67
|
+
// Success
|
|
68
|
+
if (!currentConfig.integrations) currentConfig.integrations = {};
|
|
69
|
+
if (!currentConfig.integrations.telegram) currentConfig.integrations.telegram = { enabled: true, bot_token: token };
|
|
70
|
+
|
|
71
|
+
currentConfig.integrations.telegram.authorized_chat_id = ctx.chat?.id;
|
|
72
|
+
saveConfig(currentConfig);
|
|
73
|
+
|
|
74
|
+
await ctx.reply('✅ Otorisasi Berhasil! Agen Nyxora kini hanya akan mematuhi perintah Anda. Koneksi diamankan.');
|
|
75
|
+
console.log(pc.green(`\n[Telegram] Successfully paired with Chat ID: ${ctx.chat?.id}`));
|
|
76
|
+
return; // Done parsing auth, ignore this specific message for further logic
|
|
77
|
+
} else {
|
|
78
|
+
await ctx.reply('❌ PIN salah.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
36
82
|
}
|
|
83
|
+
|
|
84
|
+
// If not paired and not an auth command, silently drop
|
|
85
|
+
return;
|
|
86
|
+
});
|
|
37
87
|
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
bot.command('clear', async (ctx) => {
|
|
89
|
+
logger.clear();
|
|
90
|
+
await ctx.reply('✅ Memori AI telah dihapus. Mari kita mulai obrolan baru!');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
bot.on('text', async (ctx) => {
|
|
94
|
+
const text = ctx.message.text;
|
|
95
|
+
if (text.startsWith('/')) return; // Ignore other commands
|
|
40
96
|
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
console.log(`[Telegram] Received from ${ctx.from?.first_name || 'User'}: ${text}`);
|
|
98
|
+
|
|
99
|
+
// Send typing action
|
|
100
|
+
await ctx.sendChatAction('typing');
|
|
43
101
|
|
|
44
102
|
try {
|
|
45
103
|
let progressMsgId: number | null = null;
|
|
46
104
|
const onProgress = async (progressText: string) => {
|
|
47
105
|
try {
|
|
48
106
|
if (!progressMsgId) {
|
|
49
|
-
const sent = await
|
|
107
|
+
const sent = await ctx.reply(progressText, { parse_mode: 'Markdown' });
|
|
50
108
|
progressMsgId = sent.message_id;
|
|
51
109
|
} else {
|
|
52
|
-
await
|
|
110
|
+
await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, progressText, { parse_mode: 'Markdown' });
|
|
53
111
|
}
|
|
54
112
|
} catch (e) {}
|
|
55
113
|
};
|
|
56
114
|
|
|
57
|
-
// Feed the message to the AI agent
|
|
58
115
|
const response = await processUserInput(text, 'user', onProgress);
|
|
59
|
-
|
|
116
|
+
|
|
60
117
|
if (progressMsgId) {
|
|
61
|
-
|
|
62
|
-
bot.deleteMessage(chatId, progressMsgId).catch(() => {});
|
|
118
|
+
await ctx.telegram.deleteMessage(ctx.chat.id, progressMsgId).catch(() => {});
|
|
63
119
|
}
|
|
64
|
-
|
|
65
|
-
// Send the AI's response back to Telegram
|
|
66
|
-
|
|
67
|
-
// Check for newly created pending transactions
|
|
120
|
+
|
|
68
121
|
const pendingTxs = txManager.getPending();
|
|
69
122
|
if (pendingTxs.length > 0) {
|
|
70
123
|
const latestTx = pendingTxs[pendingTxs.length - 1];
|
|
71
|
-
// If the transaction was created within the last 2 minutes, show the inline keyboard
|
|
72
124
|
if (Date.now() - latestTx.createdAt < 120000) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
});
|
|
125
|
+
await ctx.reply(response, Markup.inlineKeyboard([
|
|
126
|
+
[
|
|
127
|
+
Markup.button.callback('✅ Approve', `approve_${latestTx.id}`),
|
|
128
|
+
Markup.button.callback('❌ Reject', `reject_${latestTx.id}`)
|
|
129
|
+
]
|
|
130
|
+
]));
|
|
81
131
|
return;
|
|
82
132
|
}
|
|
83
133
|
}
|
|
84
|
-
|
|
85
|
-
|
|
134
|
+
|
|
135
|
+
await ctx.reply(response);
|
|
86
136
|
} catch (error: any) {
|
|
87
137
|
console.error('[Telegram] Error processing message:', error);
|
|
88
|
-
|
|
138
|
+
await ctx.reply('❌ Sorry, I encountered an error while processing your message.');
|
|
89
139
|
}
|
|
90
140
|
});
|
|
91
141
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const [action, txId] = query.data.split('_');
|
|
142
|
+
// Handle callbacks
|
|
143
|
+
bot.action(/^approve_(.+)$/, async (ctx) => {
|
|
144
|
+
const txId = ctx.match[1];
|
|
97
145
|
const tx = txManager.getTransaction(txId);
|
|
98
146
|
|
|
99
147
|
if (!tx || tx.status !== 'pending') {
|
|
100
|
-
|
|
148
|
+
await ctx.answerCbQuery('Transaction not found or already processed.', { show_alert: true });
|
|
101
149
|
return;
|
|
102
150
|
}
|
|
103
151
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
bot.sendMessage(chatId, `✅ Transaction processed:\n\n${prettyMsg}`);
|
|
123
|
-
|
|
124
|
-
// Sync with dashboard
|
|
125
|
-
logger.addEntry({ role: 'assistant', content: `✅ Transaction processed:\n\n${prettyMsg}` });
|
|
126
|
-
logger.addEntry({ role: 'tool', name: tx.type === 'swap' ? 'swap_token' : 'transfer_native', content: result });
|
|
127
|
-
|
|
128
|
-
// Background update to LLM
|
|
129
|
-
processUserInput(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system').catch(() => {});
|
|
130
|
-
} catch (err: any) {
|
|
131
|
-
txManager.updateStatus(txId, 'failed', err.message);
|
|
132
|
-
const prettyError = formatTransactionError(tx, err.message);
|
|
133
|
-
bot.sendMessage(chatId, prettyError);
|
|
134
|
-
|
|
135
|
-
// Background update to LLM
|
|
136
|
-
processUserInput(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system').catch(() => {});
|
|
152
|
+
await ctx.answerCbQuery('Processing transaction...');
|
|
153
|
+
await ctx.reply(`⏳ Processing transaction ${txId}...`);
|
|
154
|
+
|
|
155
|
+
// Remove inline keyboard immediately
|
|
156
|
+
await ctx.editMessageReplyMarkup(undefined).catch(() => {});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
let result = '';
|
|
160
|
+
if (tx.type === 'transfer') {
|
|
161
|
+
result = await executeTransfer(tx.chainName as any, tx.details);
|
|
162
|
+
} else if (tx.type === 'swap') {
|
|
163
|
+
result = await executeSwap(tx.chainName as any, tx.details);
|
|
164
|
+
} else if (tx.type === 'bridge') {
|
|
165
|
+
result = await executeBridge(tx.chainName as any, tx.details);
|
|
166
|
+
} else if (tx.type === 'mint') {
|
|
167
|
+
result = await executeMintNft(tx.chainName as any, tx.details);
|
|
168
|
+
} else if (tx.type === 'custom') {
|
|
169
|
+
result = await executeCustomTx(tx.chainName as any, tx.details);
|
|
137
170
|
}
|
|
138
|
-
|
|
139
|
-
txManager.updateStatus(txId, '
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
171
|
+
|
|
172
|
+
txManager.updateStatus(txId, 'executed', result);
|
|
173
|
+
const prettyMsg = formatTransactionSuccess(tx, result);
|
|
174
|
+
await ctx.reply(`✅ Transaction processed:\n\n${prettyMsg}`);
|
|
175
|
+
|
|
176
|
+
logger.addEntry({ role: 'assistant', content: `✅ Transaction processed:\n\n${prettyMsg}` });
|
|
177
|
+
logger.addEntry({ role: 'tool', name: tx.type === 'swap' ? 'swap_token' : 'transfer_native', content: result });
|
|
178
|
+
|
|
179
|
+
processUserInput(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system').catch(() => {});
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
txManager.updateStatus(txId, 'failed', err.message);
|
|
182
|
+
const prettyError = formatTransactionError(tx, err.message);
|
|
183
|
+
await ctx.reply(prettyError);
|
|
184
|
+
processUserInput(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system').catch(() => {});
|
|
143
185
|
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
bot.action(/^reject_(.+)$/, async (ctx) => {
|
|
189
|
+
const txId = ctx.match[1];
|
|
190
|
+
txManager.updateStatus(txId, 'rejected');
|
|
191
|
+
processUserInput(`Transaction ${txId} was REJECTED via Telegram. Acknowledge this briefly.`, 'system').catch(() => {});
|
|
144
192
|
|
|
145
|
-
|
|
146
|
-
|
|
193
|
+
await ctx.answerCbQuery('Transaction cancelled.');
|
|
194
|
+
await ctx.reply(`❌ Transaction cancelled.`);
|
|
195
|
+
await ctx.editMessageReplyMarkup(undefined).catch(() => {});
|
|
147
196
|
});
|
|
148
197
|
|
|
149
|
-
bot.
|
|
150
|
-
console.error('[Telegram]
|
|
198
|
+
bot.catch((err) => {
|
|
199
|
+
console.error('[Telegram] Telegraf error:', err);
|
|
151
200
|
});
|
|
152
201
|
|
|
153
|
-
|
|
202
|
+
bot.launch();
|
|
203
|
+
|
|
204
|
+
if (isPaired) {
|
|
205
|
+
console.log('🤖 Telegram Bot is running and securely listening for your messages...');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Enable graceful stop
|
|
209
|
+
process.once('SIGINT', () => bot.stop('SIGINT'));
|
|
210
|
+
process.once('SIGTERM', () => bot.stop('SIGTERM'));
|
|
211
|
+
|
|
154
212
|
} catch (error) {
|
|
155
213
|
console.error('[Telegram] Failed to initialize bot:', error);
|
|
156
214
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
5
|
import { loadConfig } from '../config/parser';
|
|
5
6
|
import { getPath } from '../config/paths';
|
|
6
7
|
|
|
@@ -10,10 +11,17 @@ export interface MemoryEntry {
|
|
|
10
11
|
name?: string;
|
|
11
12
|
tool_call_id?: string;
|
|
12
13
|
tool_calls?: any[];
|
|
14
|
+
session_id?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ChatSession {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
timestamp: string;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export class Logger {
|
|
16
|
-
private db:
|
|
24
|
+
private db: DatabaseSync;
|
|
17
25
|
|
|
18
26
|
constructor() {
|
|
19
27
|
const config = loadConfig() || {};
|
|
@@ -29,14 +37,23 @@ export class Logger {
|
|
|
29
37
|
fs.mkdirSync(dir, { recursive: true });
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
this.db = new
|
|
40
|
+
this.db = new DatabaseSync(fullPath);
|
|
33
41
|
this.initDb();
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
private initDb() {
|
|
45
|
+
this.db.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
title TEXT NOT NULL,
|
|
49
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
50
|
+
)
|
|
51
|
+
`);
|
|
52
|
+
|
|
37
53
|
this.db.exec(`
|
|
38
54
|
CREATE TABLE IF NOT EXISTS messages (
|
|
39
55
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
session_id TEXT,
|
|
40
57
|
role TEXT NOT NULL,
|
|
41
58
|
content TEXT NOT NULL,
|
|
42
59
|
name TEXT,
|
|
@@ -46,6 +63,13 @@ export class Logger {
|
|
|
46
63
|
)
|
|
47
64
|
`);
|
|
48
65
|
|
|
66
|
+
// Ensure session_id exists for older DBs
|
|
67
|
+
try {
|
|
68
|
+
this.db.prepare('ALTER TABLE messages ADD COLUMN session_id TEXT').run();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// Column probably already exists
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
// Migration logic from old memory.json to SQLite
|
|
50
74
|
const config = loadConfig() || {};
|
|
51
75
|
const oldJsonPath = getPath((config && (config as any).memory && (config as any).memory.path) ? (config as any).memory.path : 'memory.json');
|
|
@@ -62,17 +86,24 @@ export class Logger {
|
|
|
62
86
|
VALUES (@role, @content, @name, @tool_call_id, @tool_calls)
|
|
63
87
|
`);
|
|
64
88
|
|
|
65
|
-
const insertMany =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
const insertMany = (entries: any[]) => {
|
|
90
|
+
this.db.exec('BEGIN TRANSACTION');
|
|
91
|
+
try {
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
insert.run({
|
|
94
|
+
role: entry.role,
|
|
95
|
+
content: entry.content || '',
|
|
96
|
+
name: entry.name || null,
|
|
97
|
+
tool_call_id: entry.tool_call_id || null,
|
|
98
|
+
tool_calls: entry.tool_calls ? JSON.stringify(entry.tool_calls) : null
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
this.db.exec('COMMIT');
|
|
102
|
+
} catch (e) {
|
|
103
|
+
this.db.exec('ROLLBACK');
|
|
104
|
+
throw e;
|
|
74
105
|
}
|
|
75
|
-
}
|
|
106
|
+
};
|
|
76
107
|
|
|
77
108
|
insertMany(oldMemory);
|
|
78
109
|
console.log('[Nyxora Memory] Successfully migrated memory.json to SQLite database (Atomic Storage).');
|
|
@@ -86,8 +117,34 @@ export class Logger {
|
|
|
86
117
|
}
|
|
87
118
|
}
|
|
88
119
|
|
|
89
|
-
public
|
|
90
|
-
const rows = this.db.prepare('SELECT
|
|
120
|
+
public getSessions(): ChatSession[] {
|
|
121
|
+
const rows = this.db.prepare('SELECT id, title, timestamp FROM sessions ORDER BY timestamp DESC').all();
|
|
122
|
+
return rows as unknown as ChatSession[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public createSession(title: string): string {
|
|
126
|
+
const id = crypto.randomUUID();
|
|
127
|
+
this.db.prepare('INSERT INTO sessions (id, title) VALUES (?, ?)').run(id, title);
|
|
128
|
+
return id;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public deleteSession(sessionId: string) {
|
|
132
|
+
this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
|
|
133
|
+
this.db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public renameSession(sessionId: string, newTitle: string) {
|
|
137
|
+
this.db.prepare('UPDATE sessions SET title = ? WHERE id = ?').run(newTitle, sessionId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public getHistory(sessionId?: string): MemoryEntry[] {
|
|
141
|
+
let rows;
|
|
142
|
+
if (sessionId) {
|
|
143
|
+
rows = this.db.prepare('SELECT role, content, name, tool_call_id, tool_calls, session_id FROM messages WHERE session_id = ? ORDER BY id ASC').all(sessionId);
|
|
144
|
+
} else {
|
|
145
|
+
rows = this.db.prepare('SELECT role, content, name, tool_call_id, tool_calls, session_id FROM messages WHERE session_id IS NULL ORDER BY id ASC').all();
|
|
146
|
+
}
|
|
147
|
+
|
|
91
148
|
return rows.map((row: any) => {
|
|
92
149
|
const entry: MemoryEntry = {
|
|
93
150
|
role: row.role,
|
|
@@ -96,17 +153,19 @@ export class Logger {
|
|
|
96
153
|
if (row.name) entry.name = row.name;
|
|
97
154
|
if (row.tool_call_id) entry.tool_call_id = row.tool_call_id;
|
|
98
155
|
if (row.tool_calls) entry.tool_calls = JSON.parse(row.tool_calls);
|
|
156
|
+
if (row.session_id) entry.session_id = row.session_id;
|
|
99
157
|
return entry;
|
|
100
158
|
});
|
|
101
159
|
}
|
|
102
160
|
|
|
103
|
-
public addEntry(entry: MemoryEntry) {
|
|
161
|
+
public addEntry(entry: MemoryEntry, sessionId?: string) {
|
|
104
162
|
const insert = this.db.prepare(`
|
|
105
|
-
INSERT INTO messages (role, content, name, tool_call_id, tool_calls)
|
|
106
|
-
VALUES (@role, @content, @name, @tool_call_id, @tool_calls)
|
|
163
|
+
INSERT INTO messages (session_id, role, content, name, tool_call_id, tool_calls)
|
|
164
|
+
VALUES (@session_id, @role, @content, @name, @tool_call_id, @tool_calls)
|
|
107
165
|
`);
|
|
108
166
|
|
|
109
167
|
insert.run({
|
|
168
|
+
session_id: sessionId || null,
|
|
110
169
|
role: entry.role,
|
|
111
170
|
content: entry.content || '',
|
|
112
171
|
name: entry.name || null,
|
|
@@ -115,7 +174,11 @@ export class Logger {
|
|
|
115
174
|
});
|
|
116
175
|
}
|
|
117
176
|
|
|
118
|
-
public clear() {
|
|
119
|
-
|
|
177
|
+
public clear(sessionId?: string) {
|
|
178
|
+
if (sessionId) {
|
|
179
|
+
this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
|
|
180
|
+
} else {
|
|
181
|
+
this.db.prepare('DELETE FROM messages WHERE session_id IS NULL').run();
|
|
182
|
+
}
|
|
120
183
|
}
|
|
121
184
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import ivm from 'isolated-vm';
|
|
4
4
|
|
|
5
5
|
// Define how an external skill should look like
|
|
6
6
|
export interface ExternalSkill {
|
|
@@ -26,44 +26,58 @@ export class PluginManager {
|
|
|
26
26
|
try {
|
|
27
27
|
const absolutePath = path.resolve(pluginsDir, file);
|
|
28
28
|
const code = fs.readFileSync(absolutePath, 'utf8');
|
|
29
|
-
|
|
30
|
-
// Construct a restricted require function for the sandbox
|
|
31
|
-
const restrictedRequire = (moduleName: string) => {
|
|
32
|
-
const blockedModules = ['fs', 'child_process', 'os', 'net', 'tls', 'cluster', 'worker_threads'];
|
|
33
|
-
if (blockedModules.includes(moduleName)) {
|
|
34
|
-
throw new Error(`Sandboxing error: Access to the '${moduleName}' module is blocked for security reasons.`);
|
|
35
|
-
}
|
|
36
|
-
// Allow fetch and other safe modules by delegating to actual require
|
|
37
|
-
return require(moduleName);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Create the sandbox environment
|
|
41
|
-
const sandbox = {
|
|
42
|
-
require: restrictedRequire,
|
|
43
|
-
console: console,
|
|
44
|
-
module: { exports: {} as any },
|
|
45
|
-
exports: {},
|
|
46
|
-
process: { env: {} }, // Hide actual environment variables
|
|
47
|
-
Buffer: Buffer,
|
|
48
|
-
setTimeout: setTimeout,
|
|
49
|
-
clearTimeout: clearTimeout,
|
|
50
|
-
setInterval: setInterval,
|
|
51
|
-
clearInterval: clearInterval,
|
|
52
|
-
};
|
|
53
29
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
30
|
+
const isolate = new ivm.Isolate({ memoryLimit: 128 });
|
|
31
|
+
const context = isolate.createContextSync();
|
|
32
|
+
const jail = context.global;
|
|
33
|
+
jail.setSync('global', jail.derefInto());
|
|
56
34
|
|
|
57
|
-
//
|
|
58
|
-
|
|
35
|
+
// Inject a safe fetch
|
|
36
|
+
const safeFetch = new ivm.Reference(async (url: string, options: any) => {
|
|
37
|
+
if (url.includes('127.0.0.1') || url.includes('localhost') || url.includes('::1')) {
|
|
38
|
+
throw new Error("SSRF Protection: Access to localhost is blocked.");
|
|
39
|
+
}
|
|
40
|
+
const res = await fetch(url, options);
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
return text; // Only return text to avoid passing complex Response objects
|
|
43
|
+
});
|
|
44
|
+
jail.setSync('fetchText', safeFetch);
|
|
59
45
|
|
|
60
|
-
|
|
46
|
+
// Inject console
|
|
47
|
+
const logCallback = new ivm.Reference((...args: any[]) => console.log('[Plugin]', ...args));
|
|
48
|
+
jail.setSync('log', logCallback);
|
|
49
|
+
|
|
50
|
+
const scriptCode = `
|
|
51
|
+
const console = { log: (...args) => log(...args) };
|
|
52
|
+
const fetch = async (url, options) => {
|
|
53
|
+
const text = await fetchText.apply(undefined, [url, options], { arguments: { copy: true }, result: { promise: true } });
|
|
54
|
+
return { text: async () => text, json: async () => JSON.parse(text) };
|
|
55
|
+
};
|
|
56
|
+
const module = { exports: {} };
|
|
57
|
+
const exports = module.exports;
|
|
58
|
+
${code}
|
|
59
|
+
module.exports;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const script = isolate.compileScriptSync(scriptCode, { filename: file });
|
|
63
|
+
const moduleExportsRef = script.runSync(context);
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const toolDefinition = moduleExportsRef.getSync('toolDefinition');
|
|
66
|
+
const executeRef = moduleExportsRef.getSync('execute');
|
|
67
|
+
|
|
68
|
+
if (toolDefinition && executeRef && typeof executeRef === 'object') {
|
|
69
|
+
const toolName = toolDefinition.function.name;
|
|
70
|
+
|
|
71
|
+
const safeExecute = async (args: any) => {
|
|
72
|
+
const result = await executeRef.apply(undefined, [args], { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.skills.set(toolName, { toolDefinition, execute: safeExecute });
|
|
77
|
+
console.log(`[PluginManager] Loaded sandboxed external skill: ${toolName}`);
|
|
66
78
|
}
|
|
79
|
+
|
|
80
|
+
moduleExportsRef.release();
|
|
67
81
|
} catch (error: any) {
|
|
68
82
|
console.error(`[PluginManager] Failed to load sandboxed plugin ${file}:`, error.message);
|
|
69
83
|
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
2
4
|
|
|
5
|
+
let sessionToken: string | null = null;
|
|
3
6
|
import crypto from 'crypto';
|
|
4
7
|
|
|
5
8
|
export function getSessionToken(): string {
|
|
6
9
|
if (!sessionToken) {
|
|
7
|
-
|
|
10
|
+
const tokenFile = path.join(os.homedir(), '.nyxora', 'auth.token');
|
|
11
|
+
try {
|
|
12
|
+
if (fs.existsSync(tokenFile)) {
|
|
13
|
+
sessionToken = fs.readFileSync(tokenFile, 'utf8').trim();
|
|
14
|
+
} else {
|
|
15
|
+
sessionToken = crypto.randomBytes(32).toString('hex');
|
|
16
|
+
fs.writeFileSync(tokenFile, sessionToken, { mode: 0o600 });
|
|
17
|
+
}
|
|
18
|
+
} catch(e) {
|
|
19
|
+
if (!sessionToken) sessionToken = crypto.randomBytes(32).toString('hex');
|
|
20
|
+
}
|
|
8
21
|
}
|
|
9
22
|
return sessionToken;
|
|
10
23
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPublicClient, http, PublicClient } from 'viem';
|
|
1
|
+
import { createPublicClient, http, fallback, PublicClient, Transport } from 'viem';
|
|
2
2
|
import { mainnet, base, bsc, arbitrum, optimism, sepolia } from 'viem/chains';
|
|
3
3
|
import { loadConfig } from '../config/parser';
|
|
4
4
|
|
|
@@ -18,12 +18,27 @@ export function getPublicClient(chainName: ChainName): PublicClient {
|
|
|
18
18
|
if (!chain) throw new Error(`Unsupported chain: ${chainName}`);
|
|
19
19
|
|
|
20
20
|
const config = loadConfig();
|
|
21
|
-
const
|
|
21
|
+
const customRpcRaw = config.web3?.rpc_urls?.[chainName];
|
|
22
|
+
|
|
23
|
+
const transports: Transport[] = [];
|
|
24
|
+
|
|
25
|
+
if (customRpcRaw) {
|
|
26
|
+
if (Array.isArray(customRpcRaw)) {
|
|
27
|
+
customRpcRaw.forEach(url => {
|
|
28
|
+
if (url.trim()) transports.push(http(url.trim()));
|
|
29
|
+
});
|
|
30
|
+
} else if (typeof customRpcRaw === 'string' && customRpcRaw.trim()) {
|
|
31
|
+
transports.push(http(customRpcRaw.trim()));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Always append the default public RPC as the last resort
|
|
36
|
+
transports.push(http());
|
|
22
37
|
|
|
23
38
|
// @ts-ignore
|
|
24
39
|
return createPublicClient({
|
|
25
40
|
chain,
|
|
26
|
-
transport:
|
|
41
|
+
transport: fallback(transports, { rank: false }),
|
|
27
42
|
});
|
|
28
43
|
}
|
|
29
44
|
|