aiden-runtime 3.16.2 → 3.18.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.
@@ -123,7 +123,8 @@ const skillLibrary_1 = require("../core/skillLibrary");
123
123
  const costTracker_1 = require("../core/costTracker");
124
124
  const sessionMemory_1 = require("../core/sessionMemory");
125
125
  const memoryExtractor_1 = require("../core/memoryExtractor");
126
- const pluginSystem_1 = require("../core/pluginSystem");
126
+ const pluginLoader_1 = require("../core/pluginLoader");
127
+ const permissionSystem_1 = require("../core/permissionSystem");
127
128
  const aidenIdentity_1 = require("../core/aidenIdentity");
128
129
  const eventBus_1 = require("../core/eventBus");
129
130
  const workflowTracker_1 = require("../core/workflowTracker");
@@ -150,6 +151,7 @@ const signal_1 = require("../core/channels/signal");
150
151
  const twilio_1 = require("../core/channels/twilio");
151
152
  const imessage_1 = require("../core/channels/imessage");
152
153
  const email_1 = require("../core/channels/email");
154
+ const dashboard_1 = require("./dashboard");
153
155
  // —— Sprint 25: module-level WebSocket clients registry (shared between createApiServer routes and startApiServer WS setup)
154
156
  let wsBroadcastClients = new Set();
155
157
  let activeTelegramBot = null;
@@ -157,97 +159,10 @@ const lastExchangeBySession = new Map();
157
159
  // ── Bookmarklet — clip selected text from any page ────────────
158
160
  const BOOKMARKLET = `javascript:void(fetch('http://localhost:4200/api/clip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:window.getSelection().toString()||document.title,source:window.location.href,title:document.title})}).then(()=>alert('Clipped!')))`;
159
161
  const INSTANT_ACTIONS = [
160
- // 1. Open Chrome
161
- {
162
- patterns: [/^open\s+chrome\s*$/i],
163
- action: async () => {
164
- try {
165
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'chrome' });
166
- }
167
- catch { }
168
- return 'Opening Chrome...';
169
- },
170
- },
171
- // 2. Open Firefox
172
- {
173
- patterns: [/^open\s+firefox\s*$/i],
174
- action: async () => {
175
- try {
176
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'firefox' });
177
- }
178
- catch { }
179
- return 'Opening Firefox...';
180
- },
181
- },
182
- // 3. Open Edge
183
- {
184
- patterns: [/^open\s+(?:microsoft\s+)?edge\s*$/i],
185
- action: async () => {
186
- try {
187
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'msedge' });
188
- }
189
- catch { }
190
- return 'Opening Microsoft Edge...';
191
- },
192
- },
193
- // 4. Open Notepad
194
- {
195
- patterns: [/^open\s+notepad\s*$/i],
196
- action: async () => {
197
- try {
198
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'notepad' });
199
- }
200
- catch { }
201
- return 'Opening Notepad...';
202
- },
203
- },
204
- // 5. Open Calculator
205
- {
206
- patterns: [/^open\s+calc(?:ulator)?\s*$/i],
207
- action: async () => {
208
- try {
209
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'calc' });
210
- }
211
- catch { }
212
- return 'Opening Calculator...';
213
- },
214
- },
215
- // 6. Open VS Code
216
- {
217
- patterns: [/^open\s+(?:vs[\s-]?code|vscode)\s*$/i],
218
- action: async () => {
219
- try {
220
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'code' });
221
- }
222
- catch { }
223
- return 'Opening VS Code...';
224
- },
225
- },
226
- // 7. Open Terminal / CMD
227
- {
228
- patterns: [/^open\s+(?:terminal|cmd|command\s+prompt)\s*$/i],
229
- action: async () => {
230
- try {
231
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'cmd' });
232
- }
233
- catch { }
234
- return 'Opening terminal...';
235
- },
236
- },
237
- // 8. Open File Explorer
238
- {
239
- patterns: [
240
- /^open\s+(?:file\s+)?explorer\s*$/i,
241
- /^open\s+(?:my\s+)?files?\s*$/i,
242
- ],
243
- action: async () => {
244
- try {
245
- await (0, toolRegistry_1.executeTool)('app_launch', { app: 'explorer' });
246
- }
247
- catch { }
248
- return 'Opening File Explorer...';
249
- },
250
- },
162
+ // NOTE: "open X" / "close X" / "launch X" entries removed — they faked success via
163
+ // try/catch swallowing, returned hardcoded strings regardless of tool outcome, and
164
+ // used the wrong param key ({app:} vs {app_name:}). The planner handles these
165
+ // correctly via app_launch / app_close with real success verification.
251
166
  // 9. Take Screenshot
252
167
  {
253
168
  patterns: [
@@ -478,6 +393,13 @@ function initWorkspaceDefaults() {
478
393
  console.log(`[init] Created ${rel}`);
479
394
  }
480
395
  }
396
+ // Copy permissions.yaml from template if not present
397
+ const permTarget = path.join(WORKSPACE_ROOT, 'workspace', 'permissions.yaml');
398
+ const permTemplate = path.join(WORKSPACE_ROOT, 'workspace-templates', 'permissions.yaml');
399
+ if (!fs.existsSync(permTarget) && fs.existsSync(permTemplate)) {
400
+ fs.copyFileSync(permTemplate, permTarget);
401
+ console.log('[init] Created workspace/permissions.yaml from template');
402
+ }
481
403
  }
482
404
  initWorkspaceDefaults();
483
405
  // ── Knowledge upload — multer + progress tracking ─────────────
@@ -530,11 +452,20 @@ function createApiServer() {
530
452
  res.setHeader('X-Frame-Options', 'DENY');
531
453
  next();
532
454
  });
533
- // CORS — allow any origin (dev mode)
455
+ // CORS localhost only by default.
456
+ // Set AIDEN_CORS_ORIGIN=* (or a specific origin) to allow remote access.
457
+ const _corsAllowedOrigin = process.env.AIDEN_CORS_ORIGIN || null;
534
458
  app.use((req, res, next) => {
535
- res.setHeader('Access-Control-Allow-Origin', '*');
536
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
537
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
459
+ const origin = req.headers.origin || '';
460
+ const isLocal = !origin ||
461
+ origin.startsWith('http://localhost') ||
462
+ origin.startsWith('http://127.0.0.1');
463
+ const allowed = _corsAllowedOrigin || (isLocal ? origin || '*' : null);
464
+ if (allowed) {
465
+ res.setHeader('Access-Control-Allow-Origin', allowed);
466
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
467
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
468
+ }
538
469
  if (req.method === 'OPTIONS') {
539
470
  res.sendStatus(200);
540
471
  return;
@@ -542,6 +473,15 @@ function createApiServer() {
542
473
  next();
543
474
  });
544
475
  // ── Core routes ──────────────────────────────────────────────
476
+ // GET /ui — local web dashboard
477
+ app.get('/ui', (_req, res) => {
478
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
479
+ res.send((0, dashboard_1.getDashboardHTML)());
480
+ });
481
+ // GET /api/ping — lightweight status probe for dashboard
482
+ app.get('/api/ping', (_req, res) => {
483
+ res.json({ ok: true, version: version_1.VERSION, ts: Date.now() });
484
+ });
545
485
  // GET /api/health — liveness probe (no auth required)
546
486
  app.get('/api/health', (_req, res) => {
547
487
  res.json({ status: 'ok', version: version_1.VERSION, timestamp: new Date().toISOString() });
@@ -733,7 +673,7 @@ function createApiServer() {
733
673
  res.setHeader('Content-Type', 'text/event-stream');
734
674
  res.setHeader('Cache-Control', 'no-cache');
735
675
  res.setHeader('Connection', 'keep-alive');
736
- res.setHeader('Access-Control-Allow-Origin', '*');
676
+ // CORS already set by global middleware
737
677
  res.flushHeaders();
738
678
  res.write(`data: ${JSON.stringify({ thinking: { stage: 'understanding', message: 'Understanding...' } })}\n\n`);
739
679
  }
@@ -748,7 +688,7 @@ function createApiServer() {
748
688
  res.setHeader('Content-Type', 'text/event-stream');
749
689
  res.setHeader('Cache-Control', 'no-cache');
750
690
  res.setHeader('Connection', 'keep-alive');
751
- res.setHeader('Access-Control-Allow-Origin', '*');
691
+ // CORS already set by global middleware
752
692
  res.flushHeaders();
753
693
  }
754
694
  res.write(`data: ${JSON.stringify({ token: text, done: false, provider: 'fast-path' })}\n\n`);
@@ -1023,35 +963,43 @@ function createApiServer() {
1023
963
  { regex: /(?:search|find|look\s+up)\s+(?:for\s+)?(.+?)\s+on\s+github/i, url: q => `https://github.com/search?q=${encodeURIComponent(q)}`, label: 'GitHub' },
1024
964
  { regex: /open\s+github\s+(?:and\s+)?(?:search|find)\s+(?:for\s+)?(.+)/i, url: q => `https://github.com/search?q=${encodeURIComponent(q)}`, label: 'GitHub' },
1025
965
  ];
1026
- for (const fp of searchFastPaths) {
1027
- const m = message.match(fp.regex);
1028
- if (m) {
1029
- const query = (m[m.length - 1] || '').trim().replace(/[.!?]+$/, '');
1030
- if (query.length > 1) {
1031
- const url = fp.url(query);
1032
- console.log(`[FastPath] ${fp.label} search: “${query}” → ${url}`);
1033
- try {
1034
- await (0, toolRegistry_1.executeTool)('open_browser', { url });
1035
- }
1036
- catch (e) {
1037
- console.warn('[FastPath] open_browser failed, trying shell:', e.message);
966
+ // Play/listen/watch intents must go through the planner so the open_browser
967
+ // auto-chain (toolRegistry.ts) fires and actually starts playback.
968
+ const hasPlayIntent = /\b(play|listen|watch)\b/i.test(message);
969
+ if (hasPlayIntent) {
970
+ console.log('[FastPath] Skipping search fast-paths for play/listen/watch intent — routing to planner');
971
+ }
972
+ if (!hasPlayIntent) {
973
+ for (const fp of searchFastPaths) {
974
+ const m = message.match(fp.regex);
975
+ if (m) {
976
+ const query = (m[m.length - 1] || '').trim().replace(/[.!?]+$/, '');
977
+ if (query.length > 1) {
978
+ const url = fp.url(query);
979
+ console.log(`[FastPath] ${fp.label} search: “${query}” → ${url}`);
1038
980
  try {
1039
- await (0, toolRegistry_1.executeTool)('shell_exec', { command: `start “” “${url}”` });
981
+ await (0, toolRegistry_1.executeTool)('open_browser', { url });
1040
982
  }
1041
- catch { }
1042
- }
1043
- let replyMsg;
1044
- if (fp.label === 'YouTube') {
1045
- replyMsg = `Opening YouTube search for "${query}" — click the first result to play.\n→ ${url}`;
1046
- }
1047
- else if (fp.label === 'DuckDuckGo') {
1048
- replyMsg = `Searching DuckDuckGo for "${query}" — opening results in your browser.\n→ ${url}`;
1049
- }
1050
- else {
1051
- replyMsg = `Opening ${fp.label} in your browser.\n→ ${url}`;
983
+ catch (e) {
984
+ console.warn('[FastPath] open_browser failed, trying shell:', e.message);
985
+ try {
986
+ await (0, toolRegistry_1.executeTool)('shell_exec', { command: `start “” “${url}”` });
987
+ }
988
+ catch { }
989
+ }
990
+ let replyMsg;
991
+ if (fp.label === 'YouTube') {
992
+ replyMsg = `Opening YouTube search for “${query}” — click the first result to play.\n→ ${url}`;
993
+ }
994
+ else if (fp.label === 'DuckDuckGo') {
995
+ replyMsg = `Searching DuckDuckGo for “${query}” — opening results in your browser.\n→ ${url}`;
996
+ }
997
+ else {
998
+ replyMsg = `Opening ${fp.label} in your browser.\n→ ${url}`;
999
+ }
1000
+ fastReply(replyMsg);
1001
+ return;
1052
1002
  }
1053
- fastReply(replyMsg);
1054
- return;
1055
1003
  }
1056
1004
  }
1057
1005
  }
@@ -1097,8 +1045,9 @@ function createApiServer() {
1097
1045
  return;
1098
1046
  }
1099
1047
  // 2. "play X on youtube" / "play X on spotify"
1048
+ // hasPlayIntent guard: these go through the planner so open_browser auto-chain fires.
1100
1049
  const onPlatformMatch = /^play\s+(.+?)\s+on\s+(youtube|spotify)\s*$/i.exec(message);
1101
- if (onPlatformMatch) {
1050
+ if (onPlatformMatch && !hasPlayIntent) {
1102
1051
  const query = onPlatformMatch[1].trim();
1103
1052
  const platform = onPlatformMatch[2].toLowerCase();
1104
1053
  const url = buildMusicUrl(query, platform);
@@ -1380,6 +1329,10 @@ function createApiServer() {
1380
1329
  // ── Callback system — additive layer alongside existing SSE sends ──
1381
1330
  const sid = sessionId || 'default';
1382
1331
  callbackSystem_1.callbacks.emit('session_start', sid, { message }).catch(() => { });
1332
+ // Fire flat-plugin session hooks
1333
+ for (const fn of pluginLoader_1.pluginHooks.onSessionStart) {
1334
+ fn(sid, { message }).catch(() => { });
1335
+ }
1383
1336
  // Forward callback events from other sessions to this SSE connection.
1384
1337
  // The sessionId guard prevents re-sending this session's own emitted events.
1385
1338
  const unsubscribeSSE = callbackSystem_1.callbacks.onAny((payload) => {
@@ -1393,6 +1346,9 @@ function createApiServer() {
1393
1346
  (0, toolRegistry_1.setProgressEmitter)(null);
1394
1347
  unsubscribeSSE();
1395
1348
  callbackSystem_1.callbacks.emit('session_end', sid, {}).catch(() => { });
1349
+ for (const fn of pluginLoader_1.pluginHooks.onSessionEnd) {
1350
+ fn(sid, {}).catch(() => { });
1351
+ }
1396
1352
  (0, memoryDistiller_1.distillSession)(sid).catch(() => { });
1397
1353
  });
1398
1354
  // Sprint 6: tiered model selection
@@ -2661,15 +2617,44 @@ function createApiServer() {
2661
2617
  res.status(500).json({ error: err.message });
2662
2618
  }
2663
2619
  });
2664
- // GET /api/plugins — list loaded community plugins
2620
+ // GET /api/plugins — list all loaded plugins
2665
2621
  app.get('/api/plugins', (_req, res) => {
2666
2622
  try {
2667
- res.json({ plugins: pluginSystem_1.pluginManager.list() });
2623
+ res.json({ plugins: (0, pluginLoader_1.listFlatPlugins)() });
2668
2624
  }
2669
2625
  catch (e) {
2670
2626
  res.status(500).json({ error: e.message });
2671
2627
  }
2672
2628
  });
2629
+ // GET /api/plugins/list — alias for /api/plugins (kept for backward compat)
2630
+ app.get('/api/plugins/list', (_req, res) => {
2631
+ try {
2632
+ res.json({ plugins: (0, pluginLoader_1.listFlatPlugins)() });
2633
+ }
2634
+ catch (e) {
2635
+ res.status(500).json({ error: e.message });
2636
+ }
2637
+ });
2638
+ // POST /api/plugins/reload — hot-reload all flat .js plugins
2639
+ app.post('/api/plugins/reload', requireLocalhost, async (_req, res) => {
2640
+ try {
2641
+ const dir = path.join(process.cwd(), 'workspace', 'plugins');
2642
+ await (0, pluginLoader_1.reloadPlugins)(dir);
2643
+ res.json({ ok: true, plugins: (0, pluginLoader_1.listFlatPlugins)() });
2644
+ }
2645
+ catch (e) {
2646
+ res.status(500).json({ error: e.message });
2647
+ }
2648
+ });
2649
+ // GET /api/permissions/config — return the current parsed permissions config
2650
+ app.get('/api/permissions/config', (_req, res) => {
2651
+ res.json(permissionSystem_1.permissionSystem.getConfig());
2652
+ });
2653
+ // POST /api/permissions/reload — hot-reload workspace/permissions.yaml
2654
+ app.post('/api/permissions/reload', requireLocalhost, (_req, res) => {
2655
+ permissionSystem_1.permissionSystem.reload();
2656
+ res.json({ ok: true, mode: permissionSystem_1.permissionSystem.getMode() });
2657
+ });
2673
2658
  // GET /api/telegram/config — load Telegram bot config
2674
2659
  app.get('/api/telegram/config', (_req, res) => {
2675
2660
  try {
@@ -3067,7 +3052,7 @@ function createApiServer() {
3067
3052
  }
3068
3053
  });
3069
3054
  // POST /api/mcp/servers -- register a new MCP server and discover its tools
3070
- app.post('/api/mcp/servers', async (req, res) => {
3055
+ app.post('/api/mcp/servers', requireLocalhost, async (req, res) => {
3071
3056
  const { name, url, description } = req.body;
3072
3057
  if (!name || !url) {
3073
3058
  res.status(400).json({ error: 'name and url are required' });
@@ -3078,7 +3063,7 @@ function createApiServer() {
3078
3063
  res.json({ server, tools });
3079
3064
  });
3080
3065
  // DELETE /api/mcp/servers/:name -- remove an MCP server
3081
- app.delete('/api/mcp/servers/:name', (req, res) => {
3066
+ app.delete('/api/mcp/servers/:name', requireLocalhost, (req, res) => {
3082
3067
  mcpClient_1.mcpClient.removeServer(String(req.params.name));
3083
3068
  res.json({ success: true });
3084
3069
  });
@@ -3503,7 +3488,7 @@ function createApiServer() {
3503
3488
  res.setHeader('Content-Type', 'text/event-stream');
3504
3489
  res.setHeader('Cache-Control', 'no-cache');
3505
3490
  res.setHeader('Connection', 'keep-alive');
3506
- res.setHeader('Access-Control-Allow-Origin', '*');
3491
+ // CORS already set by global middleware
3507
3492
  res.flushHeaders();
3508
3493
  const ping = setInterval(() => {
3509
3494
  try {
@@ -3738,7 +3723,7 @@ function createApiServer() {
3738
3723
  res.setHeader('Content-Type', 'text/event-stream');
3739
3724
  res.setHeader('Cache-Control', 'no-cache');
3740
3725
  res.setHeader('Connection', 'keep-alive');
3741
- res.setHeader('Access-Control-Allow-Origin', '*');
3726
+ // CORS already set by global middleware
3742
3727
  res.flushHeaders();
3743
3728
  // Send ping every 25s to keep connection alive
3744
3729
  const ping = setInterval(() => {
@@ -4478,7 +4463,7 @@ function createApiServer() {
4478
4463
  }
4479
4464
  });
4480
4465
  // POST /api/skills/migrate — backfill skill.json for skills that are missing it
4481
- app.post('/api/skills/migrate', async (_req, res) => {
4466
+ app.post('/api/skills/migrate', requireLocalhost, async (_req, res) => {
4482
4467
  try {
4483
4468
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
4484
4469
  const path = await Promise.resolve().then(() => __importStar(require('path')));
@@ -4880,12 +4865,12 @@ function createApiServer() {
4880
4865
  });
4881
4866
  });
4882
4867
  // DELETE /api/memory — clear all conversation memory
4883
- app.delete('/api/memory', (_req, res) => {
4868
+ app.delete('/api/memory', requireLocalhost, (_req, res) => {
4884
4869
  conversationMemory_1.conversationMemory.clear();
4885
4870
  res.json({ success: true, message: 'Conversation memory cleared' });
4886
4871
  });
4887
4872
  // POST /api/memory/clear — alias for DELETE (for frontend compatibility)
4888
- app.post('/api/memory/clear', (_req, res) => {
4873
+ app.post('/api/memory/clear', requireLocalhost, (_req, res) => {
4889
4874
  try {
4890
4875
  conversationMemory_1.conversationMemory.clear();
4891
4876
  res.json({ success: true, message: 'All memory cleared' });
@@ -4895,7 +4880,7 @@ function createApiServer() {
4895
4880
  }
4896
4881
  });
4897
4882
  // POST /api/conversations/clear — clear all saved conversation sessions from disk
4898
- app.post('/api/conversations/clear', (_req, res) => {
4883
+ app.post('/api/conversations/clear', requireLocalhost, (_req, res) => {
4899
4884
  try {
4900
4885
  const sessionsDir = path.join(WORKSPACE_ROOT, 'workspace', 'sessions');
4901
4886
  if (fs.existsSync(sessionsDir)) {
@@ -4913,7 +4898,7 @@ function createApiServer() {
4913
4898
  }
4914
4899
  });
4915
4900
  // POST /api/knowledge/clear — clear knowledge base files
4916
- app.post('/api/knowledge/clear', (_req, res) => {
4901
+ app.post('/api/knowledge/clear', requireLocalhost, (_req, res) => {
4917
4902
  try {
4918
4903
  const kbDir = path.join(WORKSPACE_ROOT, 'workspace', 'knowledge');
4919
4904
  if (fs.existsSync(kbDir)) {
@@ -5116,6 +5101,17 @@ function createApiServer() {
5116
5101
  httpReq.end();
5117
5102
  });
5118
5103
  }
5104
+ // ── Localhost-only guard for destructive endpoints ───────────
5105
+ // Applied as middleware to endpoints that must not be reachable
5106
+ // from remote hosts even when AIDEN_HOST=0.0.0.0.
5107
+ function requireLocalhost(req, res, next) {
5108
+ const ip = req.ip || req.socket?.remoteAddress || '';
5109
+ const isLocal = ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
5110
+ if (!isLocal) {
5111
+ return res.status(403).json({ error: 'This endpoint is only accessible from localhost' });
5112
+ }
5113
+ next();
5114
+ }
5119
5115
  // ── API key guard (optional) ─────────────────────────────────
5120
5116
  function _checkApiKey(req, res) {
5121
5117
  const required = process.env.AIDEN_API_KEY;
@@ -5674,13 +5670,24 @@ function startupCheck() {
5674
5670
  }
5675
5671
  // ── Server launcher ───────────────────────────────────────────
5676
5672
  function startApiServer(portArg) {
5673
+ // ── Redirect all diagnostic output to stderr ─────────────────────────────
5674
+ // The CLI writes the streaming response to process.stdout character-by-character.
5675
+ // If console.log also writes to stdout (the default), server logs physically
5676
+ // interleave with rendered tokens in the same terminal, producing output like:
5677
+ // "I'm back. What's up, sh[Router] planner: groq-1...iva?"
5678
+ // Sending ALL diagnostic output to stderr prevents this regardless of how the
5679
+ // user runs the server (same terminal, background process, pipe, etc.).
5680
+ // console.error already targets stderr — leave it alone.
5681
+ const _toStderr = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
5682
+ console.log = _toStderr;
5683
+ console.info = _toStderr;
5684
+ console.warn = _toStderr;
5677
5685
  // Read port from config/api.json with sensible fallback.
5678
- // Host defaults to 0.0.0.0 in headless/Linux mode so the API
5679
- // is reachable from the WSL2 host (Windows) and other LAN clients.
5680
- // Electron mode keeps 127.0.0.1 for security (loopback only).
5686
+ // Host defaults to 127.0.0.1 (loopback only) for security.
5687
+ // Set AIDEN_HOST=0.0.0.0 to expose on all interfaces (e.g. headless/WSL2).
5681
5688
  let port = portArg ?? 4200;
5682
5689
  const isHeadless = process.env.AIDEN_HEADLESS === 'true';
5683
- let host = isHeadless ? '0.0.0.0' : '127.0.0.1';
5690
+ let host = process.env.AIDEN_HOST || (isHeadless ? '0.0.0.0' : '127.0.0.1');
5684
5691
  try {
5685
5692
  const cfgPath = path.join(WORKSPACE_ROOT, 'config', 'api.json');
5686
5693
  if (fs.existsSync(cfgPath)) {
@@ -5849,8 +5856,9 @@ function startApiServer(portArg) {
5849
5856
  catch (e) {
5850
5857
  console.error('[Startup] setupHttpKeepalive failed:', e.message);
5851
5858
  }
5852
- // Load community plugins from workspace/plugins/
5853
- pluginSystem_1.pluginManager.loadAll().catch(e => console.error('[Plugins] Load failed:', e.message));
5859
+ // Load plugins from workspace/plugins/*.js (unified flat format)
5860
+ const flatPluginDir = path.join(process.cwd(), 'workspace', 'plugins');
5861
+ (0, pluginLoader_1.loadPlugins)(flatPluginDir).catch(e => console.error('[PluginLoader] Load failed:', e.message));
5854
5862
  // Start background license refresh (12-hour interval, silent)
5855
5863
  (0, licenseManager_1.startLicenseRefresh)();
5856
5864
  // Log provider chain before listening so it's visible in startup log