aiden-runtime 3.18.0 → 3.19.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,13 +40,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
40
40
  return (mod && mod.__esModule) ? mod : { "default": mod };
41
41
  };
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.TOOL_NAMES_ONLY = exports.TOOL_DESCRIPTIONS = exports.TOOLS = void 0;
43
+ exports.registryMcpDestructiveList = exports.registryMcpSafeList = exports.registrySequentialOnlySet = exports.registryParallelSafeSet = exports.registryNoRetrySet = exports.registryValidTools = exports.registryAllowedTools = exports.registryTimeouts = exports.registryCategories = exports.registryTiers = exports.registryDescriptions = exports.registryNames = exports.TOOL_REGISTRY = exports.TOOL_NAMES_ONLY = exports.TOOL_DESCRIPTIONS = exports.TOOLS = exports.APP_ALIASES = void 0;
44
+ exports.isCommandDenied = isCommandDenied;
45
+ exports.scanCodeForDestructivePaths = scanCodeForDestructivePaths;
46
+ exports.isCommandAllowed = isCommandAllowed;
44
47
  exports.getActiveBrowserPage = getActiveBrowserPage;
45
48
  exports.setProgressEmitter = setProgressEmitter;
49
+ exports.resolveWritePath = resolveWritePath;
50
+ exports.resolveLaunchCommand = resolveLaunchCommand;
46
51
  exports.registerExternalTool = registerExternalTool;
47
52
  exports.getExternalToolsMeta = getExternalToolsMeta;
53
+ exports.isKnownTool = isKnownTool;
48
54
  exports.executeTool = executeTool;
49
55
  exports.getToolTier = getToolTier;
56
+ exports.bumpGeneration = bumpGeneration;
57
+ exports.getGeneration = getGeneration;
50
58
  exports.detectToolCategories = detectToolCategories;
51
59
  exports.getToolsForCategories = getToolsForCategories;
52
60
  // core/toolRegistry.ts — Centralized tool registry with real Playwright
@@ -55,6 +63,7 @@ const child_process_1 = require("child_process");
55
63
  const util_1 = require("util");
56
64
  const fs_1 = __importDefault(require("fs"));
57
65
  const path_1 = __importDefault(require("path"));
66
+ const os_1 = __importDefault(require("os"));
58
67
  const paths_1 = require("./paths");
59
68
  const computerControl_1 = require("./computerControl");
60
69
  const webSearch_1 = require("./webSearch");
@@ -71,6 +80,7 @@ const permissionSystem_1 = require("./permissionSystem");
71
80
  const youtubeTranscript_1 = require("./youtubeTranscript");
72
81
  const knowledgeBase_1 = require("./knowledgeBase");
73
82
  const calendarTool_1 = require("./tools/calendarTool");
83
+ const nowPlaying_1 = require("./tools/nowPlaying");
74
84
  const gmailTool_1 = require("./tools/gmailTool");
75
85
  const index_1 = require("../providers/index");
76
86
  const playwrightBridge_1 = require("./playwrightBridge");
@@ -134,6 +144,13 @@ const DENIED_COMMANDS = [
134
144
  /\bnet\s+user\b/i,
135
145
  /Set-ExecutionPolicy/i,
136
146
  /\bNew-Service\b/i,
147
+ // ── C7: path-scoped deny — Remove-Item on critical system / user paths ──────
148
+ // Belt-and-suspenders: Remove-Item is also removed from SHELL_ALLOWLIST so it
149
+ // requires explicit approval. These patterns hard-block attempts to target
150
+ // system-owned directories regardless of approval state.
151
+ /Remove-Item\b.*[Cc]:[/\\][Uu]sers[/\\]/i,
152
+ /Remove-Item\b.*[Cc]:[/\\][Ww]indows[/\\]/i,
153
+ /Remove-Item\b.*[Cc]:[/\\][Pp]rogram/i,
137
154
  ];
138
155
  function isCommandDenied(cmd) {
139
156
  return DENIED_COMMANDS.some(p => p.test(cmd));
@@ -153,6 +170,60 @@ function isShellDangerous(cmd) {
153
170
  const lower = cmd.toLowerCase();
154
171
  return SHELL_DANGEROUS_PATTERNS.some(p => lower.includes(p.toLowerCase()));
155
172
  }
173
+ // ── C8: Code-level destructive path guard for run_node / run_python ──────────
174
+ // Scans code strings for destructive filesystem operations targeting protected
175
+ // system paths. Closes the bypass where the planner re-routes through run_node
176
+ // or run_python after shell_exec is denied by DENIED_COMMANDS.
177
+ //
178
+ // Two-stage check: (1) code contains a destructive fs call, AND (2) code
179
+ // references a protected path. Both must match for denial — benign code that
180
+ // merely reads protected paths, or destructive code targeting workspace, passes.
181
+ const PROTECTED_PATH_PATTERNS = [
182
+ // {1,2} so we match both real paths (C:\Users\) and string-literal escapes (C:\\Users\\)
183
+ /[Cc]:[/\\]{1,2}[Uu]sers[/\\]{1,2}/,
184
+ /[Cc]:[/\\]{1,2}[Ww]indows[/\\]{1,2}/,
185
+ /[Cc]:[/\\]{1,2}[Pp]rogram\s?[Ff]iles/,
186
+ /[Cc]:[/\\]{1,2}[Ss]ystem/,
187
+ /['"`]\/etc[/'"` ]/,
188
+ /['"`]\/home[/'"` ]/,
189
+ /['"`]\/usr[/'"` ]/,
190
+ /['"`]\/var[/'"` ]/,
191
+ ];
192
+ const CODE_DESTRUCTIVE_NODE = [
193
+ /\bfs\s*\.\s*rmSync\b/,
194
+ /\bfs\s*\.\s*unlinkSync\b/,
195
+ /\bfs\s*\.\s*rmdirSync\b/,
196
+ /\bfs\.promises\s*\.\s*rm\b/,
197
+ /\bfs\.promises\s*\.\s*unlink\b/,
198
+ /\bfs\.promises\s*\.\s*rmdir\b/,
199
+ /\brimraf\b/,
200
+ /\bfs\s*\.\s*rm\s*\(/,
201
+ /\bdel\s*\(/, // fs-extra del()
202
+ /\bunlinkSync\s*\(/, // bare import
203
+ ];
204
+ const CODE_DESTRUCTIVE_PYTHON = [
205
+ /\bos\s*\.\s*remove\b/,
206
+ /\bos\s*\.\s*unlink\b/,
207
+ /\bos\s*\.\s*rmdir\b/,
208
+ /\bos\s*\.\s*removedirs\b/,
209
+ /\bshutil\s*\.\s*rmtree\b/,
210
+ /\bpathlib\b.*\bunlink\b/,
211
+ /\bsend2trash\b/,
212
+ ];
213
+ function scanCodeForDestructivePaths(code, lang) {
214
+ const destructivePatterns = lang === 'node' ? CODE_DESTRUCTIVE_NODE : CODE_DESTRUCTIVE_PYTHON;
215
+ const matchedOp = destructivePatterns.find(p => p.test(code));
216
+ if (!matchedOp)
217
+ return { denied: false, reason: '' };
218
+ const matchedPath = PROTECTED_PATH_PATTERNS.find(p => p.test(code));
219
+ if (!matchedPath)
220
+ return { denied: false, reason: '' };
221
+ const opStr = code.match(matchedOp)?.[0] ?? 'destructive op';
222
+ const pathStr = code.match(matchedPath)?.[0] ?? 'protected path';
223
+ const reason = `[Security] ${lang} code blocked: "${opStr}" targeting "${pathStr}" — destructive operation on protected system path`;
224
+ process.stderr.write(reason + '\n');
225
+ return { denied: true, reason };
226
+ }
156
227
  // ── Sprint 25: Shell command allowlist ────────────────────────
157
228
  // Unknown commands (not in this list) are blocked and require explicit user approval.
158
229
  const SHELL_ALLOWLIST = [
@@ -179,7 +250,9 @@ const SHELL_ALLOWLIST = [
179
250
  // 11. Archive tools
180
251
  /^(tar|zip|unzip|7z|gzip|gunzip)\b/i,
181
252
  // 12. PowerShell safe cmdlets (read, navigate, item management, output)
182
- /^(Get-|Select-|Where-|Sort-|Format-|Out-|Write-Output|Write-Host|ConvertTo-|ConvertFrom-|Measure-|Test-Path|Resolve-Path|Split-Path|Join-Path|Compare-Object|New-Item|Copy-Item|Move-Item|Rename-Item|Remove-Item|Set-Content|Add-Content|Clear-Content|Set-Location|Push-Location|Pop-Location)/i,
253
+ // Note: Remove-Item intentionally absent — falls through to needsApproval:true (C7).
254
+ // Hard-deny for Remove-Item on critical paths is in DENIED_COMMANDS above.
255
+ /^(Get-|Select-|Where-|Sort-|Format-|Out-|Write-Output|Write-Host|ConvertTo-|ConvertFrom-|Measure-|Test-Path|Resolve-Path|Split-Path|Join-Path|Compare-Object|New-Item|Copy-Item|Move-Item|Rename-Item|Set-Content|Add-Content|Clear-Content|Set-Location|Push-Location|Pop-Location)/i,
183
256
  // 13. Instant Actions: lock screen (rundll32) and volume one-liners (powershell -c)
184
257
  /^rundll32\b/i,
185
258
  /^powershell\s+-c\b/i,
@@ -330,6 +403,104 @@ let _emitProgress = null;
330
403
  function setProgressEmitter(fn) {
331
404
  _emitProgress = fn;
332
405
  }
406
+ // ── resolveWritePath ──────────────────────────────────────────
407
+ // Pure path resolver for file_write. Exported for unit tests.
408
+ // Expands shorthands, resolves to absolute, then enforces the
409
+ // allow-list: workspace (cwd), Desktop, Documents.
410
+ // Throws with a clear message if the resolved path falls outside.
411
+ function resolveWritePath(rawPath, opts) {
412
+ const home = opts?.home ?? os_1.default.homedir();
413
+ const cwd = opts?.cwd ?? process.cwd();
414
+ const user = process.env.USERNAME || process.env.USER || os_1.default.userInfo().username || 'User';
415
+ // Expand shorthands
416
+ let p = rawPath
417
+ .replace(/^~[\/\\]/, home + path_1.default.sep)
418
+ .replace(/^Desktop[\/\\]/i, path_1.default.join(home, 'Desktop') + path_1.default.sep)
419
+ .replace(/^C:\\Users\\Aiden\\/i, `C:\\Users\\${user}\\`)
420
+ .replace(/^C:\/Users\/Aiden\//i, `C:/Users/${user}/`);
421
+ // Resolve relative paths against cwd
422
+ const resolved = /^[A-Za-z]:[/\\]/.test(p) || p.startsWith('/')
423
+ ? p
424
+ : path_1.default.join(cwd, p);
425
+ // Allow-list: workspace root, Desktop, Documents
426
+ const allowedRoots = [
427
+ cwd,
428
+ path_1.default.join(home, 'Desktop'),
429
+ path_1.default.join(home, 'Documents'),
430
+ ];
431
+ const norm = (s) => s.toLowerCase().replace(/\//g, '\\').replace(/\\$/, '');
432
+ const nr = norm(resolved);
433
+ const ok = allowedRoots.some(root => {
434
+ const r = norm(root);
435
+ return nr === r || nr.startsWith(r + '\\');
436
+ });
437
+ if (!ok) {
438
+ throw new Error(`Path '${resolved}' is outside allowed write locations. Allowed: workspace, Desktop, Documents.`);
439
+ }
440
+ return resolved;
441
+ }
442
+ exports.APP_ALIASES = {
443
+ spotify: { win32: { type: 'uri', value: 'spotify' }, darwin: { type: 'app', value: 'Spotify' }, linux: { type: 'cmd', value: 'spotify' } },
444
+ chrome: { win32: { type: 'cmd', value: 'chrome' }, darwin: { type: 'app', value: 'Google Chrome' }, linux: { type: 'cmd', value: 'google-chrome' } },
445
+ firefox: { win32: { type: 'cmd', value: 'firefox' }, darwin: { type: 'app', value: 'Firefox' }, linux: { type: 'cmd', value: 'firefox' } },
446
+ edge: { win32: { type: 'cmd', value: 'msedge' }, darwin: { type: 'app', value: 'Microsoft Edge' }, linux: { type: 'cmd', value: 'microsoft-edge' } },
447
+ discord: { win32: { type: 'uri', value: 'discord' }, darwin: { type: 'app', value: 'Discord' }, linux: { type: 'cmd', value: 'discord' } },
448
+ slack: { win32: { type: 'uri', value: 'slack' }, darwin: { type: 'app', value: 'Slack' }, linux: { type: 'cmd', value: 'slack' } },
449
+ zoom: { win32: { type: 'uri', value: 'zoommtg' }, darwin: { type: 'app', value: 'zoom.us' }, linux: { type: 'cmd', value: 'zoom' } },
450
+ teams: { win32: { type: 'uri', value: 'msteams' }, darwin: { type: 'app', value: 'Microsoft Teams' }, linux: { type: 'cmd', value: 'teams' } },
451
+ vscode: { win32: { type: 'cmd', value: 'code' }, darwin: { type: 'app', value: 'Visual Studio Code' }, linux: { type: 'cmd', value: 'code' } },
452
+ notepad: { win32: { type: 'cmd', value: 'notepad.exe' }, darwin: { type: 'app', value: 'TextEdit' }, linux: { type: 'cmd', value: 'gedit' } },
453
+ 'notepad++': { win32: { type: 'cmd', value: 'notepad++' } },
454
+ calculator: { win32: { type: 'cmd', value: 'calc' }, darwin: { type: 'app', value: 'Calculator' }, linux: { type: 'cmd', value: 'gnome-calculator' } },
455
+ paint: { win32: { type: 'cmd', value: 'mspaint' } },
456
+ explorer: { win32: { type: 'cmd', value: 'explorer' }, darwin: { type: 'cmd', value: 'open ~' }, linux: { type: 'cmd', value: 'nautilus' } },
457
+ terminal: { win32: { type: 'cmd', value: 'wt' }, darwin: { type: 'app', value: 'Terminal' }, linux: { type: 'cmd', value: 'gnome-terminal' } },
458
+ word: { win32: { type: 'cmd', value: 'winword' } },
459
+ excel: { win32: { type: 'cmd', value: 'excel' } },
460
+ powershell: { win32: { type: 'cmd', value: 'powershell' } },
461
+ cmd: { win32: { type: 'cmd', value: 'cmd' } },
462
+ };
463
+ // Display name aliases → canonical key in APP_ALIASES
464
+ const DISPLAY_ALIASES = {
465
+ 'google chrome': 'chrome', 'microsoft edge': 'edge',
466
+ 'vs code': 'vscode', 'visual studio code': 'vscode',
467
+ 'microsoft teams': 'teams', 'file explorer': 'explorer',
468
+ 'windows terminal': 'terminal', 'task manager': 'calculator',
469
+ 'calc': 'calculator',
470
+ };
471
+ /**
472
+ * C13: Resolve the shell command to launch an app on a given platform.
473
+ * Pure function — takes platform arg for testability.
474
+ * @param appName - user-facing app name (lowercase, trimmed)
475
+ * @param platform - override for process.platform (for testing)
476
+ */
477
+ function resolveLaunchCommand(appName, platform) {
478
+ const plat = (platform ?? process.platform);
479
+ const canonical = DISPLAY_ALIASES[appName] ?? appName;
480
+ const entry = exports.APP_ALIASES[canonical]?.[plat];
481
+ if (entry) {
482
+ switch (entry.type) {
483
+ case 'uri':
484
+ return plat === 'win32' ? `cmd /c start "" "${entry.value}:"`
485
+ : plat === 'darwin' ? `open "${entry.value}://"`
486
+ : `xdg-open "${entry.value}://"`;
487
+ case 'app':
488
+ return `open -a "${entry.value}"`;
489
+ case 'cmd':
490
+ if (plat === 'win32')
491
+ return `cmd /c start "" "${entry.value}"`;
492
+ if (plat === 'darwin')
493
+ return `open -a "${entry.value}"`;
494
+ return entry.value;
495
+ }
496
+ }
497
+ // Fallback for unknown apps
498
+ if (plat === 'win32')
499
+ return `cmd /c start "" "${appName}"`;
500
+ if (plat === 'darwin')
501
+ return `open -a "${appName}"`;
502
+ return `xdg-open "${appName}"`;
503
+ }
333
504
  // ── Tool implementations ──────────────────────────────────────
334
505
  exports.TOOLS = {
335
506
  // ── respond — direct conversational reply (no external tools needed) ──
@@ -740,17 +911,7 @@ exports.TOOLS = {
740
911
  return { success: false, output: '', error: 'Access denied: protected path. Aiden cannot write credentials, SSH keys, or env files.' };
741
912
  }
742
913
  try {
743
- // Expand Desktop and ~ shorthands, and fix any "Aiden" username to actual system user
744
- const _user = process.env.USERNAME || process.env.USER || require('os').userInfo().username || 'User';
745
- const _home = require('os').homedir();
746
- filePath = filePath
747
- .replace(/^~[\/\\]/i, _home + path_1.default.sep)
748
- .replace(/^Desktop[\/\\]/i, path_1.default.join(_home, 'Desktop') + path_1.default.sep)
749
- .replace(/^C:\\Users\\Aiden\\/i, `C:\\Users\\${_user}\\`)
750
- .replace(/^C:\/Users\/Aiden\//i, `C:/Users/${_user}/`);
751
- const resolved = filePath.match(/^[A-Z]:/i) || filePath.startsWith('/')
752
- ? filePath
753
- : path_1.default.join(process.cwd(), filePath);
914
+ const resolved = resolveWritePath(filePath);
754
915
  fs_1.default.mkdirSync(path_1.default.dirname(resolved), { recursive: true });
755
916
  fs_1.default.writeFileSync(resolved, content, 'utf-8');
756
917
  const written = fs_1.default.existsSync(resolved);
@@ -825,6 +986,10 @@ exports.TOOLS = {
825
986
  const script = p.script || p.code || p.command || '';
826
987
  if (!script)
827
988
  return { success: false, output: '', error: 'No script' };
989
+ // ── C8: Destructive path guard ─────────────────────────────────
990
+ const pyGuard = scanCodeForDestructivePaths(script, 'python');
991
+ if (pyGuard.denied)
992
+ return { success: false, output: '', error: pyGuard.reason };
828
993
  // ── N+34: Docker sandbox routing ───────────────────────────
829
994
  const _pyMode = process.env.AIDEN_SANDBOX_MODE || 'off';
830
995
  if (_pyMode === 'strict' || _pyMode === 'auto') {
@@ -876,6 +1041,10 @@ exports.TOOLS = {
876
1041
  const script = p.script || p.code || p.command || '';
877
1042
  if (!script)
878
1043
  return { success: false, output: '', error: 'No script' };
1044
+ // ── C8: Destructive path guard ─────────────────────────────────
1045
+ const nodeGuard = scanCodeForDestructivePaths(script, 'node');
1046
+ if (nodeGuard.denied)
1047
+ return { success: false, output: '', error: nodeGuard.reason };
879
1048
  const tmp = path_1.default.join(process.cwd(), 'workspace', `js_${Date.now()}.js`);
880
1049
  fs_1.default.mkdirSync(path_1.default.dirname(tmp), { recursive: true });
881
1050
  fs_1.default.writeFileSync(tmp, script);
@@ -905,6 +1074,15 @@ exports.TOOLS = {
905
1074
  return { success: false, output: '', error: e.message };
906
1075
  }
907
1076
  },
1077
+ now_playing: async () => {
1078
+ try {
1079
+ const result = await (0, nowPlaying_1.getNowPlaying)();
1080
+ return { success: true, output: JSON.stringify(result) };
1081
+ }
1082
+ catch (e) {
1083
+ return { success: false, output: '', error: e.message };
1084
+ }
1085
+ },
908
1086
  notify: async (p) => {
909
1087
  const msg = (p.message || p.command || p.title || p.body || '')
910
1088
  .replace(/'/g, '').replace(/"/g, '').replace(/`/g, '').replace(/\$/g, '').trim();
@@ -1481,7 +1659,7 @@ exports.TOOLS = {
1481
1659
  },
1482
1660
  screenshot: async (_p) => {
1483
1661
  try {
1484
- const filepath = await (0, computerControl_1.takeScreenshot)();
1662
+ const filepath = await (0, computerControl_1.takeScreenshot)(_p?.outputPath ? { outputPath: _p.outputPath } : undefined);
1485
1663
  const stats = require('fs').statSync(filepath);
1486
1664
  return { success: true, output: `Screenshot saved: ${filepath} (${Math.round(stats.size / 1024)}kb)`, path: filepath };
1487
1665
  }
@@ -1602,48 +1780,15 @@ exports.TOOLS = {
1602
1780
  const appName = (p.app_name ?? p.appName ?? p.app ?? p.path ?? p.command ?? p.name ?? p.target ?? '').toString().toLowerCase().trim();
1603
1781
  if (!appName)
1604
1782
  return { success: false, output: '', error: 'No app_name provided. Pass app_name e.g. "chrome" or "spotify".' };
1605
- // Map friendly display names → executable/URI names
1606
- const exeMap = {
1607
- 'chrome': 'chrome',
1608
- 'google chrome': 'chrome',
1609
- 'firefox': 'firefox',
1610
- 'edge': 'msedge',
1611
- 'microsoft edge': 'msedge',
1612
- 'spotify': 'spotify',
1613
- 'discord': 'discord',
1614
- 'vscode': 'code',
1615
- 'vs code': 'code',
1616
- 'visual studio code': 'code',
1617
- 'notepad': 'notepad',
1618
- 'notepad++': 'notepad++',
1619
- 'word': 'winword',
1620
- 'excel': 'excel',
1621
- 'powerpoint': 'powerpnt',
1622
- 'slack': 'slack',
1623
- 'zoom': 'zoom',
1624
- 'teams': 'teams',
1625
- 'microsoft teams': 'teams',
1626
- 'explorer': 'explorer',
1627
- 'file explorer': 'explorer',
1628
- 'task manager': 'taskmgr',
1629
- 'taskmgr': 'taskmgr',
1630
- 'calculator': 'calc',
1631
- 'calc': 'calc',
1632
- 'paint': 'mspaint',
1633
- 'terminal': 'wt',
1634
- 'windows terminal': 'wt',
1635
- 'cmd': 'cmd',
1636
- 'powershell': 'powershell',
1637
- };
1638
- const exe = exeMap[appName] ?? appName;
1783
+ // C13: cross-platform launch via resolveLaunchCommand()
1784
+ const cmd = resolveLaunchCommand(appName);
1639
1785
  try {
1640
1786
  const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1641
- // Use cmd /c start — cmd built-in, avoids Start-Process (blocked by permissions)
1642
- execSync(`cmd /c start "" "${exe}"`, { timeout: 10000 });
1643
- return { success: true, output: `Launched: "${appName}"` };
1787
+ execSync(cmd, { timeout: 10000 });
1788
+ return { success: true, output: `Launched: "${appName}" via: ${cmd}` };
1644
1789
  }
1645
1790
  catch (e) {
1646
- return { success: false, output: '', error: e.message };
1791
+ return { success: false, output: '', error: `Failed to launch "${appName}": ${e.message}` };
1647
1792
  }
1648
1793
  },
1649
1794
  app_close: async (p) => {
@@ -2572,12 +2717,18 @@ public class AidenVolSet {
2572
2717
  // ── Plugin-registered tools ───────────────────────────────────
2573
2718
  const externalTools = {};
2574
2719
  const externalToolsMeta = {};
2720
+ // v3.19 Phase 1 — registry generation counter (Hermes run_agent.py:113,159).
2721
+ // Incremented whenever a new external tool is registered so deriver caches
2722
+ // know to recompute. Declared here so registerExternalTool can reference it
2723
+ // before the deriver block (which appears after TOOL_REGISTRY).
2724
+ let _generation = 0;
2575
2725
  function registerExternalTool(name, fn, source) {
2576
2726
  externalTools[name] = async (input) => {
2577
2727
  const r = await fn(input);
2578
2728
  return { success: r.success, output: r.output };
2579
2729
  };
2580
2730
  externalToolsMeta[name] = { source };
2731
+ _generation++; // invalidate deriver caches
2581
2732
  if ((process.env.AIDEN_LOG_LEVEL || 'info') === 'debug') {
2582
2733
  console.log('[ToolRegistry] Plugin "' + source + '" registered tool: ' + name);
2583
2734
  }
@@ -2586,6 +2737,13 @@ function registerExternalTool(name, fn, source) {
2586
2737
  function getExternalToolsMeta() {
2587
2738
  return { ...externalToolsMeta };
2588
2739
  }
2740
+ /** Dynamic tool-existence check that includes both TOOLS (static) and
2741
+ * externalTools (registered at runtime via registerExternalTool / registerSlashMirrorTools).
2742
+ * Use this in the executor instead of the pre-computed ALLOWED_TOOLS constant, which
2743
+ * is frozen at module-load time before mirror tools are registered. */
2744
+ function isKnownTool(name) {
2745
+ return name in exports.TOOLS || name in externalTools;
2746
+ }
2589
2747
  // ── Internal dispatcher — no retry, no timeout ────────────────
2590
2748
  async function runTool(tool, input) {
2591
2749
  // Build per-call context with tool-scoped progress emitter
@@ -2730,6 +2888,7 @@ exports.TOOL_DESCRIPTIONS = {
2730
2888
  run_python: 'Execute a Python script and return stdout/stderr',
2731
2889
  run_node: 'Execute Node.js/JavaScript code and return the output',
2732
2890
  system_info: 'Get system hardware and OS information (CPU, RAM, disk, OS)',
2891
+ now_playing: 'Get the currently playing media (song, artist, app). Calls Windows MediaSession live — always reflects real-time state. Use whenever the user asks what is playing, whether music is paused, or what track is on.',
2733
2892
  notify: 'Send a desktop notification to the user',
2734
2893
  get_stocks: 'Get top gainers, losers, or most active stocks from NSE/BSE',
2735
2894
  get_market_data: 'Get real-time price, change%, and volume for a stock symbol',
@@ -2739,7 +2898,7 @@ exports.TOOL_DESCRIPTIONS = {
2739
2898
  mouse_click: 'Click the mouse at screen coordinates',
2740
2899
  keyboard_type: 'Type text using the keyboard',
2741
2900
  keyboard_press: 'Press a keyboard key or shortcut (e.g. ctrl+c)',
2742
- screenshot: 'Take a screenshot of the entire screen',
2901
+ screenshot: 'Take a screenshot of the entire screen. Optional: outputPath (absolute path, e.g. C:\\Users\\shiva\\Desktop\\shot.png) to save to a specific location; defaults to workspace/screenshots/.',
2743
2902
  screen_read: 'Read and describe the current screen contents',
2744
2903
  vision_loop: 'Autonomously control the computer using vision to complete a goal',
2745
2904
  wait: 'Pause execution for a specified number of milliseconds',
@@ -2778,6 +2937,9 @@ exports.TOOL_DESCRIPTIONS = {
2778
2937
  swarm: 'Run N isolated subagents on the same task in parallel and aggregate their answers via voting or synthesis. Use for high-confidence research where multiple independent perspectives reduce error.',
2779
2938
  send_file_local: 'Send a file to another device on the local network via LocalSend (op: discover | send)',
2780
2939
  receive_file_local: 'Wait for an incoming LocalSend file transfer on the local network',
2940
+ ingest_youtube: 'Download and ingest a YouTube video into memory: transcribes audio, extracts metadata, and stores as a searchable memory entry.',
2941
+ memory_store: 'Persist a fact, preference, or note to permanent memory right now. Use when the user says "remember", "save this", "keep track of", or wants something stored. Pass { fact: "..." }.',
2942
+ memory_forget: 'Remove a fact or preference from permanent memory. Use when the user says "forget", "remove from memory", "delete from memory". Pass { fact: "keyword to match" }.',
2781
2943
  };
2782
2944
  // ── N+28: TOOL_NAMES_ONLY ──────────────────────────────────────
2783
2945
  // One-liner per tool — first sentence of TOOL_DESCRIPTIONS, truncated to 60 chars.
@@ -2804,7 +2966,10 @@ const TOOL_TIERS = {
2804
2966
  get_company_info: 1,
2805
2967
  social_research: 1,
2806
2968
  system_info: 1,
2969
+ now_playing: 1,
2807
2970
  notify: 1,
2971
+ memory_store: 1,
2972
+ memory_forget: 1,
2808
2973
  wait: 1,
2809
2974
  get_briefing: 1,
2810
2975
  get_natural_events: 1,
@@ -2920,6 +3085,7 @@ const TOOL_CATEGORIES = {
2920
3085
  get_natural_events: ['data'],
2921
3086
  notify: ['system'],
2922
3087
  system_info: ['system'],
3088
+ now_playing: ['system'],
2923
3089
  wait: ['system', 'browser', 'screen'],
2924
3090
  clipboard_read: ['system', 'code'],
2925
3091
  clipboard_write: ['system', 'code'],
@@ -2937,6 +3103,8 @@ const TOOL_CATEGORIES = {
2937
3103
  analytics: ['introspection'],
2938
3104
  spend: ['introspection'],
2939
3105
  memory_show: ['introspection', 'memory'],
3106
+ memory_store: ['memory'],
3107
+ memory_forget: ['memory'],
2940
3108
  lessons: ['introspection', 'memory'],
2941
3109
  skills_list: ['introspection'],
2942
3110
  tools_list: ['introspection'],
@@ -2957,6 +3125,607 @@ const TOOL_CATEGORIES = {
2957
3125
  voice_clone: ['voice'],
2958
3126
  voice_design: ['voice'],
2959
3127
  };
3128
+ exports.TOOL_REGISTRY = {
3129
+ // ── Core / response ──────────────────────────────────────────────────────────
3130
+ respond: {
3131
+ description: 'Send a direct conversational response to the user. Use for greetings, capability questions, clarifications, simple factual answers, and anything that does NOT require external tools. This is the default tool when no other tool is needed.',
3132
+ tier: 1, category: ['core'],
3133
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3134
+ mcp: 'safe', // api/mcp.ts:25
3135
+ },
3136
+ manage_goals: {
3137
+ description: 'Track and manage goals and projects. Use when user asks what to work on, mentions a project, deadline, or launch plan. Actions: list, add, update, complete, remove, suggest.',
3138
+ tier: 1, category: ['core'],
3139
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3140
+ mcp: 'safe', // api/mcp.ts:25
3141
+ },
3142
+ compact_context: {
3143
+ description: 'Summarize and compress the current conversation context. Saves session to disk and extracts durable memories. Call when context is getting long.',
3144
+ tier: 1, category: ['core'],
3145
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3146
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3147
+ },
3148
+ run_agent: {
3149
+ description: 'Spawn a sub-agent to complete a sub-goal autonomously',
3150
+ tier: 1, category: ['core'],
3151
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3152
+ mcp: 'destructive', // api/mcp.ts:44
3153
+ },
3154
+ lookup_skill: {
3155
+ description: 'Search learned skills for a matching pattern. Returns the SKILL.md of the best match. Use before planning multi-step tasks to check if Aiden already knows how to do it.',
3156
+ tier: 1, category: ['core'],
3157
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3158
+ mcp: 'safe', // api/mcp.ts:25
3159
+ },
3160
+ lookup_tool_schema: {
3161
+ description: 'Get the full description for a named tool. Call before using an unfamiliar tool.',
3162
+ tier: 1, category: ['core'],
3163
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3164
+ mcp: 'safe', // api/mcp.ts:25
3165
+ },
3166
+ // ── Web / research ───────────────────────────────────────────────────────────
3167
+ web_search: {
3168
+ description: 'Search the web for current information, news, or any topic',
3169
+ tier: 1, category: ['web', 'data'], timeoutMs: 15000,
3170
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3171
+ mcp: 'safe', // api/mcp.ts:25
3172
+ },
3173
+ fetch_url: {
3174
+ description: 'Fetch the content of any URL and return the text',
3175
+ tier: 1, category: ['web'], timeoutMs: 20000,
3176
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3177
+ mcp: 'safe', // api/mcp.ts:25
3178
+ },
3179
+ fetch_page: {
3180
+ description: 'Fetch a web page and extract its readable text content',
3181
+ tier: 1, category: ['web'], timeoutMs: 20000,
3182
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3183
+ mcp: 'safe', // api/mcp.ts:25
3184
+ },
3185
+ deep_research: {
3186
+ description: 'Conduct thorough multi-step research on a topic using multiple sources',
3187
+ tier: 1, category: ['web'], timeoutMs: 60000,
3188
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3189
+ mcp: 'safe', // api/mcp.ts:25
3190
+ },
3191
+ social_research: {
3192
+ description: 'Research a person or company across social and public sources',
3193
+ tier: 1, category: ['web', 'data'], timeoutMs: 30000,
3194
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3195
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3196
+ },
3197
+ ingest_youtube: {
3198
+ description: 'Ingest a YouTube video — downloads transcript or audio and extracts searchable text',
3199
+ tier: 1, category: ['web', 'memory'],
3200
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3201
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3202
+ },
3203
+ // ── Browser ──────────────────────────────────────────────────────────────────
3204
+ open_browser: {
3205
+ description: 'Open a URL in the system browser',
3206
+ tier: 3, category: ['browser'], timeoutMs: 15000,
3207
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3208
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3209
+ mcp: 'safe', // api/mcp.ts:25
3210
+ },
3211
+ browser_click: {
3212
+ description: 'Click on an element in the browser by selector',
3213
+ tier: 3, category: ['browser'], timeoutMs: 10000,
3214
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3215
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3216
+ mcp: 'destructive', // api/mcp.ts:44
3217
+ },
3218
+ browser_scroll: {
3219
+ description: 'Scroll the browser page or a specific element. Params: direction (up|down|top|bottom, default down), amount (pixels, default 500), selector (optional CSS selector to scroll a specific element)',
3220
+ tier: 3, category: ['browser'], timeoutMs: 8000,
3221
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3222
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3223
+ mcp: 'destructive', // api/mcp.ts:44
3224
+ },
3225
+ browser_type: {
3226
+ description: 'Type text into a browser input field',
3227
+ tier: 3, category: ['browser'], timeoutMs: 10000,
3228
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3229
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3230
+ mcp: 'destructive', // api/mcp.ts:44
3231
+ },
3232
+ browser_extract: {
3233
+ description: 'Extract text content from the current browser page',
3234
+ tier: 3, category: ['browser'], timeoutMs: 10000,
3235
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3236
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3237
+ mcp: 'safe', // api/mcp.ts:25
3238
+ },
3239
+ browser_screenshot: {
3240
+ description: 'Take a screenshot of the current browser window',
3241
+ tier: 3, category: ['browser'], timeoutMs: 8000,
3242
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3243
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3244
+ mcp: 'safe', // api/mcp.ts:25
3245
+ },
3246
+ browser_get_url: {
3247
+ description: 'Return the URL currently loaded in the browser',
3248
+ tier: 3, category: ['browser'], timeoutMs: 5000,
3249
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3250
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3251
+ mcp: 'safe', // api/mcp.ts:25
3252
+ },
3253
+ // ── Files ────────────────────────────────────────────────────────────────────
3254
+ file_write: {
3255
+ description: 'Write content to a file at the specified path',
3256
+ tier: 2, category: ['files'],
3257
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3258
+ mcp: 'destructive', // api/mcp.ts:44
3259
+ },
3260
+ file_read: {
3261
+ description: 'Read the contents of a file at the specified path',
3262
+ tier: 2, category: ['files'],
3263
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3264
+ mcp: 'safe', // api/mcp.ts:25
3265
+ },
3266
+ file_list: {
3267
+ description: 'List files in a directory',
3268
+ tier: 2, category: ['files'],
3269
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3270
+ mcp: 'safe', // api/mcp.ts:25
3271
+ },
3272
+ watch_folder: {
3273
+ description: 'Watch a folder and react automatically when new files appear',
3274
+ tier: 2, category: ['files', 'system'], timeoutMs: 10000,
3275
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3276
+ mcp: 'destructive', // api/mcp.ts:44
3277
+ },
3278
+ watch_folder_list: {
3279
+ description: 'List all currently watched folder paths',
3280
+ tier: 2, category: ['files', 'system'], timeoutMs: 5000,
3281
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3282
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3283
+ },
3284
+ // ── Shell / code execution ───────────────────────────────────────────────────
3285
+ shell_exec: {
3286
+ description: 'Execute a shell/PowerShell command and return the output',
3287
+ tier: 2, category: ['code', 'system'], timeoutMs: 30000,
3288
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3289
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3290
+ mcp: 'destructive', // api/mcp.ts:44
3291
+ },
3292
+ run_powershell: {
3293
+ description: 'Run a PowerShell command on Windows',
3294
+ tier: 2, category: ['code', 'system'], timeoutMs: 30000,
3295
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3296
+ mcp: 'destructive', // api/mcp.ts:44
3297
+ },
3298
+ cmd: {
3299
+ description: 'Run a Windows cmd.exe command and return stdout/stderr/exitCode',
3300
+ tier: 2, category: ['code', 'system'], timeoutMs: 30000,
3301
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3302
+ mcp: 'destructive', // api/mcp.ts:44
3303
+ },
3304
+ ps: {
3305
+ description: 'Run a PowerShell command directly (no temp file) and return stdout/stderr/exitCode',
3306
+ tier: 2, category: ['code', 'system'], timeoutMs: 30000,
3307
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3308
+ mcp: 'destructive', // api/mcp.ts:44
3309
+ },
3310
+ wsl: {
3311
+ description: 'Run a bash command inside WSL (Windows Subsystem for Linux); auto-translates C:\\ paths to /mnt/c/',
3312
+ tier: 2, category: ['code', 'system'], timeoutMs: 30000,
3313
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3314
+ mcp: 'destructive', // api/mcp.ts:44
3315
+ },
3316
+ run_python: {
3317
+ description: 'Execute a Python script and return stdout/stderr',
3318
+ tier: 2, category: ['code'], timeoutMs: 60000,
3319
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3320
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3321
+ mcp: 'destructive', // api/mcp.ts:44
3322
+ },
3323
+ run_node: {
3324
+ description: 'Execute Node.js/JavaScript code and return the output',
3325
+ tier: 2, category: ['code'], timeoutMs: 60000,
3326
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3327
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3328
+ mcp: 'destructive', // api/mcp.ts:44
3329
+ },
3330
+ run: {
3331
+ description: 'Run a command or script (generic alias for shell_exec)',
3332
+ tier: 2, category: ['code'],
3333
+ parallel: 'never', // agentLoop.ts:1965 — not in SEQUENTIAL_ONLY
3334
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3335
+ },
3336
+ code_interpreter_python: {
3337
+ description: 'Run Python code in a sandboxed interpreter with data science libraries',
3338
+ tier: 2, category: ['code'], timeoutMs: 35000,
3339
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3340
+ mcp: 'destructive', // api/mcp.ts:44
3341
+ },
3342
+ code_interpreter_node: {
3343
+ description: 'Run Node.js code in a sandboxed interpreter',
3344
+ tier: 2, category: ['code'], timeoutMs: 35000,
3345
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3346
+ mcp: 'destructive', // api/mcp.ts:44
3347
+ },
3348
+ // ── Screen / vision / input ──────────────────────────────────────────────────
3349
+ screenshot: {
3350
+ description: 'Take a screenshot of the entire screen. Optional param: outputPath (absolute path, e.g. C:\\Users\\shiva\\Desktop\\shot.png) — if omitted, saves to workspace/screenshots/.',
3351
+ tier: 4, category: ['screen'], timeoutMs: 10000,
3352
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3353
+ mcp: 'safe', // api/mcp.ts:25
3354
+ },
3355
+ screen_read: {
3356
+ description: 'Read and describe the current screen contents',
3357
+ tier: 4, category: ['screen'],
3358
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3359
+ mcp: 'safe', // api/mcp.ts:25
3360
+ },
3361
+ vision_loop: {
3362
+ description: 'Autonomously control the computer using vision to complete a goal',
3363
+ tier: 4, category: ['screen'], timeoutMs: 120000,
3364
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3365
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3366
+ },
3367
+ vision_analyze: {
3368
+ description: 'Analyze an image file using computer vision and return a structured description',
3369
+ tier: 4, category: ['screen', 'data'], timeoutMs: 45000,
3370
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3371
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3372
+ },
3373
+ mouse_move: {
3374
+ description: 'Move the mouse cursor to screen coordinates',
3375
+ tier: 4, category: ['screen'],
3376
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3377
+ mcp: 'destructive', // api/mcp.ts:44
3378
+ },
3379
+ mouse_click: {
3380
+ description: 'Click the mouse at screen coordinates',
3381
+ tier: 4, category: ['screen'],
3382
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3383
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3384
+ mcp: 'destructive', // api/mcp.ts:44
3385
+ },
3386
+ keyboard_type: {
3387
+ description: 'Type text using the keyboard',
3388
+ tier: 4, category: ['screen'],
3389
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3390
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3391
+ mcp: 'destructive', // api/mcp.ts:44
3392
+ },
3393
+ keyboard_press: {
3394
+ description: 'Press a keyboard key or shortcut (e.g. ctrl+c)',
3395
+ tier: 4, category: ['screen'],
3396
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3397
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3398
+ mcp: 'destructive', // api/mcp.ts:44
3399
+ },
3400
+ // ── Data / market ────────────────────────────────────────────────────────────
3401
+ get_stocks: {
3402
+ description: 'Get top gainers, losers, or most active stocks from NSE/BSE',
3403
+ tier: 1, category: ['data'], timeoutMs: 20000,
3404
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3405
+ mcp: 'safe', // api/mcp.ts:25
3406
+ },
3407
+ get_market_data: {
3408
+ description: 'Get real-time price, change%, and volume for a stock symbol',
3409
+ tier: 1, category: ['data'], timeoutMs: 15000,
3410
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3411
+ mcp: 'safe', // api/mcp.ts:25
3412
+ },
3413
+ get_company_info: {
3414
+ description: 'Get company profile, sector, P/E ratio, EPS, and revenue',
3415
+ tier: 1, category: ['data'], timeoutMs: 15000,
3416
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3417
+ mcp: 'safe', // api/mcp.ts:25
3418
+ },
3419
+ get_briefing: {
3420
+ description: 'Run the morning briefing: weather, markets, news, and daily summary',
3421
+ tier: 1, category: ['data'],
3422
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3423
+ mcp: 'safe', // api/mcp.ts:25
3424
+ },
3425
+ get_natural_events: {
3426
+ description: 'Fetch active natural events from NASA EONET API. Returns current earthquakes, wildfires, storms, floods, and other natural events worldwide.',
3427
+ tier: 1, category: ['data'],
3428
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3429
+ mcp: 'safe', // api/mcp.ts:25
3430
+ },
3431
+ // ── System / OS ──────────────────────────────────────────────────────────────
3432
+ system_info: {
3433
+ description: 'Get system hardware and OS information (CPU, RAM, disk, OS)',
3434
+ tier: 1, category: ['system'],
3435
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3436
+ mcp: 'safe', // api/mcp.ts:25
3437
+ },
3438
+ now_playing: {
3439
+ description: 'Get the currently playing media (song, artist, app). Calls Windows MediaSession live — always reflects real-time state. Use whenever the user asks what is playing, whether music is paused, or what track is on.',
3440
+ tier: 1, category: ['system'],
3441
+ parallel: 'safe', // read-only, no side effects
3442
+ mcp: 'safe',
3443
+ timeoutMs: 5000,
3444
+ },
3445
+ notify: {
3446
+ description: 'Send a desktop notification to the user',
3447
+ tier: 1, category: ['system'],
3448
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3449
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3450
+ mcp: 'destructive', // api/mcp.ts:44
3451
+ },
3452
+ wait: {
3453
+ description: 'Pause execution for a specified number of milliseconds',
3454
+ tier: 1, category: ['system', 'browser', 'screen'], timeoutMs: 6000,
3455
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3456
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3457
+ },
3458
+ clipboard_read: {
3459
+ description: 'Read the current contents of the system clipboard',
3460
+ tier: 2, category: ['system', 'code'], timeoutMs: 5000,
3461
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3462
+ mcp: 'safe', // api/mcp.ts:25
3463
+ },
3464
+ clipboard_write: {
3465
+ description: 'Write text to the system clipboard',
3466
+ tier: 2, category: ['system', 'code'], timeoutMs: 5000,
3467
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3468
+ mcp: 'destructive', // api/mcp.ts:44
3469
+ },
3470
+ window_list: {
3471
+ description: 'List all open windows on the desktop',
3472
+ tier: 3, category: ['browser', 'system'], timeoutMs: 10000,
3473
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3474
+ mcp: 'safe', // api/mcp.ts:25
3475
+ },
3476
+ window_focus: {
3477
+ description: 'Bring a specific window to the foreground by title',
3478
+ tier: 3, category: ['browser', 'system'], timeoutMs: 8000,
3479
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3480
+ mcp: 'destructive', // api/mcp.ts:44
3481
+ },
3482
+ app_launch: {
3483
+ description: 'Launch an application by name or executable path',
3484
+ tier: 3, category: ['browser', 'system'], timeoutMs: 10000,
3485
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3486
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3487
+ mcp: 'destructive', // api/mcp.ts:44
3488
+ },
3489
+ app_close: {
3490
+ description: 'Close an application by window title or process name',
3491
+ tier: 3, category: ['browser', 'system'], timeoutMs: 8000,
3492
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3493
+ retry: false, // agentLoop.ts:1881 NO_RETRY_TOOLS
3494
+ mcp: 'destructive', // api/mcp.ts:44
3495
+ },
3496
+ system_volume: {
3497
+ description: 'Get or set Windows speaker volume (get/up/down/mute/unmute/set)',
3498
+ tier: 2, category: ['system'], timeoutMs: 8000,
3499
+ parallel: 'sequential', // agentLoop.ts:1965 SEQUENTIAL_ONLY
3500
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3501
+ },
3502
+ schedule_reminder: {
3503
+ description: "Schedule a desktop notification reminder. Params: message (string), delaySeconds or delayMs (number), recurring ('hourly'|'daily'|'weekly', optional). op='list' to see pending reminders, op='cancel' with id to cancel one.",
3504
+ tier: 0, category: ['system'],
3505
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3506
+ mcp: 'destructive', // api/mcp.ts:44
3507
+ },
3508
+ // ── Git ──────────────────────────────────────────────────────────────────────
3509
+ git_status: {
3510
+ description: 'Show git status and recent commits for a repository. Provide path parameter for a specific directory.',
3511
+ tier: 2, category: ['git'], timeoutMs: 15000,
3512
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3513
+ mcp: 'safe', // api/mcp.ts:25
3514
+ },
3515
+ git_commit: {
3516
+ description: 'Stage and commit files to a local git repository',
3517
+ tier: 2, category: ['git'], timeoutMs: 30000,
3518
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3519
+ mcp: 'destructive', // api/mcp.ts:44
3520
+ },
3521
+ git_push: {
3522
+ description: 'Push committed changes to a remote git repository',
3523
+ tier: 2, category: ['git'], timeoutMs: 60000,
3524
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3525
+ mcp: 'destructive', // api/mcp.ts:44
3526
+ },
3527
+ // ── Comms / calendar / email ─────────────────────────────────────────────────
3528
+ get_calendar: {
3529
+ description: 'Get upcoming calendar events from Google Calendar (requires iCal URL in Settings → Channels). Parameters: daysAhead (number, default 7).',
3530
+ tier: 1, category: ['data', 'system'],
3531
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3532
+ mcp: 'safe', // api/mcp.ts:25
3533
+ },
3534
+ read_email: {
3535
+ description: 'Read recent unread emails from Gmail (requires App Password in Settings → Channels). Parameters: count (number, default 10), folder (string, default INBOX).',
3536
+ tier: 1, category: ['data', 'system'],
3537
+ parallel: 'safe', // agentLoop.ts:1957 PARALLEL_SAFE
3538
+ mcp: 'safe', // api/mcp.ts:25
3539
+ },
3540
+ send_email: {
3541
+ description: 'Send an email via Gmail (requires App Password in Settings → Channels). Parameters: to (string), subject (string), body (string).',
3542
+ tier: 1, category: ['data', 'system'],
3543
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3544
+ mcp: 'destructive', // api/mcp.ts:44
3545
+ },
3546
+ send_file_local: {
3547
+ description: 'Send a file to another device on the local network via LocalSend (op: discover | send)',
3548
+ tier: 2, category: ['files', 'system'],
3549
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3550
+ mcp: 'destructive', // api/mcp.ts:44
3551
+ },
3552
+ receive_file_local: {
3553
+ description: 'Wait for an incoming LocalSend file transfer on the local network',
3554
+ tier: 2, category: ['files', 'system'],
3555
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3556
+ mcp: 'destructive', // api/mcp.ts:44
3557
+ },
3558
+ // ── Delegation / subagents ───────────────────────────────────────────────────
3559
+ spawn: {
3560
+ description: "Delegate a sub-task to an isolated subagent with its own context and half the remaining iteration budget. Returns the subagent's synthesized answer.",
3561
+ tier: 2, category: ['delegation'],
3562
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3563
+ mcp: 'destructive', // api/mcp.ts:44
3564
+ },
3565
+ spawn_subagent: {
3566
+ description: "Spawn an isolated subagent to handle a parallel sub-task. The subagent runs in its own conversation context with half your remaining iteration budget. Use for: research that would bloat your context, parallel work where you need both results, sandboxed exploration. Returns the subagent's final reply text.",
3567
+ tier: 2, category: ['delegation'],
3568
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3569
+ mcp: 'destructive', // api/mcp.ts:44
3570
+ },
3571
+ swarm: {
3572
+ description: 'Run N isolated subagents on the same task in parallel and aggregate their answers via voting or synthesis. Use for high-confidence research where multiple independent perspectives reduce error.',
3573
+ tier: 2, category: ['delegation'],
3574
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3575
+ mcp: 'destructive', // api/mcp.ts:44
3576
+ },
3577
+ // ── Voice ────────────────────────────────────────────────────────────────────
3578
+ voice_speak: {
3579
+ description: 'Speak text aloud using the TTS provider chain (VoxCPM → Edge TTS → ElevenLabs → SAPI). Accepts text, voice, rate, volume, provider overrides.',
3580
+ tier: 2, category: ['voice'], timeoutMs: 60000,
3581
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3582
+ mcp: 'destructive', // api/mcp.ts:44
3583
+ },
3584
+ voice_transcribe: {
3585
+ description: 'Transcribe an audio file to text using the STT provider chain (Groq Whisper → OpenAI Whisper → Whisper.cpp). Returns { text, provider, durationMs }.',
3586
+ tier: 2, category: ['voice'], timeoutMs: 60000,
3587
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3588
+ mcp: 'safe', // api/mcp.ts:25
3589
+ },
3590
+ voice_clone: {
3591
+ description: 'Clone a voice from a reference audio file and synthesize new text. Requires text and referenceAudioPath. Uses VoxCPM when USE_VOXCPM=1.',
3592
+ tier: 2, category: ['voice'], timeoutMs: 120000,
3593
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3594
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3595
+ },
3596
+ voice_design: {
3597
+ description: 'Design a custom voice from a text description and synthesize text with it. Requires text and voiceDescription. Uses VoxCPM when USE_VOXCPM=1.',
3598
+ tier: 2, category: ['voice'], timeoutMs: 120000,
3599
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3600
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3601
+ },
3602
+ // ── Interaction / UX ─────────────────────────────────────────────────────────
3603
+ clarify: {
3604
+ description: 'Ask the user a clarifying question and wait for their typed response before proceeding',
3605
+ tier: 1, category: ['interaction', 'core'], timeoutMs: 300000,
3606
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3607
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3608
+ },
3609
+ todo: {
3610
+ description: 'Manage the current session todo list — add, check off, or display pending tasks',
3611
+ tier: 1, category: ['interaction', 'core'],
3612
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3613
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3614
+ },
3615
+ memory_store: {
3616
+ description: 'Persist a fact, preference, or observation to permanent memory right now. Use whenever the user says "remember", "save this", "keep track of", or similar. Pass { fact: "the thing to remember" }.',
3617
+ tier: 1, category: ['memory'],
3618
+ parallel: 'never',
3619
+ retry: false, // write operation — don't double-write on retry
3620
+ mcp: 'excluded',
3621
+ },
3622
+ memory_forget: {
3623
+ description: 'Remove a fact or preference from permanent memory. Use when the user says "forget X", "remove X from memory", "delete X from memory". Pass { fact: "keyword to match" }.',
3624
+ tier: 1, category: ['memory'],
3625
+ parallel: 'never',
3626
+ retry: false, // write operation — don't double-delete on retry
3627
+ mcp: 'excluded',
3628
+ },
3629
+ search: {
3630
+ description: 'Search workspace memory, session context, and file system for relevant stored information',
3631
+ tier: 1, category: ['memory', 'introspection'],
3632
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3633
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3634
+ },
3635
+ cronjob: {
3636
+ description: 'Schedule a recurring task using cron-style timing (alias for schedule_reminder with recurring param)',
3637
+ tier: 1, category: ['system', 'core'],
3638
+ parallel: 'never', // agentLoop.ts:1957 — not in PARALLEL_SAFE
3639
+ mcp: 'excluded', // api/mcp.ts — not in SAFE_TOOLS or DESTRUCTIVE_TOOLS
3640
+ },
3641
+ };
3642
+ // ── v3.19 Phase 1, Commit 2: deriver functions ───────────────────────────────
3643
+ // Inspired by Hermes run_agent.py:113,159. Each deriver caches its result and
3644
+ // recomputes only when _generation changes (i.e. when a new external tool is
3645
+ // registered via registerExternalTool). Callers should use these instead of
3646
+ // reading TOOL_REGISTRY directly — Commits 4-6 will swap every hand-maintained
3647
+ // list to call the appropriate deriver.
3648
+ /** Expose the current generation for consumers that manage their own caches. */
3649
+ function bumpGeneration() { _generation++; }
3650
+ function getGeneration() { return _generation; }
3651
+ /** Build a zero-argument memoiser that recomputes whenever _generation changes. */
3652
+ function makeCache(build) {
3653
+ let cached;
3654
+ let cachedGen = -1;
3655
+ return () => {
3656
+ if (cachedGen !== _generation) {
3657
+ cached = build();
3658
+ cachedGen = _generation;
3659
+ }
3660
+ return cached;
3661
+ };
3662
+ }
3663
+ // ── 1. Names ──────────────────────────────────────────────────────────────────
3664
+ /** All core tool names (TOOL_REGISTRY keys only, excludes slash mirrors).
3665
+ * Replaces TOOL_NAMES_ONLY (toolRegistry.ts). */
3666
+ exports.registryNames = makeCache(() => Object.keys(exports.TOOL_REGISTRY));
3667
+ // ── 2. Descriptions ───────────────────────────────────────────────────────────
3668
+ /** Map of name → description string. Falls back to ''.
3669
+ * Replaces TOOL_DESCRIPTIONS (toolRegistry.ts). */
3670
+ exports.registryDescriptions = makeCache(() => Object.fromEntries(Object.entries(exports.TOOL_REGISTRY).map(([n, m]) => [n, m.description ?? ''])));
3671
+ // ── 3. Tiers ──────────────────────────────────────────────────────────────────
3672
+ /** Map of name → ToolTier. Falls back to tier 1.
3673
+ * Replaces TOOL_TIERS (toolRegistry.ts). */
3674
+ exports.registryTiers = makeCache(() => Object.fromEntries(Object.entries(exports.TOOL_REGISTRY).map(([n, m]) => [n, m.tier ?? 1])));
3675
+ // ── 4. Categories ─────────────────────────────────────────────────────────────
3676
+ /** Map of name → ToolCategory[]. Falls back to ['core'].
3677
+ * Replaces TOOL_CATEGORIES (toolRegistry.ts). */
3678
+ exports.registryCategories = makeCache(() => Object.fromEntries(Object.entries(exports.TOOL_REGISTRY).map(([n, m]) => [n, m.category ?? ['core']])));
3679
+ // ── 5. Timeouts ───────────────────────────────────────────────────────────────
3680
+ /** Map of name → timeout in ms. Falls back to 15 000 ms.
3681
+ * Replaces TOOL_TIMEOUTS (toolRegistry.ts). */
3682
+ exports.registryTimeouts = makeCache(() => Object.fromEntries(Object.entries(exports.TOOL_REGISTRY).map(([n, m]) => [n, m.timeoutMs ?? 15000])));
3683
+ // ── 6. Allowed / valid tools ──────────────────────────────────────────────────
3684
+ /** Complete allowed-tool list: TOOL_REGISTRY keys + registered external tools.
3685
+ * Replaces ALLOWED_TOOLS (agentLoop.ts:808) and VALID_TOOLS (agentLoop.ts:1521). */
3686
+ exports.registryAllowedTools = makeCache(() => [
3687
+ ...Object.keys(exports.TOOL_REGISTRY),
3688
+ ...Object.keys(externalTools),
3689
+ ]);
3690
+ // ── 6b. Valid tools ───────────────────────────────────────────────────────────
3691
+ /** Valid-tool list for agent-loop routing: same data as registryAllowedTools,
3692
+ * kept as a separate deriver for independent traceability per call site.
3693
+ * Replaces VALID_TOOLS (agentLoop.ts:1521). */
3694
+ exports.registryValidTools = makeCache(() => [
3695
+ ...Object.keys(exports.TOOL_REGISTRY),
3696
+ ...Object.keys(externalTools),
3697
+ ]);
3698
+ // ── 7. No-retry set ───────────────────────────────────────────────────────────
3699
+ /** Set of tools that must NOT be retried on failure (retry === false).
3700
+ * Replaces NO_RETRY_TOOLS (agentLoop.ts:1881). */
3701
+ exports.registryNoRetrySet = makeCache(() => new Set(Object.entries(exports.TOOL_REGISTRY)
3702
+ .filter(([, m]) => m.retry === false)
3703
+ .map(([n]) => n)));
3704
+ // ── 8. Parallel-safe set ──────────────────────────────────────────────────────
3705
+ /** Set of tools safe to execute in parallel (parallel === 'safe').
3706
+ * Replaces PARALLEL_SAFE (agentLoop.ts:1957). */
3707
+ exports.registryParallelSafeSet = makeCache(() => new Set(Object.entries(exports.TOOL_REGISTRY)
3708
+ .filter(([, m]) => m.parallel === 'safe')
3709
+ .map(([n]) => n)));
3710
+ // ── 9. Sequential-only set ────────────────────────────────────────────────────
3711
+ /** Set of tools that must always run sequentially (parallel === 'sequential').
3712
+ * Replaces SEQUENTIAL_ONLY (agentLoop.ts:1965). */
3713
+ exports.registrySequentialOnlySet = makeCache(() => new Set(Object.entries(exports.TOOL_REGISTRY)
3714
+ .filter(([, m]) => m.parallel === 'sequential')
3715
+ .map(([n]) => n)));
3716
+ // ── 10. MCP safe list ─────────────────────────────────────────────────────────
3717
+ /** Tools safe to expose via MCP (mcp === 'safe').
3718
+ * Replaces SAFE_TOOLS (api/mcp.ts:25). */
3719
+ exports.registryMcpSafeList = makeCache(() => Object.entries(exports.TOOL_REGISTRY)
3720
+ .filter(([, m]) => m.mcp === 'safe')
3721
+ .map(([n]) => n));
3722
+ // ── 11. MCP destructive list ──────────────────────────────────────────────────
3723
+ /** Tools exposed via MCP but flagged destructive (mcp === 'destructive').
3724
+ * Replaces DESTRUCTIVE_TOOLS (api/mcp.ts:44). */
3725
+ exports.registryMcpDestructiveList = makeCache(() => Object.entries(exports.TOOL_REGISTRY)
3726
+ .filter(([, m]) => m.mcp === 'destructive')
3727
+ .map(([n]) => n));
3728
+ // ─────────────────────────────────────────────────────────────────────────────
2960
3729
  function detectToolCategories(message) {
2961
3730
  const categories = new Set(['core']);
2962
3731
  const msg = message.toLowerCase();
@@ -2972,7 +3741,9 @@ function detectToolCategories(message) {
2972
3741
  categories.add('screen');
2973
3742
  if (/stock|nifty|market|price|nse|bse|sensex|reliance|trading|shares|equity|briefing|weather|natural|earthquake/i.test(msg))
2974
3743
  categories.add('data');
2975
- if (/notify|notification|remind|alert|system info|cpu|ram|disk|hardware|clipboard|launch|close app/i.test(msg))
3744
+ if (/email|inbox|mail|gmail|unread|read_email|send_email|calendar|meetings|events/i.test(msg))
3745
+ categories.add('data');
3746
+ if (/notify|notification|remind|alert|system info|cpu|ram|disk|hardware|clipboard|launch|close app|now.?playing|what.*playing|what.*song|what.*music|is.*playing|music.*paused|current.*track/i.test(msg))
2976
3747
  categories.add('system');
2977
3748
  if (/voice|speak|say aloud|listen|record audio|tts|text.to.speech|transcribe|speech.to.text|clone.*voice|voice.*design|voice.*clone|design.*voice/i.test(msg))
2978
3749
  categories.add('voice');