nyxora 26.7.9 → 26.7.10

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 (77) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -8
  3. package/SUPPORT.md +18 -0
  4. package/dist/packages/core/src/agent/osAgent.js +64 -8
  5. package/dist/packages/core/src/agent/promptBuilder.js +6 -10
  6. package/dist/packages/core/src/agent/reasoning.js +63 -11
  7. package/dist/packages/core/src/agent/web3Agent.js +64 -8
  8. package/dist/packages/core/src/channels/telegram.js +12 -32
  9. package/dist/packages/core/src/gateway/setup.js +2 -0
  10. package/dist/packages/core/src/utils/contextSummarizer.js +21 -44
  11. package/dist/packages/core/src/utils/historySanitizer.js +9 -2
  12. package/dist/packages/core/src/web3/aggregator/providers/ArbitrumBridgeProvider.js +58 -12
  13. package/dist/packages/core/src/web3/aggregator/providers/KyberSwapProvider.js +1 -1
  14. package/dist/packages/core/src/web3/aggregator/providers/LifiProvider.js +1 -1
  15. package/dist/packages/core/src/web3/aggregator/providers/OneInchProvider.js +1 -1
  16. package/dist/packages/core/src/web3/aggregator/providers/OpenOceanProvider.js +1 -1
  17. package/dist/packages/core/src/web3/aggregator/providers/RelayProvider.js +1 -1
  18. package/dist/packages/core/src/web3/aggregator/providers/ZeroXProvider.js +1 -1
  19. package/dist/packages/core/src/web3/skills/bridgeToken.js +1 -1
  20. package/dist/packages/core/src/web3/skills/checkSecurity.js +2 -0
  21. package/dist/packages/core/src/web3/utils/chains.js +4 -0
  22. package/dist/packages/core/src/web3/utils/rpcEngine.js +10 -0
  23. package/dist/packages/core/src/web3/utils/tokens.js +6 -0
  24. package/dist/packages/policy/src/engine.js +260 -0
  25. package/dist/packages/policy/src/index.js +17 -0
  26. package/dist/packages/policy/src/server.js +9 -248
  27. package/dist/packages/signer/src/NyxoraSigner.js +22 -1
  28. package/package.json +7 -1
  29. package/packages/core/package.json +2 -2
  30. package/packages/core/src/agent/osAgent.ts +67 -7
  31. package/packages/core/src/agent/promptBuilder.ts +6 -10
  32. package/packages/core/src/agent/reasoning.ts +62 -11
  33. package/packages/core/src/agent/web3Agent.ts +66 -7
  34. package/packages/core/src/channels/telegram.ts +10 -36
  35. package/packages/core/src/config/parser.d.ts +83 -0
  36. package/packages/core/src/config/parser.js +290 -0
  37. package/packages/core/src/config/paths.d.ts +2 -0
  38. package/packages/core/src/config/paths.js +68 -0
  39. package/packages/core/src/gateway/setup.ts +2 -0
  40. package/packages/core/src/utils/contextSummarizer.ts +19 -52
  41. package/packages/core/src/utils/historySanitizer.ts +9 -2
  42. package/packages/core/src/utils/safeLogger.d.ts +4 -0
  43. package/packages/core/src/utils/safeLogger.js +59 -0
  44. package/packages/core/src/web3/aggregator/providers/ArbitrumBridgeProvider.ts +60 -13
  45. package/packages/core/src/web3/aggregator/providers/KyberSwapProvider.ts +1 -1
  46. package/packages/core/src/web3/aggregator/providers/LifiProvider.ts +1 -1
  47. package/packages/core/src/web3/aggregator/providers/OneInchProvider.ts +1 -1
  48. package/packages/core/src/web3/aggregator/providers/OpenOceanProvider.ts +1 -1
  49. package/packages/core/src/web3/aggregator/providers/RelayProvider.ts +1 -1
  50. package/packages/core/src/web3/aggregator/providers/ZeroXProvider.ts +1 -1
  51. package/packages/core/src/web3/config.d.ts +3 -0
  52. package/packages/core/src/web3/config.js +20 -0
  53. package/packages/core/src/web3/skills/bridgeToken.ts +1 -1
  54. package/packages/core/src/web3/skills/checkRegistryStatus.d.ts +21 -0
  55. package/packages/core/src/web3/skills/checkRegistryStatus.js +92 -0
  56. package/packages/core/src/web3/skills/checkSecurity.ts +2 -0
  57. package/packages/core/src/web3/utils/chains.d.ts +1596 -0
  58. package/packages/core/src/web3/utils/chains.js +36 -0
  59. package/packages/core/src/web3/utils/chains.ts +4 -1
  60. package/packages/core/src/web3/utils/rpcEngine.d.ts +4 -0
  61. package/packages/core/src/web3/utils/rpcEngine.js +138 -0
  62. package/packages/core/src/web3/utils/rpcEngine.ts +6 -0
  63. package/packages/core/src/web3/utils/tokens.ts +6 -0
  64. package/packages/core/src/web3/utils/vaultClient.d.ts +2 -0
  65. package/packages/core/src/web3/utils/vaultClient.js +122 -0
  66. package/packages/dashboard/dist/assets/{index-LAS2_trL.js → index-p3yL9asH.js} +3 -3
  67. package/packages/dashboard/dist/index.html +1 -1
  68. package/packages/dashboard/dist/robinhood.png +0 -0
  69. package/packages/dashboard/dist/robinhood.svg +0 -0
  70. package/packages/dashboard/package.json +2 -2
  71. package/packages/mcp-server/package.json +2 -1
  72. package/packages/policy/package.json +19 -4
  73. package/packages/policy/src/engine.ts +288 -0
  74. package/packages/policy/src/index.ts +1 -0
  75. package/packages/policy/src/server.ts +9 -278
  76. package/packages/signer/package.json +3 -2
  77. package/packages/signer/src/NyxoraSigner.ts +22 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepashangelog.com/en/1.0.0/),
6
6
 
7
+ ## [26.7.10]
8
+ ### Web3 Integrations
9
+ - **Robinhood Chain Support**: Officially integrated `Robinhood Chain` (Mainnet) and `Robinhood Testnet` support natively via `viem` `v2.55.0` upgrade. Added seamless RPC mappings, updated the dashboard network selectors with official SVG branding, and ensured high-frequency trading capabilities in the Signer remain stable.
10
+
11
+ ### Core Architecture & Routing
12
+ - **Deterministic Intent Fast-Path**: Bypassed the LLM semantic router for short confirmation phrases. Messages containing global confirmation keywords (e.g., "yes", "sure", "proceed") now deterministically inherit the strict contextual boundary (`os` or `web3`) of the Assistant's previous permission request, completely eradicating routing hallucinations.
13
+ - **Global Codebase Standardization**: Refactored hardcoded regional slang from the routing logic into a comprehensive, professional English-first `CONFIRM_WORDS` array, natively supporting global users while retaining common localized confirmations as secondary fallbacks.
14
+
15
+ ### Fallback Execution Engine
16
+ - **Native `<execute_bash>` Support**: Enhanced the `osAgent` and `web3Agent` Fallback Parsers to autonomously intercept and parse `<execute_bash>` and `<execute>` XML tags. This restores full execution capability for open-weight models that instinctively utilize these tags instead of strict JSON tool calls.
17
+ - **Display Sanitization Hardening**: Appended `execute_bash` and `execute` to the automated UI Sanitizer `tagsToRemove` registry. Raw bash execution blocks hallucinated by the LLM are now stripped at the edge layer before streaming to Telegram or the Dashboard.
18
+
19
+ ### Web3 Signer SDK & Execution Guardrails
20
+ - **Receipt Waiter Integration (Anti-False-Positive)**: Remediated a severe "Fire-and-Forget" architectural bug in `NyxoraSigner.ts` where broadcasted transactions were immediately reported as successful to the LLM regardless of actual on-chain finality. The Signer now strictly executes a `waitForTransactionReceipt` hook (capped at a 20-second timeout).
21
+ - **Revert Detection**: If a transaction reverts on-chain (e.g., due to strict MEV slippage or gas exhaustion), the Signer violently rejects the promise with `reverted`, preventing the AI from falsely declaring success.
22
+ - **Pending Timeout Grace**: If the blockchain experiences congestion and fails to confirm within the 20-second window, the system falls back gracefully by returning `"Transaction broadcasted (Pending receipt)"` to ensure the 30-second Policy Engine HTTP timeout is never triggered, allowing the AI to report accurate pending status.
23
+
7
24
  ## [26.7.9]
8
25
  ### Features & Architecture
9
26
 
package/README.md CHANGED
@@ -2,14 +2,15 @@
2
2
  **Your Personal Web3 Assistant.**
3
3
 
4
4
 
5
- [![Status: Prototype](https://img.shields.io/badge/Status-Prototype-orange.svg)](#)
6
-
7
- [![MCP Supported](https://img.shields.io/badge/MCP-Supported-blue.svg)](#)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
- [![Security: Defense-in-Depth](https://img.shields.io/badge/Security-Defense--in--Depth-blue.svg)](#️-advanced-security-threat-model)
10
- [![Execution: Cryptographic Approval](https://img.shields.io/badge/Execution-Cryptographic--Approval-orange.svg)](#️-advanced-security-threat-model)
11
- [![Privacy: Local-Only Keys](https://img.shields.io/badge/Privacy-Local--Only--Keys-success.svg)](#️-advanced-security-threat-model)
12
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com)
5
+ [![Status: Alpha](https://img.shields.io/badge/Status-Alpha-orange.svg?style=for-the-badge)](#)
6
+ [![Donate on DeBank](https://img.shields.io/badge/DONATE-DEBANK-FE7E41?style=for-the-badge&logo=ethereum&logoColor=white)](https://debank.com/profile/0xe5c21f46993c67cfe04fcf1579486d390be7b535)
7
+
8
+ [![MCP Supported](https://img.shields.io/badge/MCP-Supported-blue.svg?style=for-the-badge)](#)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
10
+ [![Security: Defense-in-Depth](https://img.shields.io/badge/Security-Defense--in--Depth-blue.svg?style=for-the-badge)](#️-advanced-security-threat-model)
11
+ [![Execution: Cryptographic Approval](https://img.shields.io/badge/Execution-Cryptographic--Approval-orange.svg?style=for-the-badge)](#️-advanced-security-threat-model)
12
+ [![Privacy: Local-Only Keys](https://img.shields.io/badge/Privacy-Local--Only--Keys-success.svg?style=for-the-badge)](#️-advanced-security-threat-model)
13
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge)](https://makeapullrequest.com)
13
14
 
14
15
  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.
15
16
 
package/SUPPORT.md ADDED
@@ -0,0 +1,18 @@
1
+ # Support Nyxora Protocol
2
+
3
+ Nyxora is an open-source initiative built on the belief that powerful, zero-trust Web3 AI infrastructure should be accessible to everyone. We dedicate countless hours to research, continuous development, and maintaining this ecosystem to ensure it remains secure and cutting-edge.
4
+
5
+ If Nyxora has brought value to you, your projects, or your organization, consider supporting our ongoing development. While completely optional, your contribution plays a vital role in helping us cover infrastructure costs, fund security audits, and dedicate more time to shipping new features.
6
+
7
+ ### How to Contribute
8
+
9
+ We gratefully accept community support via any EVM-compatible network (Ethereum, Arbitrum, Optimism, Polygon, Binance Smart Chain, etc.).
10
+
11
+ **EVM Address:**
12
+ `0xE5c21F46993C67CFe04FCF1579486D390Be7B535`
13
+
14
+ > 🔗 **[View & Donate via DeBank](https://debank.com/profile/0xe5c21f46993c67cfe04fcf1579486d390be7b535)**
15
+
16
+ *Every contribution, no matter the size, is deeply appreciated and directly fuels the future of autonomous Web3 AI.*
17
+
18
+ Thank you for being an amazing part of the Nyxora journey! 🌌
@@ -171,7 +171,7 @@ CRITICAL INSTRUCTIONS:
171
171
  }
172
172
  tracker_1.Tracker.addEvent('llm.response', { provider: config.llm.provider, tool_calls: responseMessage.tool_calls?.length || 0 });
173
173
  // P1: Capture <think> blocks for scratchpad, get clean content
174
- const cleanedContent = scratchpad.capture(responseMessage.content || '', turnCount);
174
+ let cleanedContent = scratchpad.capture(responseMessage.content || '', turnCount);
175
175
  exports.logger.addEntry({
176
176
  role: 'assistant',
177
177
  content: cleanedContent || '',
@@ -225,9 +225,51 @@ CRITICAL INSTRUCTIONS:
225
225
  });
226
226
  }
227
227
  }
228
+ // 3. <tool_code> JSON arrays
229
+ const toolCodeRegex = /<tool_code>([\s\S]*?)<\/tool_code>/g;
230
+ let toolCodeMatch;
231
+ while ((toolCodeMatch = toolCodeRegex.exec(responseMessage.content)) !== null) {
232
+ try {
233
+ const jsonStr = toolCodeMatch[1].trim();
234
+ const parsedArray = JSON.parse(jsonStr);
235
+ if (Array.isArray(parsedArray)) {
236
+ for (const item of parsedArray) {
237
+ if (item.function_name && item.tool_args) {
238
+ fallbacks.push({
239
+ id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
240
+ type: 'function',
241
+ function: {
242
+ name: item.function_name,
243
+ arguments: typeof item.tool_args === 'string' ? item.tool_args : JSON.stringify(item.tool_args)
244
+ }
245
+ });
246
+ }
247
+ }
248
+ }
249
+ }
250
+ catch (e) {
251
+ // Ignore parse errors, maybe it's not JSON
252
+ }
253
+ }
254
+ // 4. <execute_bash> or <execute> blocks
255
+ const executeRegex = /<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g;
256
+ let executeMatch;
257
+ while ((executeMatch = executeRegex.exec(responseMessage.content)) !== null) {
258
+ const bashCmd = executeMatch[1].trim();
259
+ if (bashCmd) {
260
+ fallbacks.push({
261
+ id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
262
+ type: 'function',
263
+ function: {
264
+ name: 'run_terminal_command',
265
+ arguments: JSON.stringify({ command: bashCmd })
266
+ }
267
+ });
268
+ }
269
+ }
228
270
  if (fallbacks.length > 0) {
229
271
  responseMessage.tool_calls = fallbacks;
230
- responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').trim();
272
+ responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').replace(/<tool_code>([\s\S]*?)<\/tool_code>/g, '').replace(/<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g, '').trim();
231
273
  console.log(picocolors_1.default.cyan(`[Fallback Parser] Intercepted ${fallbacks.length} raw text commands and converted to tool_calls.`));
232
274
  // Update logger entry with the intercepted tool calls
233
275
  exports.logger.addEntry({
@@ -238,6 +280,22 @@ CRITICAL INSTRUCTIONS:
238
280
  }
239
281
  }
240
282
  // -----------------------------------------------------
283
+ // --- DISPLAY SANITIZATION ---
284
+ // Remove commonly leaked XML tags (reasoning & tool calls) to prevent UI clutter
285
+ if (responseMessage.content) {
286
+ const tagsToRemove = ['tool_code', 'tool_call', 'tool_calls', 'function_call', 'function_calls', 'execute', 'execute_bash', 'think', 'thought', 'reasoning'];
287
+ tagsToRemove.forEach(tag => {
288
+ // Remove complete tags and their contents
289
+ const regex = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>\\s*`, 'gi');
290
+ responseMessage.content = responseMessage.content.replace(regex, '');
291
+ // Remove any orphaned closing tags
292
+ const orphanRegex = new RegExp(`<\\/${tag}>\\s*`, 'gi');
293
+ responseMessage.content = responseMessage.content.replace(orphanRegex, '');
294
+ });
295
+ responseMessage.content = responseMessage.content.trim();
296
+ cleanedContent = responseMessage.content;
297
+ }
298
+ // -----------------------------------------------------
241
299
  if (!responseMessage.tool_calls || responseMessage.tool_calls.length === 0) {
242
300
  // --- CRITIC PASS REMOVED ---
243
301
  // The new PromptBuilder handles robust reasoning. Post-generation critic
@@ -368,7 +426,7 @@ CRITICAL INSTRUCTIONS:
368
426
  // V2 Optimization: Zero-LLM Fast Return for transaction tools
369
427
  if (canFastReturnAll && accumulatedResults.length > 0) {
370
428
  const finalContent = accumulatedResults.join('\n\n---\n\n');
371
- exports.logger.addEntry({ role: 'assistant', content: finalContent }, sessionId);
429
+ exports.logger.addEntry({ role: 'user', content: `[System Notification: The tool executed successfully and returned the following result directly to the user]\n${finalContent}` }, sessionId);
372
430
  triggerBackgroundReview(sessionId);
373
431
  return finalContent;
374
432
  }
@@ -454,11 +512,9 @@ CRITICAL INSTRUCTIONS:
454
512
  let streamedContent = '';
455
513
  const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
456
514
  streamedContent = '';
457
- // RC#1 FIX: Only clear the Telegram buffer on the FIRST turn.
458
- // On subsequent turns (after tool calls), the buffer already shows useful
459
- // progress info. Resetting it causes visible content to disappear.
460
- if (turnCount === 1)
461
- onChunk('[CLEAR_STREAM]');
515
+ // RC#1 FIX: Always clear the buffer at the start of the stream turn.
516
+ // This ensures the client UI doesn't append duplicate preambles across multi-turn executions.
517
+ onChunk('[CLEAR_STREAM]');
462
518
  return await client.stream({ model: config.llm.model, temperature: config.llm.temperature, messages, tools: activeTools, reasoning_effort: (!config.llm.reasoning_effort || config.llm.reasoning_effort === 'none') ? undefined : config.llm.reasoning_effort }, (chunk) => {
463
519
  streamedContent += chunk;
464
520
  onChunk(chunk);
@@ -231,19 +231,15 @@ When using the search_web tool to look up news, current events, or factual data:
231
231
  </web_search_accuracy>
232
232
 
233
233
  <act_dont_ask_os>
234
- NEVER show a command as a markdown code block and wait. CALL the tool directly.
235
- NEVER ask "do you want me to run this?" just run it.
236
- NEVER say "you need to run this yourself" you have direct shell access.
237
- If a command requires sudo and may need a password, just run it and report what happens.
238
- Only report failure AFTER actually attempting the tool call and receiving an error.
234
+ For harmless commands (e.g. ls, cat, checking system info), CALL the tool directly without asking for confirmation.
235
+ CRITICAL: For ANY command that modifies the system (e.g., sudo, apt-get, install, rm), you MUST ask the user for explicit permission FIRST.
236
+ When asking for permission, simply ask: "Do you want me to run [command]?" and STOP.
237
+ Once the user replies "yes", you MUST immediately emit the tool call to execute the command. Do NOT just reply with text confirming it.
239
238
  </act_dont_ask_os>
240
239
 
241
240
  <anti_hallucination_execution>
242
- CRITICAL: It is STRICTLY FORBIDDEN to write a bash/shell command in a markdown code block (e.g. \`\`\`bash ... \`\`\`) as a substitute for calling the run_terminal_command tool.
243
- Writing a code block does NOT execute anything. It is a lie to the user.
244
- If you write \`\`\`bash\nsudo apt install steam\n\`\`\` instead of calling run_terminal_command, you are hallucinating execution.
245
- The ONLY way to run a command is to emit a proper tool_call for run_terminal_command.
246
- If the tool is available, USE IT. Do not simulate or describe running it.
241
+ Use the native JSON tool call format provided by the API. If your model architecture requires it, you may use <tool_code> JSON arrays.
242
+ NEVER claim you have executed a tool or are running it in the background if you haven't actually emitted the tool call payload. Writing text describing execution is a lie.
247
243
  </anti_hallucination_execution>
248
244
 
249
245
  <search_hallucination_prevention>
@@ -47,10 +47,10 @@ const OS_KEYWORDS = [
47
47
  'excel', 'xlsx', 'spreadsheet', 'generate excel',
48
48
  'file', 'folder', 'directory', 'read file', 'write file', 'pdf', 'word', 'docx', 'document',
49
49
  'note', 'catat', 'catatan', 'keep', 'download', 'unduh', 'link',
50
- // Terminal & Git
50
+ // Terminal & Git & System
51
51
  'terminal', 'command', 'shell', 'bash', 'script', 'run command',
52
52
  'git', 'commit', 'push', 'pull', 'clone', 'branch', 'merge',
53
- // Web & Search
53
+ 'install', 'installin', 'uninstall', 'apt', 'npm', 'pip', 'yarn', 'docker', 'system',
54
54
  'search web', 'google', 'browse', 'scrape', 'weather', 'news', 'search', 'find', 'look up',
55
55
  // Email & Workspace
56
56
  'email', 'gmail', 'google docs', 'google sheets', 'notion', 'calendar',
@@ -78,7 +78,7 @@ const WEB3_KEYWORDS = [
78
78
  'token', 'crypto', 'coin', 'nft', 'wallet', 'address', 'contract',
79
79
  // Major Chains & L2s
80
80
  'ethereum', 'polygon', 'arbitrum', 'optimism', 'bsc', 'mainnet', 'testnet',
81
- 'on-chain', 'blockchain', 'solana', 'avalanche', 'base',
81
+ 'on-chain', 'blockchain', 'solana', 'avalanche', 'base', 'robinhood',
82
82
  // Top 50+ Global Assets / Tickers
83
83
  'eth', 'bnb', 'usdt', 'usdc', 'sol', 'matic', 'arb', 'op', 'btc', 'bitcoin',
84
84
  'xrp', 'doge', 'dogecoin', 'avax', 'ada', 'cardano', 'pepe', 'shib', 'shiba',
@@ -221,18 +221,44 @@ async function processUserInput(input, role = 'user', onProgress, sessionId) {
221
221
  context = 'web3';
222
222
  preCheckMatched = true;
223
223
  }
224
+ if (!preCheckMatched) {
225
+ // Support global confirmation keywords for short fast-path routing
226
+ const CONFIRM_WORDS = [
227
+ // English (Global Defaults)
228
+ 'yes', 'yeah', 'yep', 'yup', 'sure', 'ok', 'okay', 'alright', 'confirm', 'proceed', 'continue', 'do it', 'go ahead', 'exactly', 'indeed',
229
+ // Common Localizations (e.g., ID, ES)
230
+ 'ya', 'yakin', 'boleh', 'gas', 'lanjut', 'setuju', 'si', 'sí', 'vale', 'claro'
231
+ ];
232
+ const isConfirm = lowerInput.length < 25 && CONFIRM_WORDS.some(kw => lowerInput.includes(kw));
233
+ if (isConfirm && textOnlyHistory.length > 0) {
234
+ const lastMsg = textOnlyHistory[textOnlyHistory.length - 1];
235
+ if (lastMsg.role === 'assistant') {
236
+ const lastContent = lastMsg.content.toLowerCase();
237
+ // Check if the assistant was asking for permission to run an OS command
238
+ if (lastContent.includes('sudo ') || lastContent.includes('terminal') || lastContent.includes('apt') || lastContent.includes('perintah') || lastContent.includes('install') || lastContent.includes('command')) {
239
+ context = 'os';
240
+ preCheckMatched = true;
241
+ // Check if the assistant was asking for permission to run a Web3 transaction
242
+ }
243
+ else if (lastContent.includes('swap') || lastContent.includes('transfer') || lastContent.includes('token') || lastContent.includes('wallet') || lastContent.includes('transaction')) {
244
+ context = 'web3';
245
+ preCheckMatched = true;
246
+ }
247
+ }
248
+ }
249
+ }
224
250
  if (preCheckMatched) {
225
- console.log(picocolors_1.default.cyan(`[Orchestrator] Intent pre-classified (keyword match) as: ${context.toUpperCase()}`));
251
+ console.log(picocolors_1.default.cyan(`[Orchestrator] Intent pre-classified as: ${context.toUpperCase()}`));
226
252
  }
227
253
  else {
228
- // ── Fallback: LLM Router (untuk intent ambigu / percakapan umum) ─────────
254
+ // ── Fallback: LLM Router (for ambiguous intents / general conversation) ─────────
229
255
  const routerPrompt = `You are Nyxora's Semantic Intent Router. Classify the user's FINAL message into one of four categories: 'web3', 'os', 'compound', or 'general'.
230
256
  Rules:
231
- 1. FOCUS ONLY ON THE FINAL MESSAGE. History is only for context.
257
+ 1. FOCUS ON THE FINAL MESSAGE, but use history to understand short answers. If the final message is a short confirmation (e.g. "ya", "yes", "do it") or answer to a pending permission request for a tool, CLASSIFY IT BASED ON THE CONTEXT. If the previous message was about an OS command, reply 'os'. If it was about crypto/web3, reply 'web3'.
232
258
  2. The user may speak in ANY language, including casual slang, idioms, or abbreviations.
233
- 3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message. Example: "swap 1 ETH to USDC and email me the receipt". If in doubt, do NOT use 'compound'.
259
+ 3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message.
234
260
  4. If the core intent involves ONLY blockchain, crypto, bridging, swapping, trading, sending/receiving, tokens, wallets, transactions, OR asking for the price/conversion of ANY asset to fiat, reply 'web3'.
235
- 5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup (e.g., 'who won the game', 'what is the registration date', 'cek info', 'cari tahu'), reply 'os'.
261
+ 5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup, reply 'os'.
236
262
  6. If the message is casual conversation, chit-chat, greetings, capability questions (e.g., 'what can you do?', 'bisa ngapain?', 'help', 'menu'), or any open-ended/vague question, reply 'general'.
237
263
  Reply with EXACTLY ONE WORD: compound, web3, os, or general.`;
238
264
  const routerMessages = [
@@ -401,14 +427,40 @@ async function processUserInputStream(input, originalOnChunk, onProgress, sessio
401
427
  context = 'web3';
402
428
  preCheckMatched = true;
403
429
  }
430
+ if (!preCheckMatched) {
431
+ // Support global confirmation keywords for short fast-path routing
432
+ const CONFIRM_WORDS = [
433
+ // English (Global Defaults)
434
+ 'yes', 'yeah', 'yep', 'yup', 'sure', 'ok', 'okay', 'alright', 'confirm', 'proceed', 'continue', 'do it', 'go ahead', 'exactly', 'indeed',
435
+ // Common Localizations (e.g., ID, ES)
436
+ 'ya', 'yakin', 'boleh', 'gas', 'lanjut', 'setuju', 'si', 'sí', 'vale', 'claro'
437
+ ];
438
+ const isConfirm = lowerInput.length < 25 && CONFIRM_WORDS.some(kw => lowerInput.includes(kw));
439
+ if (isConfirm && textOnlyHistory.length > 0) {
440
+ const lastMsg = textOnlyHistory[textOnlyHistory.length - 1];
441
+ if (lastMsg.role === 'assistant') {
442
+ const lastContent = lastMsg.content.toLowerCase();
443
+ // Check if the assistant was asking for permission to run an OS command
444
+ if (lastContent.includes('sudo ') || lastContent.includes('terminal') || lastContent.includes('apt') || lastContent.includes('perintah') || lastContent.includes('install') || lastContent.includes('command')) {
445
+ context = 'os';
446
+ preCheckMatched = true;
447
+ // Check if the assistant was asking for permission to run a Web3 transaction
448
+ }
449
+ else if (lastContent.includes('swap') || lastContent.includes('transfer') || lastContent.includes('token') || lastContent.includes('wallet') || lastContent.includes('transaction')) {
450
+ context = 'web3';
451
+ preCheckMatched = true;
452
+ }
453
+ }
454
+ }
455
+ }
404
456
  if (!preCheckMatched) {
405
457
  const routerPrompt = `You are Nyxora's Semantic Intent Router. Classify the user's FINAL message into one of four categories: 'web3', 'os', 'compound', or 'general'.
406
458
  Rules:
407
- 1. FOCUS ONLY ON THE FINAL MESSAGE. History is only for context.
459
+ 1. FOCUS ON THE FINAL MESSAGE, but use history to understand short answers. If the final message is a short confirmation (e.g. "ya", "yes", "do it") or answer to a pending permission request for a tool, CLASSIFY IT BASED ON THE CONTEXT. If the previous message was about an OS command, reply 'os'. If it was about crypto/web3, reply 'web3'.
408
460
  2. The user may speak in ANY language, including casual slang, idioms, or abbreviations.
409
- 3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message. Example: "swap 1 ETH to USDC and email me the receipt". If in doubt, do NOT use 'compound'.
461
+ 3. 'compound' is EXTREMELY RARE. Only reply 'compound' when the user EXPLICITLY requests BOTH a blockchain/crypto action AND an OS/file/email action in the SAME message.
410
462
  4. If the core intent involves ONLY blockchain, crypto, bridging, swapping, trading, sending/receiving, tokens, wallets, transactions, OR asking for the price/conversion of ANY asset to fiat, reply 'web3'.
411
- 5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup (e.g., 'who won the game', 'what is the registration date', 'cek info', 'cari tahu'), reply 'os'.
463
+ 5. If the core intent involves ONLY OS automation, weather, emails, files, terminal, changing AI settings, OR asking ANY question that requires a web search or real-world factual lookup, reply 'os'.
412
464
  6. If the message is casual conversation, chit-chat, greetings, capability questions (e.g., 'what can you do?', 'bisa ngapain?', 'help', 'menu'), or any open-ended/vague question, reply 'general'.
413
465
  Reply with EXACTLY ONE WORD: compound, web3, os, or general.`;
414
466
  const routerMessages = [
@@ -96,7 +96,7 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
96
96
  }
97
97
  tracker_1.Tracker.addEvent('llm.response', { provider: config.llm.provider, tool_calls: responseMessage.tool_calls?.length || 0 });
98
98
  // P1: Capture <think> blocks for scratchpad, get clean content
99
- const cleanedContent = scratchpad.capture(responseMessage.content || '', turnCount);
99
+ let cleanedContent = scratchpad.capture(responseMessage.content || '', turnCount);
100
100
  exports.logger.addEntry({
101
101
  role: 'assistant',
102
102
  content: cleanedContent || '',
@@ -150,9 +150,51 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
150
150
  });
151
151
  }
152
152
  }
153
+ // 3. <tool_code> JSON arrays
154
+ const toolCodeRegex = /<tool_code>([\s\S]*?)<\/tool_code>/g;
155
+ let toolCodeMatch;
156
+ while ((toolCodeMatch = toolCodeRegex.exec(responseMessage.content)) !== null) {
157
+ try {
158
+ const jsonStr = toolCodeMatch[1].trim();
159
+ const parsedArray = JSON.parse(jsonStr);
160
+ if (Array.isArray(parsedArray)) {
161
+ for (const item of parsedArray) {
162
+ if (item.function_name && item.tool_args) {
163
+ fallbacks.push({
164
+ id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
165
+ type: 'function',
166
+ function: {
167
+ name: item.function_name,
168
+ arguments: typeof item.tool_args === 'string' ? item.tool_args : JSON.stringify(item.tool_args)
169
+ }
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+ catch (e) {
176
+ // Ignore parse errors, maybe it's not JSON
177
+ }
178
+ }
179
+ // 4. <execute_bash> or <execute> blocks
180
+ const executeRegex = /<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g;
181
+ let executeMatch;
182
+ while ((executeMatch = executeRegex.exec(responseMessage.content)) !== null) {
183
+ const bashCmd = executeMatch[1].trim();
184
+ if (bashCmd) {
185
+ fallbacks.push({
186
+ id: 'call_fallback_' + Math.random().toString(36).substr(2, 9),
187
+ type: 'function',
188
+ function: {
189
+ name: 'run_terminal_command',
190
+ arguments: JSON.stringify({ command: bashCmd })
191
+ }
192
+ });
193
+ }
194
+ }
153
195
  if (fallbacks.length > 0) {
154
196
  responseMessage.tool_calls = fallbacks;
155
- responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').trim();
197
+ responseMessage.content = responseMessage.content.replace(/^\/[a-zA-Z0-9_]+\s+.*$/gm, '').replace(/```(?:bash|sh|zsh)?\n([\s\S]*?)```/g, '').replace(/<tool_code>([\s\S]*?)<\/tool_code>/g, '').replace(/<(?:execute_bash|execute)>([\s\S]*?)<\/(?:execute_bash|execute)>/g, '').trim();
156
198
  console.log(picocolors_1.default.cyan(`[Fallback Parser] Intercepted ${fallbacks.length} raw text commands and converted to tool_calls.`));
157
199
  // Update logger entry with the intercepted tool calls
158
200
  exports.logger.addEntry({
@@ -163,6 +205,22 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
163
205
  }
164
206
  }
165
207
  // ---------------------------------------------------------------
208
+ // --- DISPLAY SANITIZATION ---
209
+ // Remove commonly leaked XML tags (reasoning & tool calls) to prevent UI clutter
210
+ if (responseMessage.content) {
211
+ const tagsToRemove = ['tool_code', 'tool_call', 'tool_calls', 'function_call', 'function_calls', 'execute', 'think', 'thought', 'reasoning'];
212
+ tagsToRemove.forEach(tag => {
213
+ // Remove complete tags and their contents
214
+ const regex = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>\\s*`, 'gi');
215
+ responseMessage.content = responseMessage.content.replace(regex, '');
216
+ // Remove any orphaned closing tags
217
+ const orphanRegex = new RegExp(`<\\/${tag}>\\s*`, 'gi');
218
+ responseMessage.content = responseMessage.content.replace(orphanRegex, '');
219
+ });
220
+ responseMessage.content = responseMessage.content.trim();
221
+ cleanedContent = responseMessage.content;
222
+ }
223
+ // ---------------------------------------------------------------
166
224
  if (!responseMessage.tool_calls || responseMessage.tool_calls.length === 0) {
167
225
  return cleanedContent || '⚠️ I encountered an issue processing your request. This can happen with very complex multi-step tasks. Please try rephrasing or breaking the request into smaller steps.';
168
226
  }
@@ -284,10 +342,9 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
284
342
  else {
285
343
  consecutiveToolErrors = 0;
286
344
  }
287
- // V2 Optimization: Zero-LLM Fast Return for transaction tools
288
345
  if (canFastReturnAll && accumulatedResults.length > 0) {
289
346
  const finalContent = accumulatedResults.join('\n\n---\n\n');
290
- exports.logger.addEntry({ role: 'assistant', content: finalContent }, sessionId);
347
+ exports.logger.addEntry({ role: 'user', content: `[System Notification: The tool executed successfully and returned the following result directly to the user]\n${finalContent}` }, sessionId);
291
348
  return finalContent;
292
349
  }
293
350
  // Loop continues, sending tool results in the next turn
@@ -346,10 +403,9 @@ async function processWeb3IntentStream(input, onChunk, onProgress, sessionId) {
346
403
  let streamedContent = '';
347
404
  const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
348
405
  streamedContent = '';
349
- // RC#1 FIX: Only clear the Telegram buffer on the FIRST turn.
350
- // Subsequent turns must NOT wipe the buffer that shows tool progress.
351
- if (turnCount === 1)
352
- onChunk('[CLEAR_STREAM]');
406
+ // RC#1 FIX: Always clear the buffer at the start of the stream turn.
407
+ // Subsequent turns must wipe the buffer to prevent UI duplication of the planning text.
408
+ onChunk('[CLEAR_STREAM]');
353
409
  return await client.stream({ model: config.llm.model, temperature: config.llm.temperature, messages, tools: activeTools, reasoning_effort: config.llm.reasoning_effort }, (chunk) => {
354
410
  streamedContent += chunk;
355
411
  onChunk(chunk);
@@ -204,23 +204,12 @@ function startTelegramBot() {
204
204
  buffer = '⏳ Processing...';
205
205
  finalContentAlreadyStreamed = false;
206
206
  }
207
- else if (chunk === '[TOOL_CALL_DETECTED]' || chunk === '[TOOL_CALL_FINISHED]') {
208
- // Hermes-Agent tool boundary: Finalize the current message synchronously to avoid race conditions.
209
- const htmlToSave = formatToTelegramHTML(buffer);
210
- const msgIdToSave = progressMsgId;
211
- progressMsgId = null;
212
- buffer = '';
213
- finalContentAlreadyStreamed = false;
214
- if (pendingEdit) {
215
- clearTimeout(pendingEdit);
216
- pendingEdit = null;
217
- }
218
- if (msgIdToSave && htmlToSave) {
219
- await ctx.api.editMessageText(ctx.chat.id, msgIdToSave, htmlToSave, { parse_mode: 'HTML' }).catch(() => { });
220
- }
221
- else if (!msgIdToSave && htmlToSave) {
222
- await ctx.reply(htmlToSave, { parse_mode: 'HTML', reply_parameters: { message_id: ctx.message.message_id } }).catch(() => { });
223
- }
207
+ else if (chunk === '[TOOL_CALL_DETECTED]') {
208
+ buffer = '⏳ Processing...';
209
+ scheduleEdit();
210
+ return;
211
+ }
212
+ else if (chunk === '[TOOL_CALL_FINISHED]') {
224
213
  return;
225
214
  }
226
215
  else {
@@ -412,21 +401,12 @@ function startTelegramBot() {
412
401
  if (chunk === '[CLEAR_STREAM]') {
413
402
  buffer = '⏳ Processing...';
414
403
  }
415
- else if (chunk === '[TOOL_CALL_DETECTED]' || chunk === '[TOOL_CALL_FINISHED]') {
416
- const htmlToSave = formatToTelegramHTML(buffer);
417
- const msgIdToSave = progressMsgId;
418
- progressMsgId = null;
419
- buffer = '';
420
- if (pendingEdit) {
421
- clearTimeout(pendingEdit);
422
- pendingEdit = null;
423
- }
424
- if (msgIdToSave && htmlToSave) {
425
- await ctx.api.editMessageText(ctx.chat.id, msgIdToSave, htmlToSave, { parse_mode: 'HTML' }).catch(() => { });
426
- }
427
- else if (!msgIdToSave && htmlToSave) {
428
- await ctx.reply(htmlToSave, { parse_mode: 'HTML', reply_parameters: { message_id: ctx.message.message_id } }).catch(() => { });
429
- }
404
+ else if (chunk === '[TOOL_CALL_DETECTED]') {
405
+ buffer = '⏳ Processing...';
406
+ scheduleEdit();
407
+ return;
408
+ }
409
+ else if (chunk === '[TOOL_CALL_FINISHED]') {
430
410
  return;
431
411
  }
432
412
  else {
@@ -277,6 +277,8 @@ Provider: ${config.llm.provider}`;
277
277
  { value: 'base_sepolia', label: 'Base Sepolia (Testnet)' },
278
278
  { value: 'arbitrum_sepolia', label: 'Arbitrum Sepolia (Testnet)' },
279
279
  { value: 'optimism_sepolia', label: 'OP Sepolia (Testnet)' },
280
+ { value: 'robinhood', label: 'Robinhood Chain' },
281
+ { value: 'robinhood_testnet', label: 'Robinhood Testnet' },
280
282
  ],
281
283
  });
282
284
  if ((0, prompts_1.isCancel)(defaultChain))
@@ -16,51 +16,37 @@ exports.needsCompression = needsCompression;
16
16
  const llmUtils_1 = require("./llmUtils");
17
17
  const parser_1 = require("../config/parser");
18
18
  const picocolors_1 = __importDefault(require("picocolors"));
19
- /** How many text-only exchanges to keep verbatim before summarising older ones. */
20
- const VERBATIM_TAIL = 10;
21
- /** Minimum history length (user+assistant pairs) before we bother summarising. */
22
- const SUMMARISE_THRESHOLD = 20;
23
- /**
24
- * Cache: prevents re-compressing the same history on every nudge iteration.
25
- * Key: sessionId — value contains the textCount at time of compression and
26
- * the resulting compressed history.
27
- *
28
- * During nudge loops, agent adds only system-role nudge messages. Those do NOT
29
- * increment the textCount (user+assistant only). So the cache key stays the same
30
- * and we return the cached result instead of making another LLM call.
31
- * Cache is invalidated when the textCount grows (i.e. real LLM response landed).
32
- */
19
+ const VERBATIM_TAIL = 20;
20
+ const SUMMARISE_THRESHOLD = 40;
33
21
  const compressionCache = new Map();
34
- /**
35
- * Returns a history array that fits in context.
36
- * If history is short, returns as-is.
37
- * If long, prepends a "Conversation Summary" system message and keeps the tail verbatim.
38
- *
39
- * @param history Full raw history from logger.
40
- * @param sessionId Optional session identifier used to cache results across nudge turns.
41
- */
42
22
  async function compressHistory(history, sessionId) {
43
- const textMessages = history.filter(m => (m.role === 'user' || m.role === 'assistant') && m.content && !m.tool_calls);
44
- if (textMessages.length < SUMMARISE_THRESHOLD) {
45
- return history; // nothing to compress yet
23
+ if (history.length < SUMMARISE_THRESHOLD) {
24
+ return history;
46
25
  }
47
- // Cache hit: textCount hasn't grown since last compression for this session.
48
- // This prevents 4x redundant LLM summarization calls during nudge iterations.
49
26
  if (sessionId) {
50
27
  const cached = compressionCache.get(sessionId);
51
- if (cached && cached.textCount === textMessages.length) {
28
+ // FIX: Invalidate cache if ANY message is added to history, including tool calls
29
+ if (cached && cached.textCount === history.length) {
52
30
  return cached.compressed;
53
31
  }
54
32
  }
55
- // Split: old messages to summarise, recent tail to keep verbatim
56
- const tailMessages = textMessages.slice(-VERBATIM_TAIL);
57
- const oldMessages = textMessages.slice(0, -VERBATIM_TAIL);
33
+ // FIX: Keep the tail strictly chronological so LLM doesn't get confused
34
+ const tailMessages = history.slice(-VERBATIM_TAIL);
35
+ const oldMessages = history.slice(0, -VERBATIM_TAIL);
58
36
  if (oldMessages.length === 0)
59
37
  return history;
60
38
  try {
61
39
  const config = (0, parser_1.loadConfig)();
62
40
  const historyText = oldMessages
63
- .map(m => `${m.role === 'user' ? 'USER' : 'ASSISTANT'}: ${m.content}`)
41
+ .map(m => {
42
+ let name = m.role.toUpperCase();
43
+ let content = m.content || '';
44
+ if (m.role === 'assistant' && m.tool_calls)
45
+ content = `[Called tools: ${m.tool_calls.map(tc => tc.function.name).join(', ')}]`;
46
+ if (m.role === 'tool')
47
+ content = `[Tool Result: ${m.name}]`;
48
+ return `${name}: ${content}`;
49
+ })
64
50
  .join('\n');
65
51
  const summaryRes = await (0, llmUtils_1.executeWithRetry)(async (client) => client.chat({
66
52
  model: config.llm.model,
@@ -79,31 +65,22 @@ Write in third person. Be factual, not narrative.`
79
65
  if (!summaryText)
80
66
  return history;
81
67
  console.log(picocolors_1.default.magenta(`[ContextSummarizer] Compressed ${oldMessages.length} old messages into summary.`));
82
- // Return: summary as system note + verbatim tail + all tool messages (never drop tool messages)
83
- const toolMessages = history.filter(m => m.role === 'tool' || (m.role === 'assistant' && m.tool_calls));
84
68
  const compressed = [
85
69
  {
86
70
  role: 'system',
87
71
  content: `--- CONVERSATION SUMMARY (earlier context) ---\n${summaryText}\n--- END SUMMARY ---`
88
72
  },
89
- ...tailMessages,
90
- ...toolMessages.slice(-6) // keep last 6 tool interactions
73
+ ...tailMessages
91
74
  ];
92
- // Persist in cache for this session
93
75
  if (sessionId) {
94
- compressionCache.set(sessionId, { textCount: textMessages.length, compressed });
76
+ compressionCache.set(sessionId, { textCount: history.length, compressed });
95
77
  }
96
78
  return compressed;
97
79
  }
98
80
  catch (e) {
99
- // On failure, just return the tail — never crash the agent
100
81
  return history.slice(-VERBATIM_TAIL * 2);
101
82
  }
102
83
  }
103
- /**
104
- * Fast check — should we attempt compression?
105
- */
106
84
  function needsCompression(history) {
107
- const textCount = history.filter(m => (m.role === 'user' || m.role === 'assistant') && m.content && !m.tool_calls).length;
108
- return textCount >= SUMMARISE_THRESHOLD;
85
+ return history.length >= SUMMARISE_THRESHOLD;
109
86
  }