nyxora 1.3.0 → 1.4.0

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.
@@ -22,6 +22,14 @@ const checkSecurity_1 = require("../web3/skills/checkSecurity");
22
22
  const marketAnalysis_1 = require("../web3/skills/marketAnalysis");
23
23
  const checkPortfolio_1 = require("../web3/skills/checkPortfolio");
24
24
  const limitOrderManager_1 = require("./limitOrderManager");
25
+ const updateProfile_1 = require("./updateProfile");
26
+ const updateSecurityPolicy_1 = require("../system/skills/updateSecurityPolicy");
27
+ const readFile_1 = require("../system/skills/readFile");
28
+ const writeFile_1 = require("../system/skills/writeFile");
29
+ const executeShell_1 = require("../system/skills/executeShell");
30
+ const browseWeb_1 = require("../system/skills/browseWeb");
31
+ const installSkill_1 = require("../system/skills/installSkill");
32
+ const pluginManager_1 = require("../system/pluginManager");
25
33
  const paths_1 = require("../config/paths");
26
34
  exports.logger = new logger_1.Logger();
27
35
  let currentKeyIndex = 0;
@@ -113,6 +121,17 @@ If the user doesn't specify a chain, default to: ${config.agent.default_chain}.`
113
121
  catch (error) {
114
122
  console.error('Failed to read user.md:', error);
115
123
  }
124
+ // Read security_policy.md for NLP security constraints
125
+ try {
126
+ const policyPath = (0, paths_1.getPath)('security_policy.md');
127
+ if (fs_1.default.existsSync(policyPath)) {
128
+ const securityInstructions = fs_1.default.readFileSync(policyPath, 'utf8');
129
+ basePrompt += `\n\n--- SECURITY POLICY (MANDATORY RULES) ---\n${securityInstructions}\n\nCRITICAL: If the user asks you to perform an action that violates the Security Policy above, YOU MUST NOT EXECUTE IT DIRECTLY. Instead, ask for their explicit permission first.`;
130
+ }
131
+ }
132
+ catch (error) {
133
+ console.error('Failed to read security_policy.md:', error);
134
+ }
116
135
  return basePrompt;
117
136
  }
118
137
  async function processUserInput(input, role = 'user') {
@@ -162,7 +181,15 @@ async function processUserInput(input, role = 'user') {
162
181
  checkPortfolio_1.checkPortfolioToolDefinition,
163
182
  limitOrderManager_1.createLimitOrderToolDefinition,
164
183
  limitOrderManager_1.listLimitOrdersToolDefinition,
165
- limitOrderManager_1.cancelLimitOrderToolDefinition
184
+ limitOrderManager_1.cancelLimitOrderToolDefinition,
185
+ updateProfile_1.updateProfileToolDefinition,
186
+ updateSecurityPolicy_1.updateSecurityPolicyToolDefinition,
187
+ readFile_1.readLocalFileToolDefinition,
188
+ writeFile_1.writeLocalFileToolDefinition,
189
+ executeShell_1.runTerminalCommandToolDefinition,
190
+ browseWeb_1.browseWebsiteToolDefinition,
191
+ installSkill_1.installExternalSkillToolDefinition,
192
+ ...pluginManager_1.pluginManager.getToolDefinitions()
166
193
  ],
167
194
  tool_choice: "auto",
168
195
  });
@@ -244,8 +271,44 @@ async function processUserInput(input, role = 'user') {
244
271
  result = limitOrderManager_1.limitOrderManager.cancelOrder(args.id);
245
272
  break;
246
273
  }
247
- default:
248
- result = `Error: Tool ${toolName} is not implemented.`;
274
+ case 'update_profile': {
275
+ result = (0, updateProfile_1.updateProfile)(args.content, args.mode);
276
+ break;
277
+ }
278
+ case 'update_security_policy': {
279
+ result = (0, updateSecurityPolicy_1.updateSecurityPolicy)(args.rule, args.action);
280
+ break;
281
+ }
282
+ case 'read_local_file': {
283
+ result = (0, readFile_1.readLocalFile)(args.filePath);
284
+ break;
285
+ }
286
+ case 'write_local_file': {
287
+ result = (0, writeFile_1.writeLocalFile)(args.filePath, args.content);
288
+ break;
289
+ }
290
+ case 'run_terminal_command': {
291
+ result = await (0, executeShell_1.runTerminalCommand)(args.command);
292
+ break;
293
+ }
294
+ case 'browse_website': {
295
+ result = await (0, browseWeb_1.browseWebsite)(args.url);
296
+ break;
297
+ }
298
+ case 'install_external_skill': {
299
+ result = await (0, installSkill_1.installExternalSkill)(args.url);
300
+ break;
301
+ }
302
+ default: {
303
+ const externalResult = await pluginManager_1.pluginManager.executeTool(toolName, args);
304
+ if (externalResult !== null) {
305
+ result = externalResult;
306
+ }
307
+ else {
308
+ result = `Error: Tool ${toolName} is not implemented.`;
309
+ }
310
+ break;
311
+ }
249
312
  }
250
313
  exports.logger.addEntry({
251
314
  role: 'tool',
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.updateProfileToolDefinition = void 0;
7
+ exports.updateProfile = updateProfile;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const paths_1 = require("../config/paths");
10
+ function updateProfile(content, mode) {
11
+ try {
12
+ const userMdPath = (0, paths_1.getPath)('user.md');
13
+ if (mode === 'replace') {
14
+ fs_1.default.writeFileSync(userMdPath, content, 'utf8');
15
+ return "Profile replaced successfully. New user.md has been saved.";
16
+ }
17
+ else {
18
+ let existingContent = "";
19
+ if (fs_1.default.existsSync(userMdPath)) {
20
+ existingContent = fs_1.default.readFileSync(userMdPath, 'utf8');
21
+ }
22
+ const newContent = existingContent + "\n" + content;
23
+ fs_1.default.writeFileSync(userMdPath, newContent, 'utf8');
24
+ return "Profile appended successfully. New instructions added to user.md.";
25
+ }
26
+ }
27
+ catch (error) {
28
+ return `Failed to update profile: ${error.message}`;
29
+ }
30
+ }
31
+ exports.updateProfileToolDefinition = {
32
+ type: "function",
33
+ function: {
34
+ name: "update_profile",
35
+ description: "Updates or rewrites the user.md file. Use this when the user asks you to remember something about them, change their persona, or update your instructions.",
36
+ parameters: {
37
+ type: "object",
38
+ properties: {
39
+ content: {
40
+ type: "string",
41
+ description: "The content to write or append to user.md",
42
+ },
43
+ mode: {
44
+ type: "string",
45
+ enum: ["append", "replace"],
46
+ description: "Whether to append the content to the existing file or replace the entire file.",
47
+ }
48
+ },
49
+ required: ["content", "mode"],
50
+ },
51
+ },
52
+ };
@@ -13,6 +13,7 @@ const parser_1 = require("../config/parser");
13
13
  const tracker_1 = require("./tracker");
14
14
  const transactionManager_1 = require("../agent/transactionManager");
15
15
  const limitOrderManager_1 = require("../agent/limitOrderManager");
16
+ const pluginManager_1 = require("../system/pluginManager");
16
17
  const transfer_1 = require("../web3/skills/transfer");
17
18
  const swapToken_1 = require("../web3/skills/swapToken");
18
19
  const getBalance_1 = require("../web3/skills/getBalance");
@@ -181,6 +182,9 @@ app.use((req, res, next) => {
181
182
  }
182
183
  });
183
184
  function startServer() {
185
+ pluginManager_1.pluginManager.loadPlugins().then(() => {
186
+ console.log(`[PluginManager] Finished loading external skills.`);
187
+ });
184
188
  limitOrderManager_1.limitOrderManager.startMonitor();
185
189
  const PORT = process.env.PORT || 3000;
186
190
  app.listen(PORT, () => {
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pluginManager = exports.PluginManager = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ class PluginManager {
10
+ skills = new Map();
11
+ async loadPlugins() {
12
+ const pluginsDir = path_1.default.join(process.cwd(), 'src', 'external_skills');
13
+ if (!fs_1.default.existsSync(pluginsDir)) {
14
+ fs_1.default.mkdirSync(pluginsDir, { recursive: true });
15
+ return;
16
+ }
17
+ const files = fs_1.default.readdirSync(pluginsDir);
18
+ for (const file of files) {
19
+ if (file.endsWith('.js') || file.endsWith('.ts')) {
20
+ try {
21
+ // Dynamic import requires relative path from this file or absolute path
22
+ // For TS compiled to JS, absolute path is safer
23
+ const absolutePath = path_1.default.resolve(pluginsDir, file);
24
+ // Note: In development with ts-node, requiring .ts works.
25
+ // In production, we need compiled .js files.
26
+ const module = require(absolutePath);
27
+ if (module.toolDefinition && module.execute) {
28
+ const toolName = module.toolDefinition.function.name;
29
+ this.skills.set(toolName, module);
30
+ console.log(`[PluginManager] Loaded external skill: ${toolName}`);
31
+ }
32
+ }
33
+ catch (error) {
34
+ console.error(`[PluginManager] Failed to load plugin ${file}:`, error);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ getToolDefinitions() {
40
+ return Array.from(this.skills.values()).map(skill => skill.toolDefinition);
41
+ }
42
+ async executeTool(toolName, args) {
43
+ const skill = this.skills.get(toolName);
44
+ if (skill) {
45
+ try {
46
+ return await skill.execute(args);
47
+ }
48
+ catch (error) {
49
+ return `External skill ${toolName} failed: ${error.message}`;
50
+ }
51
+ }
52
+ return null; // Tool not found in external skills
53
+ }
54
+ }
55
+ exports.PluginManager = PluginManager;
56
+ exports.pluginManager = new PluginManager();
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.browseWebsiteToolDefinition = void 0;
4
+ exports.browseWebsite = browseWebsite;
5
+ async function browseWebsite(url) {
6
+ try {
7
+ const response = await fetch(url, {
8
+ headers: {
9
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Nyxora/1.0',
10
+ }
11
+ });
12
+ if (!response.ok) {
13
+ return `Error: Failed to fetch URL, Status: ${response.status} ${response.statusText}`;
14
+ }
15
+ const html = await response.text();
16
+ // A very basic HTML to text stripping to avoid exceeding context limits
17
+ // In a production app, we would use cheerio or puppeteer.
18
+ const text = html
19
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
20
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
21
+ .replace(/<[^>]+>/g, ' ')
22
+ .replace(/\s+/g, ' ')
23
+ .trim();
24
+ // Limit to first 20000 characters to prevent context overflow
25
+ if (text.length > 20000) {
26
+ return text.substring(0, 20000) + "... [Content Truncated]";
27
+ }
28
+ return text;
29
+ }
30
+ catch (error) {
31
+ return `Failed to browse website: ${error.message}`;
32
+ }
33
+ }
34
+ exports.browseWebsiteToolDefinition = {
35
+ type: "function",
36
+ function: {
37
+ name: "browse_website",
38
+ description: "Fetches and reads the textual content of a webpage.",
39
+ parameters: {
40
+ type: "object",
41
+ properties: {
42
+ url: {
43
+ type: "string",
44
+ description: "The URL of the webpage to read.",
45
+ }
46
+ },
47
+ required: ["url"],
48
+ },
49
+ },
50
+ };
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runTerminalCommandToolDefinition = void 0;
4
+ exports.runTerminalCommand = runTerminalCommand;
5
+ const child_process_1 = require("child_process");
6
+ function runTerminalCommand(command) {
7
+ return new Promise((resolve) => {
8
+ (0, child_process_1.exec)(command, { maxBuffer: 1024 * 1024 * 10 }, (error, stdout, stderr) => {
9
+ let output = "";
10
+ if (stdout)
11
+ output += `STDOUT:\n${stdout}\n`;
12
+ if (stderr)
13
+ output += `STDERR:\n${stderr}\n`;
14
+ if (error)
15
+ output += `ERROR:\n${error.message}\n`;
16
+ if (!output)
17
+ output = "Command executed successfully with no output.";
18
+ resolve(output);
19
+ });
20
+ });
21
+ }
22
+ exports.runTerminalCommandToolDefinition = {
23
+ type: "function",
24
+ function: {
25
+ name: "run_terminal_command",
26
+ description: "Executes a shell/terminal command on the user's host machine. Use this to install packages, run scripts, manage processes, etc.",
27
+ parameters: {
28
+ type: "object",
29
+ properties: {
30
+ command: {
31
+ type: "string",
32
+ description: "The terminal command to execute.",
33
+ }
34
+ },
35
+ required: ["command"],
36
+ },
37
+ },
38
+ };
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installExternalSkillToolDefinition = void 0;
7
+ exports.installExternalSkill = installExternalSkill;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ async function installExternalSkill(url) {
11
+ try {
12
+ const response = await fetch(url);
13
+ if (!response.ok) {
14
+ return `Failed to fetch skill from URL. Status: ${response.status}`;
15
+ }
16
+ const code = await response.text();
17
+ // Extract a filename from URL, or generate a random one
18
+ let filename = url.split('/').pop() || '';
19
+ if (!filename.endsWith('.ts') && !filename.endsWith('.js')) {
20
+ filename = `skill_${Date.now()}.ts`;
21
+ }
22
+ // Ensure external_skills directory exists
23
+ const pluginsDir = path_1.default.join(process.cwd(), 'src', 'external_skills');
24
+ if (!fs_1.default.existsSync(pluginsDir)) {
25
+ fs_1.default.mkdirSync(pluginsDir, { recursive: true });
26
+ }
27
+ const filePath = path_1.default.join(pluginsDir, filename);
28
+ fs_1.default.writeFileSync(filePath, code, 'utf8');
29
+ return `Skill successfully downloaded and installed to ${filePath}. Please restart the server for the plugin manager to compile and load it.`;
30
+ }
31
+ catch (error) {
32
+ return `Failed to install skill: ${error.message}`;
33
+ }
34
+ }
35
+ exports.installExternalSkillToolDefinition = {
36
+ type: "function",
37
+ function: {
38
+ name: "install_external_skill",
39
+ description: "Downloads and installs a third-party typescript skill from a URL (e.g. GitHub Gist raw URL).",
40
+ parameters: {
41
+ type: "object",
42
+ properties: {
43
+ url: {
44
+ type: "string",
45
+ description: "The direct raw URL to the .ts or .js file of the skill.",
46
+ }
47
+ },
48
+ required: ["url"],
49
+ },
50
+ },
51
+ };
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readLocalFileToolDefinition = void 0;
7
+ exports.readLocalFile = readLocalFile;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ function readLocalFile(filePath) {
11
+ try {
12
+ const absolutePath = path_1.default.resolve(filePath);
13
+ if (!fs_1.default.existsSync(absolutePath)) {
14
+ return `Error: File not found at ${absolutePath}`;
15
+ }
16
+ const content = fs_1.default.readFileSync(absolutePath, 'utf8');
17
+ return content;
18
+ }
19
+ catch (error) {
20
+ return `Failed to read file: ${error.message}`;
21
+ }
22
+ }
23
+ exports.readLocalFileToolDefinition = {
24
+ type: "function",
25
+ function: {
26
+ name: "read_local_file",
27
+ description: "Reads the content of a local file on the user's computer.",
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ filePath: {
32
+ type: "string",
33
+ description: "The absolute or relative path to the file.",
34
+ }
35
+ },
36
+ required: ["filePath"],
37
+ },
38
+ },
39
+ };
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.updateSecurityPolicyToolDefinition = void 0;
7
+ exports.updateSecurityPolicy = updateSecurityPolicy;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const paths_1 = require("../../config/paths");
10
+ function updateSecurityPolicy(rule, action) {
11
+ try {
12
+ const policyPath = (0, paths_1.getPath)('security_policy.md');
13
+ let existingContent = "";
14
+ if (fs_1.default.existsSync(policyPath)) {
15
+ existingContent = fs_1.default.readFileSync(policyPath, 'utf8');
16
+ }
17
+ if (action === 'clear') {
18
+ fs_1.default.writeFileSync(policyPath, '', 'utf8');
19
+ return "Security policy cleared.";
20
+ }
21
+ else if (action === 'add') {
22
+ const newContent = existingContent + (existingContent.endsWith('\n') || existingContent === '' ? '' : '\n') + `* ${rule}`;
23
+ fs_1.default.writeFileSync(policyPath, newContent, 'utf8');
24
+ return `Rule added to security policy: ${rule}`;
25
+ }
26
+ else if (action === 'remove') {
27
+ // Very basic line removal
28
+ const lines = existingContent.split('\n');
29
+ const filtered = lines.filter(l => !l.includes(rule));
30
+ fs_1.default.writeFileSync(policyPath, filtered.join('\n'), 'utf8');
31
+ return `Rule removed (if it existed).`;
32
+ }
33
+ return "Invalid action.";
34
+ }
35
+ catch (error) {
36
+ return `Failed to update security policy: ${error.message}`;
37
+ }
38
+ }
39
+ exports.updateSecurityPolicyToolDefinition = {
40
+ type: "function",
41
+ function: {
42
+ name: "update_security_policy",
43
+ description: "Updates the security_policy.md file to restrict your own autonomous behavior. Use this when the user explicitly forbids you from doing something (e.g. 'do not touch drive E').",
44
+ parameters: {
45
+ type: "object",
46
+ properties: {
47
+ rule: {
48
+ type: "string",
49
+ description: "The rule to add or remove.",
50
+ },
51
+ action: {
52
+ type: "string",
53
+ enum: ["add", "remove", "clear"],
54
+ description: "The action to perform on the policy.",
55
+ }
56
+ },
57
+ required: ["rule", "action"],
58
+ },
59
+ },
60
+ };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeLocalFileToolDefinition = void 0;
7
+ exports.writeLocalFile = writeLocalFile;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ function writeLocalFile(filePath, content) {
11
+ try {
12
+ const absolutePath = path_1.default.resolve(filePath);
13
+ const dir = path_1.default.dirname(absolutePath);
14
+ if (!fs_1.default.existsSync(dir)) {
15
+ fs_1.default.mkdirSync(dir, { recursive: true });
16
+ }
17
+ fs_1.default.writeFileSync(absolutePath, content, 'utf8');
18
+ return `Success: File written to ${absolutePath}`;
19
+ }
20
+ catch (error) {
21
+ return `Failed to write file: ${error.message}`;
22
+ }
23
+ }
24
+ exports.writeLocalFileToolDefinition = {
25
+ type: "function",
26
+ function: {
27
+ name: "write_local_file",
28
+ description: "Writes or overwrites a local file on the user's computer with the provided content.",
29
+ parameters: {
30
+ type: "object",
31
+ properties: {
32
+ filePath: {
33
+ type: "string",
34
+ description: "The absolute or relative path to the file.",
35
+ },
36
+ content: {
37
+ type: "string",
38
+ description: "The string content to write to the file.",
39
+ }
40
+ },
41
+ required: ["filePath", "content"],
42
+ },
43
+ },
44
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nyxora",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "dist/gateway/cli.js",
6
6
  "files": [
package/user.md CHANGED
@@ -4,6 +4,6 @@ Every time the agent reads your messages, it will read this file first as its pr
4
4
 
5
5
  Examples:
6
6
 
7
- * "Call me Yudha"
7
+ * "Call me User"
8
8
  * "Use casual and slang language"
9
9
  * "Focus analysis from a technical trading perspective"
@@ -1 +0,0 @@
1
- :root{--bg-color:#0f172a;--bg-secondary:#1e293b;--bg-sidebar:#0b1120;--text-primary:#f8fafc;--text-secondary:#94a3b8;--accent:#3b82f6;--accent-hover:#2563eb;--glass-bg:#1e293bb3;--glass-border:#ffffff1a;--chat-user:#3b82f6;--chat-agent:#1e293b;--tool-bg:#0f172a}*{box-sizing:border-box;margin:0;padding:0}body{background-color:var(--bg-color);color:var(--text-primary);height:100vh;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;overflow:hidden}#root{width:100vw;height:100vh;display:flex}.sidebar{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);border-right:1px solid var(--glass-border);z-index:100;background-color:#0b1120d9;flex-direction:column;width:280px;display:flex;box-shadow:10px 0 30px -10px #00000080}.agent-identity-card{background:linear-gradient(#3b82f60d 0%,#0000 100%);border-bottom:1px solid #ffffff0d;align-items:center;gap:16px;margin-bottom:8px;padding:24px;display:flex}.agent-avatar{background:#3b82f626;border:1px solid #3b82f64d;border-radius:16px;justify-content:center;align-items:center;padding:12px;display:flex;box-shadow:0 0 20px #3b82f633}.agent-info{flex-direction:column;gap:4px;display:flex}.agent-name{color:#fff;letter-spacing:-.025em;font-size:1.1rem;font-weight:700}.agent-status{color:#4ade80;letter-spacing:.05em;align-items:center;gap:6px;font-size:.75rem;font-weight:600;display:flex}.status-dot{background-color:#4ade80;border-radius:50%;width:8px;height:8px;animation:2s infinite pulse-green;box-shadow:0 0 8px #4ade80}@keyframes pulse-green{0%{transform:scale(.95);box-shadow:0 0 #4ade80b3}70%{transform:scale(1);box-shadow:0 0 0 6px #4ade8000}to{transform:scale(.95);box-shadow:0 0 #4ade8000}}.sidebar-scroll-area{flex:1;padding-bottom:24px;overflow-y:auto}.sidebar-scroll-area::-webkit-scrollbar{width:4px}.sidebar-scroll-area::-webkit-scrollbar-thumb{background:#ffffff0d}.sidebar-section{text-transform:uppercase;letter-spacing:.15em;color:#64748b;padding:24px 24px 12px;font-size:.7rem;font-weight:700}.sidebar-nav{flex-direction:column;gap:4px;padding:0 16px;display:flex}.nav-item{color:#94a3b8;cursor:pointer;border:1px solid #0000;border-radius:12px;align-items:center;gap:14px;padding:12px 16px;font-size:.95rem;font-weight:500;transition:all .25s cubic-bezier(.4,0,.2,1);display:flex}.nav-icon{transition:transform .25s cubic-bezier(.4,0,.2,1)}.nav-item:hover{color:#fff;background-color:#ffffff08;transform:translate(4px)}.nav-item:hover .nav-icon{color:var(--accent);transform:scale(1.1)}.nav-item.active{color:#fff;border-left:3px solid var(--accent);background:linear-gradient(90deg,#3b82f626 0%,#0000 100%);border-radius:4px 12px 12px 4px;font-weight:600}.nav-item.active .nav-icon{color:var(--accent)}.main-content{background-image:radial-gradient(at 0 0,#3b82f61a 0,#0000 50%),radial-gradient(at 100% 100%,#8b5cf61a 0,#0000 50%);flex-direction:column;flex:1;display:flex}.topbar{border-bottom:1px solid var(--glass-border);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#0f172a99;justify-content:space-between;align-items:center;height:64px;padding:0 24px;display:flex}.topbar-left{color:var(--text-secondary);align-items:center;gap:16px;font-size:.95rem;font-weight:500;display:flex}.topbar-right{align-items:center;gap:12px;display:flex}.config-dropdown{background:var(--bg-secondary);border:1px solid var(--glass-border);color:#fff;cursor:pointer;border-radius:6px;outline:none;min-width:120px;padding:8px 12px;font-size:.85rem;transition:all .2s}.config-dropdown:hover{border-color:var(--accent)}.config-dropdown:focus{border-color:var(--accent);box-shadow:0 0 0 2px #3b82f633}.workspace-container{width:100%;height:calc(100vh - 64px);display:flex}.chat-wrapper{flex-direction:column;height:100%;padding:24px 0;display:flex}.resizer{background:var(--glass-border);cursor:col-resize;z-index:10;width:6px;transition:background .2s}.resizer:hover,.resizer:active{background:var(--accent)}.canvas-panel{background:var(--bg-sidebar);background-image:radial-gradient(#3b82f60d 0,#0000 80%);flex-direction:column;flex:1;padding:32px;display:flex;position:relative;overflow-y:auto}.canvas-header{border-bottom:1px solid #ffffff0d;justify-content:space-between;align-items:center;margin-bottom:32px;padding-bottom:16px;display:flex}.canvas-title{color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;align-items:center;gap:8px;font-family:monospace;font-size:.85rem;display:flex}.canvas-empty{color:#475569;flex-direction:column;justify-content:center;align-items:center;gap:16px;height:100%;display:flex}.chat-container{flex-direction:column;flex:1;gap:20px;padding:0 24px;display:flex;overflow-y:auto}.chat-container::-webkit-scrollbar{width:6px}.chat-container::-webkit-scrollbar-thumb{background:#ffffff1a;border-radius:4px}.message-wrapper{flex-direction:column;max-width:85%;animation:.3s ease-out forwards fadeIn;display:flex}.message-wrapper.user{align-self:flex-end}.message-wrapper.agent{align-self:flex-start}.message-bubble{border-radius:18px;padding:14px 18px;font-size:.95rem;line-height:1.6;box-shadow:0 4px 6px -1px #0000001a}.user .message-bubble{background-color:var(--chat-user);color:#fff;border-bottom-right-radius:4px}.agent .message-bubble{background-color:var(--chat-agent);border:1px solid var(--glass-border);border-bottom-left-radius:4px}.tool-call{color:var(--text-secondary);background:var(--tool-bg);border:1px solid #ffffff0d;border-radius:12px;align-items:center;gap:8px;margin-top:10px;padding:10px 14px;font-size:.85rem;display:flex}.tool-call code{color:#a78bfa;font-family:monospace}.input-area{padding:20px 24px 0}.input-form{background:var(--bg-secondary);border:1px solid var(--glass-border);border-radius:16px;gap:12px;padding:8px;transition:all .2s;display:flex}.input-form:focus-within{border-color:var(--accent);box-shadow:0 0 0 2px #3b82f633}.chat-input{color:#fff;background:0 0;border:none;outline:none;flex:1;padding:12px 16px;font-family:inherit;font-size:.95rem}.send-button{background:var(--accent);color:#fff;cursor:pointer;border:none;border-radius:12px;justify-content:center;align-items:center;width:44px;height:44px;transition:all .2s;display:flex}.voice-button{background:var(--bg-secondary);color:var(--text-secondary);border:1px solid var(--glass-border);cursor:pointer;border-radius:12px;justify-content:center;align-items:center;width:44px;height:44px;transition:all .2s;display:flex}.voice-button:hover{color:#fff;border-color:#ef4444}.voice-button.listening{color:#ef4444;background:#ef444433;border-color:#ef4444;animation:1.5s infinite pulse-red}.voice-button.active-mode{color:#3b82f6;border-color:#3b82f6}.voice-button.speaking{color:#3b82f6;background:#3b82f633;border-color:#3b82f6;animation:1.5s infinite pulse-blue}@keyframes pulse-red{0%{box-shadow:0 0 #ef444466}70%{box-shadow:0 0 0 10px #ef444400}to{box-shadow:0 0 #ef444400}}@keyframes pulse-blue{0%{box-shadow:0 0 #3b82f666}70%{box-shadow:0 0 0 10px #3b82f600}to{box-shadow:0 0 #3b82f600}}.send-button:hover:not(:disabled){background:var(--accent-hover);transform:scale(1.05)}.send-button:disabled{opacity:.5;cursor:not-allowed}.typing-indicator{background-color:var(--chat-agent);border:1px solid var(--glass-border);border-radius:18px 18px 18px 4px;align-self:flex-start;gap:4px;width:fit-content;padding:14px 18px;display:flex}.dot{background:var(--text-secondary);border-radius:50%;width:6px;height:6px;animation:1.4s ease-in-out infinite both bounce}.dot:first-child{animation-delay:-.32s}.dot:nth-child(2){animation-delay:-.16s}@keyframes bounce{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.overview-container{color:#fff;max-width:1200px;height:calc(100vh - 64px);margin:0 auto;padding:24px;font-family:Inter,sans-serif;overflow-y:auto}.overview-container::-webkit-scrollbar{width:6px}.overview-container::-webkit-scrollbar-thumb{background:#ffffff1a;border-radius:4px}.overview-header h1{margin-bottom:4px;font-size:1.5rem;font-weight:600}.overview-header p{color:var(--text-secondary);margin-bottom:24px;font-size:.9rem}.panel{background:#14182099;border:1px solid #ffffff0d;border-radius:12px;margin-bottom:24px;padding:20px}.panel-header h3{margin-bottom:4px;font-size:1.1rem;font-weight:600}.panel-header p{color:var(--text-secondary);margin-bottom:16px;font-size:.85rem}.form-group{margin-bottom:16px}.form-row{gap:16px;display:flex}.flex-1{flex:1}label{text-transform:uppercase;color:var(--text-secondary);letter-spacing:.05em;margin-bottom:8px;font-size:.75rem;display:block}input,select{color:#fff;background:#0000004d;border:1px solid #ffffff1a;border-radius:8px;width:100%;padding:10px 12px;font-family:monospace;font-size:.9rem}input:read-only{color:var(--text-secondary)}.input-with-icon{align-items:center;display:flex;position:relative}.input-with-icon svg{color:var(--text-secondary);cursor:pointer;position:absolute;right:12px}.form-actions{align-items:center;gap:12px;margin-top:20px;display:flex}.btn-primary{color:#fff;cursor:pointer;background:#3b82f6;border:none;border-radius:6px;padding:8px 16px;font-size:.9rem}.btn-secondary{color:#fff;cursor:pointer;background:0 0;border:1px solid #fff3;border-radius:6px;padding:8px 16px;font-size:.9rem}.action-hint{color:var(--text-secondary);margin-left:8px;font-size:.85rem}.snapshot-grid{grid-template-columns:repeat(4,1fr);gap:20px;display:grid}.stat-val{margin-bottom:4px;font-size:1.5rem;font-weight:700}.text-green{color:#22c55e}.stat-block p{color:var(--text-secondary);margin-top:8px;font-size:.8rem;line-height:1.4}.metrics-grid{grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:32px;display:grid}.metric-card{background:#14182099;border:1px solid #ffffff0d;border-radius:12px;padding:16px}.metric-val{margin-bottom:4px;font-size:1.5rem;font-weight:700}.metric-sub{color:var(--text-secondary);font-size:.75rem}.section-title{text-transform:uppercase;color:var(--text-secondary);letter-spacing:.05em;margin-bottom:12px;font-size:.75rem}.session-item{justify-content:space-between;margin-bottom:24px;padding:16px 20px;display:flex}.text-secondary{color:var(--text-secondary)}.attention-panel{background:#eab3081a;border:1px solid #eab30833;border-radius:12px;flex-direction:column;margin-bottom:24px;padding:16px 20px;display:flex}.attention-header{color:#eab308;align-items:center;gap:8px;margin-bottom:8px;display:flex}.attention-header h4{font-size:1rem;font-weight:600}.attention-content p{margin-bottom:4px;font-size:.95rem}.attention-content span{font-size:.85rem}.logs-grid{grid-template-columns:1fr 1fr;gap:16px;display:grid}.log-panel{background:#0a0c10cc;border:1px solid #ffffff0d;border-radius:12px;flex-direction:column;height:300px;display:flex;overflow:hidden}.log-header{background:#ffffff0d;border-bottom:1px solid #ffffff0d;padding:12px 16px;font-size:.85rem;font-weight:600}.badge{background:#ffffff1a;border-radius:10px;margin-left:8px;padding:2px 6px;font-size:.7rem}.log-content{flex-direction:column;gap:4px;padding:12px 16px;display:flex;overflow-y:auto}.log-content code,.log-json{color:#a3a3a3;word-break:break-all;font-family:Consolas,Monaco,monospace;font-size:.75rem;line-height:1.4}.log-row{gap:12px;margin-bottom:4px;font-family:Consolas,Monaco,monospace;font-size:.75rem;display:flex}.log-time{color:#fb923c;min-width:60px}.log-msg{color:#d1d5db}.log-meta{color:#6b7280}.gateway-row{margin-bottom:2px}