nyxora 1.6.13 → 1.7.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.
- package/CHANGELOG.md +28 -0
- package/README.md +2 -2
- package/bin/nyxora.mjs +8 -0
- package/package.json +1 -2
- package/packages/core/package.json +1 -1
- package/packages/core/src/agent/limitOrderManager.ts +2 -2
- package/packages/core/src/agent/reasoning.ts +36 -14
- package/packages/core/src/config/parser.ts +99 -9
- package/packages/core/src/gateway/cli.ts +28 -2
- package/packages/core/src/gateway/setup.ts +45 -10
- package/packages/core/src/gateway/telegram.ts +53 -12
- package/packages/core/src/system/skills/searchWeb.ts +187 -21
- package/packages/core/src/utils/formatter.ts +12 -5
- package/packages/core/src/web3/config.ts +7 -1
- package/packages/core/src/web3/skills/bridgeToken.ts +4 -3
- package/packages/core/src/web3/skills/checkAddress.ts +2 -2
- package/packages/core/src/web3/skills/checkPortfolio.ts +55 -39
- package/packages/core/src/web3/skills/checkSecurity.ts +3 -2
- package/packages/core/src/web3/skills/customTx.ts +2 -2
- package/packages/core/src/web3/skills/getBalance.ts +3 -3
- package/packages/core/src/web3/skills/marketAnalysis.ts +2 -2
- package/packages/core/src/web3/skills/mintNft.ts +2 -2
- package/packages/core/src/web3/skills/swapToken.ts +4 -3
- package/packages/core/src/web3/skills/transfer.ts +2 -2
- package/packages/core/src/web3/utils/tokens.ts +8 -0
- package/packages/dashboard/dist/assets/{index-24OeXn-k.css → index-BSk4CLkG.css} +1 -1
- package/packages/dashboard/dist/assets/{index-BuYfTEKE.js → index-Dc3Tu0Te.js} +21 -21
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/package.json +1 -1
- package/packages/mcp-server/package.json +1 -1
- package/packages/policy/package.json +1 -1
- package/packages/signer/package.json +1 -1
- package/launcher.js +0 -48
- package/packages/core/src/agent/reasoning.d.ts.map +0 -1
- package/packages/core/src/config/parser.d.ts.map +0 -1
- package/packages/core/src/gateway/cli.d.ts.map +0 -1
- package/packages/core/src/memory/logger.d.ts.map +0 -1
- package/packages/core/src/utils/safeLogger.js +0 -59
- package/packages/core/src/web3/config.d.ts.map +0 -1
- package/packages/core/src/web3/skills/getBalance.d.ts.map +0 -1
- package/packages/dashboard/public/favicon.svg +0 -10
- package/packages/dashboard/public/icons.svg +0 -24
- package/packages/dashboard/src/App.css +0 -184
- package/packages/dashboard/src/App.tsx +0 -588
- package/packages/dashboard/src/BalanceWidget.tsx +0 -65
- package/packages/dashboard/src/MarketWidget.tsx +0 -73
- package/packages/dashboard/src/NetworkSelector.tsx +0 -64
- package/packages/dashboard/src/NyxoraLogo.tsx +0 -25
- package/packages/dashboard/src/OsSkills.tsx +0 -352
- package/packages/dashboard/src/Overview.tsx +0 -157
- package/packages/dashboard/src/PendingTransactions.tsx +0 -75
- package/packages/dashboard/src/Settings.tsx +0 -338
- package/packages/dashboard/src/Skills.tsx +0 -200
- package/packages/dashboard/src/SwapWidget.tsx +0 -141
- package/packages/dashboard/src/TransactionWidget.tsx +0 -95
- package/packages/dashboard/src/assets/hero.png +0 -0
- package/packages/dashboard/src/assets/react.svg +0 -1
- package/packages/dashboard/src/assets/vite.svg +0 -1
- package/packages/dashboard/src/components/PillSelect.tsx +0 -65
- package/packages/dashboard/src/index.css +0 -807
- package/packages/dashboard/src/main.tsx +0 -10
- package/packages/dashboard/src/overview.css +0 -304
- package/packages/dashboard/src/utils/api.ts +0 -31
- package/packages/mcp-server/tsconfig.tsbuildinfo +0 -1
- package/test-address.ts +0 -11
- package/test-all-chains.ts +0 -19
- package/test-db.ts +0 -3
- package/test-portfolio.ts +0 -14
- package/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.7.1]
|
|
9
|
+
|
|
10
|
+
### CLI Enhancements
|
|
11
|
+
- **Global Version Checker**: Implemented native version checking for the global CLI manager. Users can now run `nyxora -v`, `nyxora --version`, or `nyxora version` to instantly check their installed daemon version without starting the application.
|
|
12
|
+
- **Smart Web Search Setup Wizard**: The `nyxora setup` command now includes an interactive prompt allowing users to choose their preferred Web Search Engine (Tavily, Brave, or Decentralized Mesh) and configure their API keys.
|
|
13
|
+
- **Fast CLI Shortcuts**: Added the `nyxora set-key <provider> <key>` global command shortcut allowing developers to quickly inject or override any API Key (OpenAI, Gemini, OpenRouter, Tavily, Brave) directly into the secure vault without traversing the wizard.
|
|
14
|
+
|
|
15
|
+
### AI Engine Optimizations
|
|
16
|
+
- **Hybrid API Vault (Security)**: API Keys are no longer stored in plain text inside `config.yaml`. Nyxora now encrypts and stores them via `@napi-rs/keyring` utilizing OS-native credential management. For Headless/VPS Linux environments lacking DBUS/Secret Service, it automatically falls back to an isolated `api_vault.key` with strict `0600` permissions.
|
|
17
|
+
- **Root-Level Config Auto-Migration**: Restructured `config.yaml` to move all API keys out of the nested `llm.credentials` into a logical, root-level `credentials` object. Implemented a silent auto-migration routine in `parser.ts` that safely upgrades legacy config files on boot without breaking existing setups.
|
|
18
|
+
- **Web Search Smart Memory Cache**: Embedded a local Memory Cache (`Map`) into the `searchWeb` skill with a 5-minute (300,000ms) TTL. Exact duplicate queries now execute in 0ms and consume 0 API quota, dramatically improving conversation flow.
|
|
19
|
+
- **Deep Research Mode**: The `search_web` tool definition now accepts a dynamic `depth` parameter (1 to 3). If users instruct the AI to conduct comprehensive research, Nyxora will automatically trigger `advanced` API payloads and extract up to 15 top web snippets simultaneously.
|
|
20
|
+
- **Strict Skill Prioritization**: Added CRITICAL RULE 7 to the core NLP System Prompt. The AI is now hard-coded to prioritize native Web3 Skills (e.g. `get_price`, `analyze_market`, `check_security`) for all crypto-related queries, using `search_web` exclusively as a fallback mechanism.
|
|
21
|
+
- **Dual-Engine Web Search (L3 Failover)**: Completely removed the fragile `duck-duck-scrape` dependency. The `search_web` skill is now powered by a robust L3 Auto-Failover architecture. Users can configure enterprise-grade search providers (Tavily or Brave). If the primary provider hits a rate limit (429) or invalid key (401/403), Nyxora seamlessly falls back to the secondary provider, and ultimately to a Decentralized SearXNG Mesh as a final safety net, guaranteeing 100% uptime.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [1.7.0]
|
|
25
|
+
|
|
26
|
+
### Bug Fixes & Optimizations
|
|
27
|
+
- **Time Sync Hallucination**: Fixed a critical issue where the AI hallucinates the current date and time. Nyxora now dynamically injects the host OS's exact `new Date().toLocaleString()` into the system prompt upon every execution.
|
|
28
|
+
- **Aggressive UI Auto-Scroll**: Resolved a severe React rendering bug in the dashboard where the 2-second history polling forced the chat window to aggressively scroll to the bottom. Auto-scroll is now strictly isolated to new message arrivals (`messages.length`).
|
|
29
|
+
- **Orphaned OS Skills**: Re-wired the `search_web` (Internet Search) and `analyze_document` (PDF/DOCX Extractor) skills back into the core reasoning engine. These skills were previously orphaned and inaccessible to the AI despite being active in the dashboard.
|
|
30
|
+
- **Multicall3 Portfolio Engine**: Fully replaced parallel `client.getBalance` and ERC20 fetching with a hyper-efficient `Multicall3` architecture. Balances are now chunked (max 30 tokens per batch) to guarantee zero rate-limits and payload errors on public RPCs.
|
|
31
|
+
- **ChatGPT-Level NLP Persona**: Upgraded the AI's core reasoning engine to natively understand unstructured text, slang, and informal contexts. Rigidly enforced Markdown Table generation for all financial data.
|
|
32
|
+
- **Telegram HTML Parser**: Implemented a custom `formatToTelegramHTML` function. Nyxora now escapes dangerous characters (`<`, `>`, `&`) and automatically wraps AI-generated Markdown tables into `<pre>` monospaced blocks, completely eliminating the "Bad Request" rendering crash on Telegram.
|
|
33
|
+
- **Dynamic Tx Formatter (Tap-to-Copy)**: The post-transaction approval message is now bilingual (auto-detecting English/Indonesian from chat history). Transaction Hashes and wallet addresses are wrapped in `<code>` tags for seamless tap-to-copy UX on mobile devices.
|
|
34
|
+
- **CLI Setup Typography**: Updated outdated CLI prompts that falsely referenced legacy `AES-256-GCM` encryption. The CLI now correctly informs the user that Private Keys are securely locked inside the OS Native Keyring Vault.
|
|
35
|
+
|
|
8
36
|
## [1.6.7]
|
|
9
37
|
|
|
10
38
|
### UI/UX
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](#️-advanced-security-threat-model)
|
|
9
9
|
[](#️-advanced-security-threat-model)
|
|
10
10
|
|
|
11
|
-
Nyxora is a **secure, non-custodial runtime infrastructure for autonomous onchain agents** built with a robust Monorepo architecture (Node.js & React). Designed for autonomous workflows with a premium
|
|
11
|
+
Nyxora is a **secure, non-custodial runtime infrastructure for autonomous onchain agents** built with a robust Monorepo architecture (Node.js & React). Designed for autonomous workflows with a premium Utility-Centric dark-themed UI and strict client-side key isolation.
|
|
12
12
|
|
|
13
13
|
**Nyxora now natively supports the Model Context Protocol (MCP)**. You can transform your external AI agents (like Claude Desktop and Cursor) into secure Web3 actors that execute swaps and fetch balances using Nyxora's secure signer vault. [View the MCP Integration Guide](https://nyxoraAI.github.io/Nyxora/guide/mcp-integration)
|
|
14
14
|
|
|
@@ -25,7 +25,7 @@ It operates under an institutional-grade **Cryptographically Bound Human-in-the-
|
|
|
25
25
|
* **Plugin Sandbox VM**: Execute community-built external skills securely inside an airtight Node.js `vm` chamber with zero access to your file system or terminal processes.
|
|
26
26
|
|
|
27
27
|
### 🌐 Web3 Skills (On-Chain)
|
|
28
|
-
* **
|
|
28
|
+
* **Security Scanner**: Nyxora can scan smart contracts via GoPlus Labs to detect Honeypots, Hidden Taxes, and malicious proxy upgrades before you buy.
|
|
29
29
|
* **Automated Take Profit (TP) & Cut Loss (CL)**: The trader's holy grail. Set natural language rules (e.g., "Sell my PEPE if price drops below $0.001"). Nyxora runs a background cron monitor and executes the swap while you sleep.
|
|
30
30
|
* **Cross-Chain Hybrid Market Scanner**: Real-time asset tracking combining CoinGecko global data with DexScreener on-chain metrics across Ethereum, Base, Solana, BSC, and more.
|
|
31
31
|
* **"Lean Degen" Auto-Whitelist**: Automatically intercepts Contract Addresses (CAs) whenever you check balances or swap tokens, saving them to your localized `user_whitelist.json` for future tracking.
|
package/bin/nyxora.mjs
CHANGED
|
@@ -217,6 +217,14 @@ async function main() {
|
|
|
217
217
|
case 'dashboard': await dashboard(); break;
|
|
218
218
|
case 'clean-logs': await cleanLogs(); break;
|
|
219
219
|
case 'autostart': await autostart(process.argv[3]); break;
|
|
220
|
+
case '-v':
|
|
221
|
+
case '--v':
|
|
222
|
+
case '--version':
|
|
223
|
+
case 'version':
|
|
224
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
225
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
226
|
+
console.log(`Nyxora v${pkg.version}`);
|
|
227
|
+
break;
|
|
220
228
|
default:
|
|
221
229
|
console.log(`
|
|
222
230
|
Nyxora CLI Manager
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nyxora",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"packages/*"
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"concurrently": "^9.2.1",
|
|
27
27
|
"cors": "^2.8.6",
|
|
28
28
|
"dotenv": "^17.4.2",
|
|
29
|
-
"duck-duck-scrape": "^2.2.7",
|
|
30
29
|
"express": "^5.2.1",
|
|
31
30
|
"express-rate-limit": "^7.5.0",
|
|
32
31
|
"helmet": "^8.0.0",
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { loadConfig } from '../config/parser';
|
|
4
4
|
import { getPath } from '../config/paths';
|
|
5
|
-
import { ChainName } from '../web3/config';
|
|
5
|
+
import { ChainName, SUPPORTED_CHAIN_NAMES } from '../web3/config';
|
|
6
6
|
import { resolveToken } from '../web3/utils/tokens';
|
|
7
7
|
import { prepareSwapToken, executeSwap } from '../web3/skills/swapToken';
|
|
8
8
|
import { txManager } from './transactionManager';
|
|
@@ -158,7 +158,7 @@ export const createLimitOrderToolDefinition = {
|
|
|
158
158
|
parameters: {
|
|
159
159
|
type: "object",
|
|
160
160
|
properties: {
|
|
161
|
-
chainName: { type: "string", enum:
|
|
161
|
+
chainName: { type: "string", enum: SUPPORTED_CHAIN_NAMES },
|
|
162
162
|
fromToken: { type: "string", description: "Token to sell" },
|
|
163
163
|
toToken: { type: "string", description: "Token to buy" },
|
|
164
164
|
amountStr: { type: "string", description: "Amount to sell" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { OpenAI } from 'openai';
|
|
4
|
-
import { loadConfig } from '../config/parser';
|
|
4
|
+
import { loadConfig, loadApiKeys } from '../config/parser';
|
|
5
5
|
import { Logger } from '../memory/logger';
|
|
6
6
|
import { Tracker } from '../gateway/tracker';
|
|
7
7
|
import { getBalanceToolDefinition, getBalance } from '../web3/skills/getBalance';
|
|
@@ -21,10 +21,12 @@ import { getMyAddressToolDefinition, getMyAddress } from '../web3/skills/getMyAd
|
|
|
21
21
|
import { createLimitOrderToolDefinition, listLimitOrdersToolDefinition, cancelLimitOrderToolDefinition, limitOrderManager } from './limitOrderManager';
|
|
22
22
|
import { updateProfileToolDefinition, updateProfile } from './updateProfile';
|
|
23
23
|
import { updateSecurityPolicyToolDefinition, updateSecurityPolicy } from '../system/skills/updateSecurityPolicy';
|
|
24
|
+
import { analyzeDocumentToolDefinition, analyzeDocument } from '../system/skills/analyzeDocument';
|
|
24
25
|
import { readLocalFileToolDefinition, readLocalFile } from '../system/skills/readFile';
|
|
25
26
|
import { writeLocalFileToolDefinition, writeLocalFile } from '../system/skills/writeFile';
|
|
26
27
|
import { runTerminalCommandToolDefinition, runTerminalCommand } from '../system/skills/executeShell';
|
|
27
28
|
import { browseWebsiteToolDefinition, browseWebsite } from '../system/skills/browseWeb';
|
|
29
|
+
import { searchWebToolDefinition, searchWeb } from '../system/skills/searchWeb';
|
|
28
30
|
import { installExternalSkillToolDefinition, installExternalSkill } from '../system/skills/installSkill';
|
|
29
31
|
import {
|
|
30
32
|
readGmailInbox,
|
|
@@ -46,8 +48,9 @@ export const logger = new Logger();
|
|
|
46
48
|
|
|
47
49
|
let currentKeyIndex = 0;
|
|
48
50
|
|
|
49
|
-
function getOpenAI(): OpenAI {
|
|
51
|
+
async function getOpenAI(): Promise<OpenAI> {
|
|
50
52
|
const config = loadConfig();
|
|
53
|
+
const vaultKeys = await loadApiKeys();
|
|
51
54
|
|
|
52
55
|
if (config.llm.provider === 'ollama') {
|
|
53
56
|
return new OpenAI({
|
|
@@ -78,16 +81,16 @@ function getOpenAI(): OpenAI {
|
|
|
78
81
|
// Fallbacks if no valid keys found in config.llm.api_keys
|
|
79
82
|
if (!apiKey) {
|
|
80
83
|
if (config.llm.provider === 'gemini') {
|
|
81
|
-
apiKey = config.
|
|
84
|
+
apiKey = vaultKeys.gemini_key || config.credentials?.gemini_key || '';
|
|
82
85
|
} else if (config.llm.provider === 'openrouter') {
|
|
83
|
-
apiKey = config.
|
|
86
|
+
apiKey = vaultKeys.openrouter_key || config.credentials?.openrouter_key || '';
|
|
84
87
|
} else {
|
|
85
|
-
apiKey = config.
|
|
88
|
+
apiKey = vaultKeys.openai_key || config.credentials?.openai_key || '';
|
|
86
89
|
}
|
|
87
90
|
if (!apiKey) {
|
|
88
|
-
throw new Error(`No API Key found for ${config.llm.provider}
|
|
91
|
+
throw new Error(`No API Key found for ${config.llm.provider}. Please run 'nyxora setup' to configure it.`);
|
|
89
92
|
}
|
|
90
|
-
console.log(`[LLM] Using
|
|
93
|
+
console.log(`[LLM] Using API Key from secure vault`);
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
if (config.llm.provider === 'gemini') {
|
|
@@ -115,7 +118,7 @@ async function executeWithRetry(
|
|
|
115
118
|
|
|
116
119
|
while (retries <= maxRetries) {
|
|
117
120
|
try {
|
|
118
|
-
const client = getOpenAI();
|
|
121
|
+
const client = await getOpenAI();
|
|
119
122
|
return await requestBuilder(client);
|
|
120
123
|
} catch (error: any) {
|
|
121
124
|
const status = error?.status || error?.response?.status;
|
|
@@ -151,12 +154,21 @@ async function executeWithRetry(
|
|
|
151
154
|
|
|
152
155
|
function getSystemPrompt() {
|
|
153
156
|
const config = loadConfig();
|
|
157
|
+
const currentDateTime = new Date().toLocaleString('en-US', { timeZoneName: 'short' });
|
|
154
158
|
let basePrompt = `You are an autonomous Web3 agent operating on EVM chains.
|
|
155
|
-
You are equipped with a native wallet.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
CRITICAL RULE:
|
|
159
|
-
CRITICAL RULE:
|
|
159
|
+
You are equipped with a native wallet.
|
|
160
|
+
The current real-world date and time is: ${currentDateTime}. Use this for any time-related questions.
|
|
161
|
+
|
|
162
|
+
CRITICAL RULE 1: ADVANCED NLP & PERSONA. You must act as a highly intelligent, adaptive, and intuitive assistant (similar to ChatGPT or Gemini). You must seamlessly understand the user's language structure, including slang, shorthand, informal context, and mixed languages. However, you must maintain a professional and highly accurate Web3 operational standard.
|
|
163
|
+
CRITICAL RULE 2: LANGUAGE MATCHING. You must always reply in the exact same language that the user uses to talk to you. If the user speaks Indonesian, reply in Indonesian. If they speak English, reply in English.
|
|
164
|
+
CRITICAL RULE 3: FORMATTING & CONCISENESS.
|
|
165
|
+
- Your responses MUST be concise and to the point. Do not add unnecessary fluff or overly long explanations unless explicitly asked.
|
|
166
|
+
- When displaying numbers or monetary values, separate thousands with commas (e.g., $1,000,000) for readability.
|
|
167
|
+
- When displaying a list of assets, tokens, portfolio, or transaction history, YOU MUST USE MARKDOWN TABLES. Do not use bullet points for financial data.
|
|
168
|
+
CRITICAL RULE 4: When the user asks to check "my balance", "saldo saya", or anything about their own wallet generally, ALWAYS use the check_portfolio tool to show all assets on the chain that have a USD value greater than 0. LEAVE THE ADDRESS PARAMETER EMPTY. Do NOT use get_balance unless the user explicitly asks for the balance of ONE specific token.
|
|
169
|
+
CRITICAL RULE 5: If the user doesn't specify a chain, default to: ${config.agent.default_chain}. If the user mentions a specific chain (e.g., "on BNB", "di Base"), you MUST override the default and execute the tool on that specific chain.
|
|
170
|
+
CRITICAL RULE 6: If you use the default chain because the user forgot to specify one, you MUST politely confirm which chain you checked in your response (e.g., "I checked your balance on the ${config.agent.default_chain} network..."). Do not issue scary warnings.
|
|
171
|
+
CRITICAL RULE 7: TOOL PRIORITIZATION. When the user asks about crypto prices, market analysis, token security, or blockchain data, YOU MUST prioritize using the dedicated Web3 skills (e.g., get_price, analyze_market, check_security) FIRST. Only if those tools fail or cannot provide the requested information, you may fallback to using search_web.`;
|
|
160
172
|
|
|
161
173
|
// Read IDENTITY.md for core AI persona
|
|
162
174
|
try {
|
|
@@ -246,10 +258,12 @@ export async function processUserInput(input: string, role: 'user' | 'system' =
|
|
|
246
258
|
cancelLimitOrderToolDefinition as any,
|
|
247
259
|
updateProfileToolDefinition as any,
|
|
248
260
|
updateSecurityPolicyToolDefinition as any,
|
|
261
|
+
analyzeDocumentToolDefinition as any,
|
|
249
262
|
readLocalFileToolDefinition as any,
|
|
250
263
|
writeLocalFileToolDefinition as any,
|
|
251
264
|
runTerminalCommandToolDefinition as any,
|
|
252
265
|
browseWebsiteToolDefinition as any,
|
|
266
|
+
searchWebToolDefinition as any,
|
|
253
267
|
installExternalSkillToolDefinition as any,
|
|
254
268
|
readGmailInboxToolDefinition as any,
|
|
255
269
|
listCalendarEventsToolDefinition as any,
|
|
@@ -382,7 +396,11 @@ export async function processUserInput(input: string, role: 'user' | 'system' =
|
|
|
382
396
|
break;
|
|
383
397
|
}
|
|
384
398
|
case 'update_security_policy': {
|
|
385
|
-
result = updateSecurityPolicy(args.
|
|
399
|
+
result = await updateSecurityPolicy(args.policy, args.action || 'add');
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case 'analyze_document': {
|
|
403
|
+
result = await analyzeDocument(args.filePath);
|
|
386
404
|
break;
|
|
387
405
|
}
|
|
388
406
|
case 'read_local_file': {
|
|
@@ -409,6 +427,10 @@ export async function processUserInput(input: string, role: 'user' | 'system' =
|
|
|
409
427
|
result = await browseWebsite(args.url);
|
|
410
428
|
break;
|
|
411
429
|
}
|
|
430
|
+
case 'search_web': {
|
|
431
|
+
result = await searchWeb(args.query, args.depth);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
412
434
|
case 'install_external_skill': {
|
|
413
435
|
result = await installExternalSkill(args.url);
|
|
414
436
|
break;
|
|
@@ -3,6 +3,39 @@ import yaml from 'yaml';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { getPath } from './paths';
|
|
5
5
|
|
|
6
|
+
export async function loadApiKeys(): Promise<Record<string, string>> {
|
|
7
|
+
const vaultPath = getPath('api_vault.key');
|
|
8
|
+
try {
|
|
9
|
+
const { Entry } = require('@napi-rs/keyring');
|
|
10
|
+
const entry = new Entry('nyxora', 'api_keys');
|
|
11
|
+
const data = await entry.getPassword();
|
|
12
|
+
if (data) return JSON.parse(data);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
if (fs.existsSync(vaultPath)) {
|
|
15
|
+
try {
|
|
16
|
+
const file = fs.readFileSync(vaultPath, 'utf8');
|
|
17
|
+
return JSON.parse(file);
|
|
18
|
+
} catch (err) {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function saveApiKeys(newKeys: Record<string, string>): Promise<void> {
|
|
25
|
+
const vaultPath = getPath('api_vault.key');
|
|
26
|
+
const currentKeys = await loadApiKeys();
|
|
27
|
+
const mergedKeys = { ...currentKeys, ...newKeys };
|
|
28
|
+
const dataString = JSON.stringify(mergedKeys);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const { Entry } = require('@napi-rs/keyring');
|
|
32
|
+
const entry = new Entry('nyxora', 'api_keys');
|
|
33
|
+
await entry.setPassword(dataString);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
fs.writeFileSync(vaultPath, dataString, { mode: 0o600 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
6
39
|
export interface NyxoraConfig {
|
|
7
40
|
agent: {
|
|
8
41
|
name: string;
|
|
@@ -13,11 +46,19 @@ export interface NyxoraConfig {
|
|
|
13
46
|
model: string;
|
|
14
47
|
temperature: number;
|
|
15
48
|
api_keys?: string[];
|
|
16
|
-
credentials?:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
49
|
+
credentials?: any; // Deprecated, kept for parsing during migration
|
|
50
|
+
};
|
|
51
|
+
web_search?: {
|
|
52
|
+
provider: 'tavily' | 'brave' | 'mesh';
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
};
|
|
55
|
+
credentials?: {
|
|
56
|
+
openai_key?: string;
|
|
57
|
+
gemini_key?: string;
|
|
58
|
+
openrouter_key?: string;
|
|
59
|
+
tavily_key?: string;
|
|
60
|
+
brave_key?: string;
|
|
61
|
+
[key: string]: string | undefined;
|
|
21
62
|
};
|
|
22
63
|
memory: {
|
|
23
64
|
type: string;
|
|
@@ -52,6 +93,47 @@ export function loadConfig(): NyxoraConfig {
|
|
|
52
93
|
const file = fs.readFileSync(configPath, 'utf8');
|
|
53
94
|
const parsed = yaml.parse(file) as Partial<NyxoraConfig>;
|
|
54
95
|
|
|
96
|
+
// Auto-migration logic: move llm.credentials to root credentials
|
|
97
|
+
let needsSave = false;
|
|
98
|
+
if (parsed.llm && (parsed.llm as any).credentials) {
|
|
99
|
+
if (!parsed.credentials) {
|
|
100
|
+
parsed.credentials = {};
|
|
101
|
+
}
|
|
102
|
+
const oldCreds = (parsed.llm as any).credentials;
|
|
103
|
+
Object.keys(oldCreds).forEach(key => {
|
|
104
|
+
if (oldCreds[key] && !parsed.credentials![key]) {
|
|
105
|
+
parsed.credentials![key] = oldCreds[key];
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
delete (parsed.llm as any).credentials;
|
|
109
|
+
needsSave = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (needsSave) {
|
|
113
|
+
try {
|
|
114
|
+
const yamlStr = yaml.stringify(parsed);
|
|
115
|
+
fs.writeFileSync(configPath, yamlStr, 'utf8');
|
|
116
|
+
console.log('[Config] Auto-migrated llm.credentials to root credentials.');
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error('[Config] Failed to auto-migrate config file', e);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Auto-migrate from config.yaml to Keyring/Vault
|
|
123
|
+
if (parsed.credentials && Object.keys(parsed.credentials).length > 0) {
|
|
124
|
+
const credsToMigrate = { ...parsed.credentials };
|
|
125
|
+
saveApiKeys(credsToMigrate).then(() => {
|
|
126
|
+
console.log('[Config] Auto-migrated API keys to secure vault.');
|
|
127
|
+
delete parsed.credentials;
|
|
128
|
+
try {
|
|
129
|
+
const yamlStr = yaml.stringify(parsed);
|
|
130
|
+
fs.writeFileSync(configPath, yamlStr, 'utf8');
|
|
131
|
+
} catch (e) {}
|
|
132
|
+
}).catch(e => {
|
|
133
|
+
console.error('[Config] Failed to migrate API keys to secure vault', e);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
55
137
|
// Merge with defaults
|
|
56
138
|
return {
|
|
57
139
|
agent: parsed.agent || { name: 'Nyxora-Default', default_chain: 'base' },
|
|
@@ -59,9 +141,13 @@ export function loadConfig(): NyxoraConfig {
|
|
|
59
141
|
provider: 'openai',
|
|
60
142
|
model: 'gpt-4o-mini',
|
|
61
143
|
temperature: 0.2,
|
|
62
|
-
api_keys: []
|
|
63
|
-
credentials: {}
|
|
144
|
+
api_keys: []
|
|
64
145
|
},
|
|
146
|
+
web_search: parsed.web_search || {
|
|
147
|
+
provider: 'mesh',
|
|
148
|
+
enabled: true
|
|
149
|
+
},
|
|
150
|
+
credentials: parsed.credentials || {},
|
|
65
151
|
memory: parsed.memory || { type: 'file', path: './memory.json' },
|
|
66
152
|
web3: parsed.web3 || { rpc_urls: {} },
|
|
67
153
|
integrations: parsed.integrations || {
|
|
@@ -80,9 +166,13 @@ export function loadConfig(): NyxoraConfig {
|
|
|
80
166
|
provider: 'openai',
|
|
81
167
|
model: 'gpt-4o-mini',
|
|
82
168
|
temperature: 0.2,
|
|
83
|
-
api_keys: []
|
|
84
|
-
|
|
169
|
+
api_keys: []
|
|
170
|
+
},
|
|
171
|
+
web_search: {
|
|
172
|
+
provider: 'mesh',
|
|
173
|
+
enabled: true
|
|
85
174
|
},
|
|
175
|
+
credentials: {},
|
|
86
176
|
memory: { type: 'file', path: './memory.json' },
|
|
87
177
|
web3: { rpc_urls: {} },
|
|
88
178
|
integrations: {
|
|
@@ -13,6 +13,7 @@ import { runSetupWizard } from './setup';
|
|
|
13
13
|
import { password, isCancel } from '@clack/prompts';
|
|
14
14
|
import { getSessionToken } from '../utils/state';
|
|
15
15
|
import pc from 'picocolors';
|
|
16
|
+
import { saveApiKeys } from '../config/parser';
|
|
16
17
|
|
|
17
18
|
async function main() {
|
|
18
19
|
// 1. Determine configuration directory
|
|
@@ -30,6 +31,33 @@ console.log(`================================`);
|
|
|
30
31
|
process.exit(0);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// Check for set-key shortcut
|
|
35
|
+
if (process.argv.includes('set-key')) {
|
|
36
|
+
const setKeyIndex = process.argv.indexOf('set-key');
|
|
37
|
+
const provider = process.argv[setKeyIndex + 1];
|
|
38
|
+
const key = process.argv[setKeyIndex + 2];
|
|
39
|
+
|
|
40
|
+
if (!provider || !key) {
|
|
41
|
+
console.error(pc.red('Usage: nyxora set-key <provider> <api_key>'));
|
|
42
|
+
console.error(pc.gray('Example: nyxora set-key tavily tvly-xxx'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const keyMap: Record<string, string> = {
|
|
47
|
+
'openai': 'openai_key',
|
|
48
|
+
'gemini': 'gemini_key',
|
|
49
|
+
'openrouter': 'openrouter_key',
|
|
50
|
+
'tavily': 'tavily_key',
|
|
51
|
+
'brave': 'brave_key'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const mappedKey = keyMap[provider.toLowerCase()] || `${provider.toLowerCase()}_key`;
|
|
55
|
+
|
|
56
|
+
await saveApiKeys({ [mappedKey]: key });
|
|
57
|
+
console.log(pc.green(`✅ API Key for ${provider} saved securely to vault.`));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
33
61
|
// 2. Setup boilerplate files if in global mode and they don't exist
|
|
34
62
|
let isFirstBoot = false;
|
|
35
63
|
if (isGlobalMode) {
|
|
@@ -65,8 +93,6 @@ console.log(`================================`);
|
|
|
65
93
|
// 4. Start the Express API Server (which also serves the static dashboard and Telegram bot)
|
|
66
94
|
startServer();
|
|
67
95
|
const token = getSessionToken(); // Initialize token file
|
|
68
|
-
console.log(`🌐 Nyxora API Server running on port ${process.env.PORT || 3000}`);
|
|
69
|
-
|
|
70
96
|
setTimeout(() => {
|
|
71
97
|
console.log(pc.cyan(`\n✨ Dashboard URL: http://localhost:3000/?token=${token}`));
|
|
72
98
|
console.log(pc.gray(` (Developers: Vite hot-reload available at http://localhost:5173/?token=${token})\n`));
|
|
@@ -3,7 +3,7 @@ import pc from 'picocolors';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { getAppDir } from '../config/paths';
|
|
6
|
-
import { loadConfig, saveConfig } from '../config/parser';
|
|
6
|
+
import { loadConfig, saveConfig, saveApiKeys } from '../config/parser';
|
|
7
7
|
import crypto from 'crypto';
|
|
8
8
|
|
|
9
9
|
function encryptKey(privateKey: string, password: string) {
|
|
@@ -161,6 +161,25 @@ Provider: ${config.llm.provider}`;
|
|
|
161
161
|
if (isCancel(apiKey)) return process.exit(0);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// 3.5. Smart Web Search Setup
|
|
165
|
+
const searchProvider = await select({
|
|
166
|
+
message: 'Enable Smart Web Search for Nyxora AI?',
|
|
167
|
+
options: [
|
|
168
|
+
{ value: 'skip', label: 'Skip (Use basic decentralized mesh)' },
|
|
169
|
+
{ value: 'tavily', label: 'Tavily Search (Built for AI - 1000 free/mo)' },
|
|
170
|
+
{ value: 'brave', label: 'Brave Search (Privacy focused - 2000 free/mo)' },
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
if (isCancel(searchProvider)) return process.exit(0);
|
|
174
|
+
|
|
175
|
+
let searchApiKey = '';
|
|
176
|
+
if (searchProvider !== 'skip') {
|
|
177
|
+
searchApiKey = (await password({
|
|
178
|
+
message: `Enter API Key for ${searchProvider} (Get it free at ${searchProvider === 'tavily' ? 'tavily.com' : 'search.brave.com'}):`,
|
|
179
|
+
})) as string;
|
|
180
|
+
if (isCancel(searchApiKey)) return process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
164
183
|
// 4. Default Chain
|
|
165
184
|
const defaultChain = await select({
|
|
166
185
|
message: 'Select Default Chain:',
|
|
@@ -169,8 +188,9 @@ Provider: ${config.llm.provider}`;
|
|
|
169
188
|
{ value: 'ethereum', label: 'Ethereum Mainnet' },
|
|
170
189
|
{ value: 'bsc', label: 'BSC' },
|
|
171
190
|
{ value: 'base', label: 'Base' },
|
|
172
|
-
{ value: '
|
|
173
|
-
{ value: '
|
|
191
|
+
{ value: 'arbitrum', label: 'Arbitrum One' },
|
|
192
|
+
{ value: 'optimism', label: 'OP Mainnet' },
|
|
193
|
+
{ value: 'polygon', label: 'Polygon (Matic)' },
|
|
174
194
|
{ value: 'sepolia', label: 'Sepolia (Testnet)' },
|
|
175
195
|
],
|
|
176
196
|
});
|
|
@@ -233,8 +253,8 @@ Provider: ${config.llm.provider}`;
|
|
|
233
253
|
message: 'Web3 Wallet Setup:',
|
|
234
254
|
options: [
|
|
235
255
|
{ value: 'skip', label: 'Skip for now (No Web3 execution)' },
|
|
236
|
-
{ value: 'generate', label: 'Auto-Generate New Wallet
|
|
237
|
-
{ value: 'manual', label: 'Input
|
|
256
|
+
{ value: 'generate', label: 'Auto-Generate New Wallet' },
|
|
257
|
+
{ value: 'manual', label: 'Manual Input (Existing Private Key)' },
|
|
238
258
|
],
|
|
239
259
|
});
|
|
240
260
|
if (isCancel(walletSetupType)) return process.exit(0);
|
|
@@ -242,7 +262,7 @@ Provider: ${config.llm.provider}`;
|
|
|
242
262
|
let privateKey = '';
|
|
243
263
|
if (walletSetupType === 'manual') {
|
|
244
264
|
privateKey = (await password({
|
|
245
|
-
message: 'Enter Wallet Private Key (0x...)\n (Will be
|
|
265
|
+
message: 'Enter Wallet Private Key (0x...)\n (Will be securely locked in your OS Native Keyring Vault):',
|
|
246
266
|
})) as string;
|
|
247
267
|
if (isCancel(privateKey)) return process.exit(0);
|
|
248
268
|
} else if (walletSetupType === 'generate') {
|
|
@@ -260,11 +280,26 @@ Provider: ${config.llm.provider}`;
|
|
|
260
280
|
config.llm.model = model as string;
|
|
261
281
|
config.agent.default_chain = defaultChain as string;
|
|
262
282
|
|
|
263
|
-
|
|
283
|
+
const newApiKeys: Record<string, string> = {};
|
|
264
284
|
if (apiKey) {
|
|
265
|
-
if (provider === 'openai')
|
|
266
|
-
if (provider === 'gemini')
|
|
267
|
-
if (provider === 'openrouter')
|
|
285
|
+
if (provider === 'openai') newApiKeys.openai_key = apiKey;
|
|
286
|
+
if (provider === 'gemini') newApiKeys.gemini_key = apiKey;
|
|
287
|
+
if (provider === 'openrouter') newApiKeys.openrouter_key = apiKey;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!config.web_search) config.web_search = { provider: 'mesh', enabled: true };
|
|
291
|
+
if (searchProvider !== 'skip') {
|
|
292
|
+
config.web_search.provider = searchProvider as any;
|
|
293
|
+
if (searchApiKey) {
|
|
294
|
+
if (searchProvider === 'tavily') newApiKeys.tavily_key = searchApiKey;
|
|
295
|
+
if (searchProvider === 'brave') newApiKeys.brave_key = searchApiKey;
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
config.web_search.provider = 'mesh';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (Object.keys(newApiKeys).length > 0) {
|
|
302
|
+
await saveApiKeys(newApiKeys);
|
|
268
303
|
}
|
|
269
304
|
|
|
270
305
|
if (!config.integrations) config.integrations = {};
|
|
@@ -10,6 +10,31 @@ import { executeCustomTx } from '../web3/skills/customTx';
|
|
|
10
10
|
import { formatTransactionSuccess, formatTransactionError } from '../utils/formatter';
|
|
11
11
|
import pc from 'picocolors';
|
|
12
12
|
|
|
13
|
+
export function formatToTelegramHTML(text: string): string {
|
|
14
|
+
if (!text) return "";
|
|
15
|
+
let html = text
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>');
|
|
19
|
+
|
|
20
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
|
21
|
+
// Match italic * avoiding bullet points at the start of a line
|
|
22
|
+
html = html.replace(/(?<!^|\n)\*(?!\s)(.*?)(?<!\s)\*/g, '<i>$1</i>');
|
|
23
|
+
html = html.replace(/_(.*?)_/g, '<i>$1</i>');
|
|
24
|
+
|
|
25
|
+
// Convert code blocks and inline code
|
|
26
|
+
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
27
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
28
|
+
|
|
29
|
+
// Transform Markdown Tables to <pre> monospaced blocks so they don't break on mobile
|
|
30
|
+
const tableRegex = /(?:\|.*\|(?:\n|$))+/g;
|
|
31
|
+
html = html.replace(tableRegex, (match) => {
|
|
32
|
+
return `<pre>${match.trim()}</pre>\n`;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return html;
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
export function startTelegramBot() {
|
|
14
39
|
const config = loadConfig();
|
|
15
40
|
const token = config.integrations?.telegram?.bot_token;
|
|
@@ -104,10 +129,10 @@ export function startTelegramBot() {
|
|
|
104
129
|
const onProgress = async (progressText: string) => {
|
|
105
130
|
try {
|
|
106
131
|
if (!progressMsgId) {
|
|
107
|
-
const sent = await ctx.reply(progressText, { parse_mode: '
|
|
132
|
+
const sent = await ctx.reply(`<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
|
|
108
133
|
progressMsgId = sent.message_id;
|
|
109
134
|
} else {
|
|
110
|
-
await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, progressText, { parse_mode: '
|
|
135
|
+
await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, `<i>${progressText.replace(/_/g, '')}</i>`, { parse_mode: 'HTML' });
|
|
111
136
|
}
|
|
112
137
|
} catch (e) {}
|
|
113
138
|
};
|
|
@@ -122,17 +147,20 @@ export function startTelegramBot() {
|
|
|
122
147
|
if (pendingTxs.length > 0) {
|
|
123
148
|
const latestTx = pendingTxs[pendingTxs.length - 1];
|
|
124
149
|
if (Date.now() - latestTx.createdAt < 120000) {
|
|
125
|
-
await ctx.reply(response,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
await ctx.reply(formatToTelegramHTML(response), {
|
|
151
|
+
parse_mode: 'HTML',
|
|
152
|
+
...Markup.inlineKeyboard([
|
|
153
|
+
[
|
|
154
|
+
Markup.button.callback('✅ Approve', `approve_${latestTx.id}`),
|
|
155
|
+
Markup.button.callback('❌ Reject', `reject_${latestTx.id}`)
|
|
156
|
+
]
|
|
157
|
+
])
|
|
158
|
+
});
|
|
131
159
|
return;
|
|
132
160
|
}
|
|
133
161
|
}
|
|
134
162
|
|
|
135
|
-
await ctx.reply(response);
|
|
163
|
+
await ctx.reply(formatToTelegramHTML(response), { parse_mode: 'HTML' });
|
|
136
164
|
} catch (error: any) {
|
|
137
165
|
console.error('[Telegram] Error processing message:', error);
|
|
138
166
|
await ctx.reply('❌ Sorry, I encountered an error while processing your message.');
|
|
@@ -170,8 +198,19 @@ export function startTelegramBot() {
|
|
|
170
198
|
}
|
|
171
199
|
|
|
172
200
|
txManager.updateStatus(txId, 'executed', result);
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
|
|
202
|
+
// Pass session history to formatTransactionSuccess to detect language
|
|
203
|
+
const sessionId = ctx.chat?.id.toString() || 'default';
|
|
204
|
+
const history = logger.getHistory(sessionId);
|
|
205
|
+
let isIndonesian = false;
|
|
206
|
+
if (history.length > 0) {
|
|
207
|
+
const lastMsg = history[history.length - 1].content.toLowerCase();
|
|
208
|
+
const idWords = ['saya', 'kamu', 'aku', 'apa', 'bagaimana', 'kenapa', 'bisa', 'tolong', 'ke', 'di', 'dari', 'yang', 'ini', 'itu', 'buat', 'cek', 'saldo'];
|
|
209
|
+
if (idWords.some(w => lastMsg.includes(w))) isIndonesian = true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const prettyMsg = formatTransactionSuccess(tx, result, isIndonesian);
|
|
213
|
+
await ctx.reply(formatToTelegramHTML(`✅ **Transaction processed:**\n\n${prettyMsg}`), { parse_mode: 'HTML' });
|
|
175
214
|
|
|
176
215
|
logger.addEntry({ role: 'assistant', content: `✅ Transaction processed:\n\n${prettyMsg}` });
|
|
177
216
|
logger.addEntry({ role: 'tool', name: tx.type === 'swap' ? 'swap_token' : 'transfer_native', content: result });
|
|
@@ -199,7 +238,9 @@ export function startTelegramBot() {
|
|
|
199
238
|
console.error('[Telegram] Telegraf error:', err);
|
|
200
239
|
});
|
|
201
240
|
|
|
202
|
-
bot.launch()
|
|
241
|
+
bot.launch().catch(err => {
|
|
242
|
+
console.error('[Telegram] Connection failed (likely blocked by ISP or timeout):', err.message);
|
|
243
|
+
});
|
|
203
244
|
|
|
204
245
|
if (isPaired) {
|
|
205
246
|
console.log('🤖 Telegram Bot is running and securely listening for your messages...');
|