aiden-runtime 3.16.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.
Files changed (159) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +465 -0
  3. package/config/devos.config.json +186 -0
  4. package/config/hardware.json +9 -0
  5. package/config/model-selection.json +7 -0
  6. package/config/setup-complete.json +20 -0
  7. package/dist/api/routes/computerUse.js +112 -0
  8. package/dist/api/server.js +6870 -0
  9. package/dist/bin/npx-init.js +71 -0
  10. package/dist/coordination/commandGate.js +115 -0
  11. package/dist/coordination/livePulse.js +127 -0
  12. package/dist/core/agentLoop.js +2718 -0
  13. package/dist/core/agentShield.js +231 -0
  14. package/dist/core/aidenIdentity.js +215 -0
  15. package/dist/core/aidenPersonality.js +166 -0
  16. package/dist/core/aidenSdk.js +374 -0
  17. package/dist/core/asyncTasks.js +82 -0
  18. package/dist/core/auditTrail.js +61 -0
  19. package/dist/core/auxiliaryClient.js +114 -0
  20. package/dist/core/bgLLM.js +108 -0
  21. package/dist/core/bm25.js +68 -0
  22. package/dist/core/callbackSystem.js +64 -0
  23. package/dist/core/channels/adapter.js +6 -0
  24. package/dist/core/channels/discord.js +173 -0
  25. package/dist/core/channels/email.js +253 -0
  26. package/dist/core/channels/imessage.js +164 -0
  27. package/dist/core/channels/manager.js +96 -0
  28. package/dist/core/channels/signal.js +140 -0
  29. package/dist/core/channels/slack.js +139 -0
  30. package/dist/core/channels/twilio.js +144 -0
  31. package/dist/core/channels/webhook.js +186 -0
  32. package/dist/core/channels/whatsapp.js +185 -0
  33. package/dist/core/clarifyBus.js +75 -0
  34. package/dist/core/codeInterpreter.js +82 -0
  35. package/dist/core/computerControl.js +439 -0
  36. package/dist/core/conversationMemory.js +334 -0
  37. package/dist/core/costTracker.js +221 -0
  38. package/dist/core/cronManager.js +217 -0
  39. package/dist/core/deepKB.js +77 -0
  40. package/dist/core/doctor.js +279 -0
  41. package/dist/core/dreamEngine.js +334 -0
  42. package/dist/core/entityGraph.js +169 -0
  43. package/dist/core/eventBus.js +16 -0
  44. package/dist/core/evolutionAnalyzer.js +153 -0
  45. package/dist/core/executionLoop.js +309 -0
  46. package/dist/core/executor.js +224 -0
  47. package/dist/core/failureAnalyzer.js +166 -0
  48. package/dist/core/fastPathExpansion.js +82 -0
  49. package/dist/core/faultEngine.js +106 -0
  50. package/dist/core/featureGates.js +70 -0
  51. package/dist/core/fileIngestion.js +113 -0
  52. package/dist/core/gateway.js +97 -0
  53. package/dist/core/goalTracker.js +75 -0
  54. package/dist/core/growthEngine.js +168 -0
  55. package/dist/core/hardwareDetector.js +98 -0
  56. package/dist/core/hooks.js +45 -0
  57. package/dist/core/httpKeepalive.js +46 -0
  58. package/dist/core/hybridSearch.js +101 -0
  59. package/dist/core/importers.js +164 -0
  60. package/dist/core/instinctSystem.js +223 -0
  61. package/dist/core/knowledgeBase.js +351 -0
  62. package/dist/core/learningMemory.js +121 -0
  63. package/dist/core/lessonsBrowser.js +125 -0
  64. package/dist/core/licenseManager.js +399 -0
  65. package/dist/core/logBuffer.js +85 -0
  66. package/dist/core/machineId.js +87 -0
  67. package/dist/core/mcpClient.js +442 -0
  68. package/dist/core/memoryDistiller.js +165 -0
  69. package/dist/core/memoryExtractor.js +212 -0
  70. package/dist/core/memoryIds.js +213 -0
  71. package/dist/core/memoryPreamble.js +113 -0
  72. package/dist/core/memoryQuery.js +136 -0
  73. package/dist/core/memoryRecall.js +140 -0
  74. package/dist/core/memoryStrategy.js +201 -0
  75. package/dist/core/messageValidator.js +85 -0
  76. package/dist/core/modelDiscovery.js +108 -0
  77. package/dist/core/modelRouter.js +118 -0
  78. package/dist/core/morningBriefing.js +203 -0
  79. package/dist/core/multiGoalValidator.js +51 -0
  80. package/dist/core/parallelExecutor.js +43 -0
  81. package/dist/core/passiveSkillObserver.js +204 -0
  82. package/dist/core/paths.js +57 -0
  83. package/dist/core/patternDetector.js +83 -0
  84. package/dist/core/planResponseRepair.js +64 -0
  85. package/dist/core/planTool.js +111 -0
  86. package/dist/core/playwrightBridge.js +356 -0
  87. package/dist/core/pluginSystem.js +121 -0
  88. package/dist/core/privateMode.js +85 -0
  89. package/dist/core/reactLoop.js +156 -0
  90. package/dist/core/recipeEngine.js +166 -0
  91. package/dist/core/responseCache.js +128 -0
  92. package/dist/core/runSandbox.js +132 -0
  93. package/dist/core/sandboxRunner.js +200 -0
  94. package/dist/core/scheduler.js +543 -0
  95. package/dist/core/secretScanner.js +49 -0
  96. package/dist/core/semanticMemory.js +223 -0
  97. package/dist/core/sessionMemory.js +259 -0
  98. package/dist/core/sessionRouter.js +91 -0
  99. package/dist/core/sessionSearch.js +163 -0
  100. package/dist/core/setupWizard.js +225 -0
  101. package/dist/core/skillImporter.js +303 -0
  102. package/dist/core/skillLibrary.js +144 -0
  103. package/dist/core/skillLoader.js +471 -0
  104. package/dist/core/skillTeacher.js +352 -0
  105. package/dist/core/skillValidator.js +210 -0
  106. package/dist/core/skillWriter.js +384 -0
  107. package/dist/core/slashAsTool.js +226 -0
  108. package/dist/core/spawnManager.js +197 -0
  109. package/dist/core/statusVerbs.js +43 -0
  110. package/dist/core/swarmManager.js +109 -0
  111. package/dist/core/taskQueue.js +119 -0
  112. package/dist/core/taskRecovery.js +128 -0
  113. package/dist/core/taskState.js +168 -0
  114. package/dist/core/telegramBot.js +152 -0
  115. package/dist/core/todoManager.js +70 -0
  116. package/dist/core/toolNameRepair.js +71 -0
  117. package/dist/core/toolRegistry.js +2730 -0
  118. package/dist/core/tools/calendarTool.js +98 -0
  119. package/dist/core/tools/companyFilingsTool.js +98 -0
  120. package/dist/core/tools/gmailTool.js +87 -0
  121. package/dist/core/tools/marketDataTool.js +135 -0
  122. package/dist/core/tools/socialResearchTool.js +121 -0
  123. package/dist/core/truthCheck.js +57 -0
  124. package/dist/core/updateChecker.js +74 -0
  125. package/dist/core/userCognitionProfile.js +238 -0
  126. package/dist/core/userProfile.js +341 -0
  127. package/dist/core/version.js +5 -0
  128. package/dist/core/visionAnalyze.js +161 -0
  129. package/dist/core/voice/audio.js +187 -0
  130. package/dist/core/voice/stt.js +226 -0
  131. package/dist/core/voice/tts.js +310 -0
  132. package/dist/core/voiceInput.js +118 -0
  133. package/dist/core/voiceOutput.js +130 -0
  134. package/dist/core/webSearch.js +326 -0
  135. package/dist/core/workflowTracker.js +72 -0
  136. package/dist/core/workspaceMemory.js +54 -0
  137. package/dist/core/youtubeTranscript.js +224 -0
  138. package/dist/integrations/computerUse/apiRegistry.js +113 -0
  139. package/dist/integrations/computerUse/screenAgent.js +203 -0
  140. package/dist/integrations/computerUse/visionLoop.js +296 -0
  141. package/dist/memory/memoryLayers.js +143 -0
  142. package/dist/providers/boa.js +93 -0
  143. package/dist/providers/cerebras.js +70 -0
  144. package/dist/providers/custom.js +89 -0
  145. package/dist/providers/gemini.js +82 -0
  146. package/dist/providers/groq.js +92 -0
  147. package/dist/providers/index.js +149 -0
  148. package/dist/providers/nvidia.js +70 -0
  149. package/dist/providers/ollama.js +99 -0
  150. package/dist/providers/openrouter.js +74 -0
  151. package/dist/providers/router.js +497 -0
  152. package/dist/providers/types.js +6 -0
  153. package/dist/security/browserVault.js +129 -0
  154. package/dist/security/dataGuard.js +89 -0
  155. package/dist/tools/eonetTool.js +72 -0
  156. package/dist/types/computerUse.js +2 -0
  157. package/dist/types/executor.js +2 -0
  158. package/dist-bundle/cli.js +357859 -0
  159. package/package.json +256 -0
@@ -0,0 +1,2730 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // DevOS — Autonomous AI Execution System
4
+ // Copyright (c) 2026 Shiva Deore. All rights reserved.
5
+ // ============================================================
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.TOOL_NAMES_ONLY = exports.TOOL_DESCRIPTIONS = exports.TOOLS = void 0;
44
+ exports.getActiveBrowserPage = getActiveBrowserPage;
45
+ exports.setProgressEmitter = setProgressEmitter;
46
+ exports.registerExternalTool = registerExternalTool;
47
+ exports.getExternalToolsMeta = getExternalToolsMeta;
48
+ exports.executeTool = executeTool;
49
+ exports.getToolTier = getToolTier;
50
+ exports.detectToolCategories = detectToolCategories;
51
+ exports.getToolsForCategories = getToolsForCategories;
52
+ // core/toolRegistry.ts — Centralized tool registry with real Playwright
53
+ // browser automation, file I/O, shell exec, and web utilities.
54
+ const child_process_1 = require("child_process");
55
+ const util_1 = require("util");
56
+ const fs_1 = __importDefault(require("fs"));
57
+ const path_1 = __importDefault(require("path"));
58
+ const paths_1 = require("./paths");
59
+ const computerControl_1 = require("./computerControl");
60
+ const webSearch_1 = require("./webSearch");
61
+ const conversationMemory_1 = require("./conversationMemory");
62
+ const minimatch_1 = __importDefault(require("minimatch"));
63
+ const morningBriefing_1 = require("./morningBriefing");
64
+ const marketDataTool_1 = require("./tools/marketDataTool");
65
+ const companyFilingsTool_1 = require("./tools/companyFilingsTool");
66
+ const mcpClient_1 = require("./mcpClient");
67
+ const codeInterpreter_1 = require("./codeInterpreter");
68
+ const sandboxRunner_1 = require("./sandboxRunner");
69
+ const responseCache_1 = require("./responseCache");
70
+ const youtubeTranscript_1 = require("./youtubeTranscript");
71
+ const knowledgeBase_1 = require("./knowledgeBase");
72
+ const calendarTool_1 = require("./tools/calendarTool");
73
+ const gmailTool_1 = require("./tools/gmailTool");
74
+ const index_1 = require("../providers/index");
75
+ const playwrightBridge_1 = require("./playwrightBridge");
76
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
77
+ // ── Shared path normalizer ─────────────────────────────────────
78
+ function normalizeFilePath(filePath) {
79
+ return filePath.replace(/\\/g, '/');
80
+ }
81
+ // ── Protected files — cannot be written by agents ─────────────
82
+ // GOALS.md is here to prevent arbitrary file_write overwrites.
83
+ // The manage_goals tool writes it directly (bypasses file_write),
84
+ // so goal management still works — only uncontrolled writes are blocked.
85
+ const PROTECTED_FILES = [
86
+ 'config/devos.config.json',
87
+ 'workspace/STANDING_ORDERS.md',
88
+ 'workspace/SOUL.md',
89
+ 'workspace/USER.md',
90
+ 'workspace/HEARTBEAT.md',
91
+ 'workspace/GOALS.md',
92
+ '.env',
93
+ '.env.local',
94
+ 'tsconfig.json',
95
+ 'package.json',
96
+ 'vitest.config.ts',
97
+ 'jest.config.ts',
98
+ ];
99
+ function isProtectedFile(filePath) {
100
+ const normalized = normalizeFilePath(filePath).replace(/^\.\//, '');
101
+ // Block test/config file writes (prevents agents from cheating tests)
102
+ if (normalized.endsWith('.test.ts') || normalized.endsWith('.spec.ts'))
103
+ return true;
104
+ if (normalized.endsWith('vitest.config.ts') || normalized.endsWith('jest.config.ts'))
105
+ return true;
106
+ return PROTECTED_FILES.some(f => normalized.endsWith(f) || normalized === f);
107
+ }
108
+ // ── Path deny rules ───────────────────────────────────────────
109
+ const DENIED_PATHS = [
110
+ '**/.ssh/**', '**/.aws/**', '**/.env*', '**/.gnupg/**',
111
+ '**/credentials*', '**/*.pem', '**/*.key',
112
+ '**/id_rsa*', '**/id_ed25519*',
113
+ ];
114
+ function isPathDenied(filePath) {
115
+ const normalized = normalizeFilePath(filePath);
116
+ return DENIED_PATHS.some(pattern => (0, minimatch_1.default)(normalized, pattern, { dot: true }));
117
+ }
118
+ // ── Command deny rules ────────────────────────────────────────
119
+ const DENIED_COMMANDS = [
120
+ /curl\s+.*\|\s*bash/i,
121
+ /wget\s+.*\|\s*bash/i,
122
+ /rm\s+-rf\s+\//,
123
+ /powershell.*-enc\s/i,
124
+ /powershell.*-encodedcommand/i,
125
+ /iex\s*\(/i,
126
+ /Invoke-Expression/i,
127
+ // ── Sprint 25: extended deny patterns ─────────────────────────
128
+ /Invoke-WebRequest.*\|/i,
129
+ /Start-Process\s/i,
130
+ /\breg\s+(add|delete)/i,
131
+ /\bschtasks\s/i,
132
+ /\bwmic\s+process\s+call/i,
133
+ /\bnet\s+user\b/i,
134
+ /Set-ExecutionPolicy/i,
135
+ /\bNew-Service\b/i,
136
+ ];
137
+ function isCommandDenied(cmd) {
138
+ return DENIED_COMMANDS.some(p => p.test(cmd));
139
+ }
140
+ // ── Sprint 24: active folder-watcher registry ─────────────────
141
+ const activeWatchers = new Map();
142
+ // ── CommandGate: dangerous shell command patterns ──────────────
143
+ const SHELL_DANGEROUS_PATTERNS = [
144
+ 'rm -rf', 'rm -r /', 'del /f /s', 'del /s /q',
145
+ 'format c:', 'format c :', 'diskpart',
146
+ 'shutdown /s', 'shutdown -s',
147
+ 'reg delete', 'reg add hklm',
148
+ 'remove-item -recurse -force', 'remove-item -force -recurse',
149
+ 'format-volume', 'clear-disk', 'stop-computer', 'restart-computer',
150
+ ];
151
+ function isShellDangerous(cmd) {
152
+ const lower = cmd.toLowerCase();
153
+ return SHELL_DANGEROUS_PATTERNS.some(p => lower.includes(p.toLowerCase()));
154
+ }
155
+ // ── Sprint 25: Shell command allowlist ────────────────────────
156
+ // Unknown commands (not in this list) are blocked and require explicit user approval.
157
+ const SHELL_ALLOWLIST = [
158
+ // 1. File system reads
159
+ /^(ls|dir|cat|type|head|tail|more|less|pwd|tree)\b/i,
160
+ // 2. File/dir create, copy, move, shell navigation
161
+ /^(mkdir|md|cp|copy|mv|move|xcopy|robocopy|echo|touch|cd|cls|clear|set|export)\b/i,
162
+ // 3. Git
163
+ /^git\b/i,
164
+ // 4. Node / npm / npx / yarn / pnpm / bun
165
+ /^(node|npm|npx|yarn|pnpm|bun)\b/i,
166
+ // 5. Python / pip
167
+ /^(python|python3|pip|pip3)\b/i,
168
+ // 6. TypeScript compiler, linting, test runners
169
+ /^(tsc|eslint|prettier|ts-node|vitest|jest|mocha)\b/i,
170
+ // 7. Build tools: Cargo, Go, dotnet
171
+ /^(cargo|go|dotnet)\b/i,
172
+ // 8. Text search & manipulation
173
+ /^(grep|rg|find|sed|awk|sort|uniq|wc|cut|tr|jq)\b/i,
174
+ // 9. Network info (read-only; curl/wget pipe-to-bash blocked by denylist above)
175
+ /^(ping|nslookup|tracert|traceroute|curl|wget)\b/i,
176
+ // 10. System info (read-only)
177
+ /^(systeminfo|tasklist|whoami|ipconfig|hostname|ver|uname|df|du|free|ps|top)\b/i,
178
+ // 11. Archive tools
179
+ /^(tar|zip|unzip|7z|gzip|gunzip)\b/i,
180
+ // 12. PowerShell safe cmdlets (read, navigate, item management, output)
181
+ /^(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,
182
+ // 13. Instant Actions: lock screen (rundll32) and volume one-liners (powershell -c)
183
+ /^rundll32\b/i,
184
+ /^powershell\s+-c\b/i,
185
+ ];
186
+ function isCommandAllowed(cmd) {
187
+ // Hard-block: denylist and dangerous patterns take priority
188
+ if (isCommandDenied(cmd))
189
+ return { allowed: false, needsApproval: false };
190
+ if (isShellDangerous(cmd))
191
+ return { allowed: false, needsApproval: false };
192
+ // Allowlist: explicitly permitted command patterns
193
+ const trimmed = cmd.trim();
194
+ if (SHELL_ALLOWLIST.some(p => p.test(trimmed)))
195
+ return { allowed: true, needsApproval: false };
196
+ // Unknown command pattern — require explicit user approval
197
+ return { allowed: false, needsApproval: true };
198
+ }
199
+ // ── Browser profile isolation ────────────────────────────────
200
+ // Each Aiden session uses a sandboxed Chromium profile — completely
201
+ // separate from the user's real Chrome cookies and login state.
202
+ const BROWSER_DATA_DIR = path_1.default.join((0, paths_1.getUserDataDir)(), 'browser-profiles');
203
+ function getBrowserProfileDir(sessionId) {
204
+ const id = sessionId || `session_${Date.now()}`;
205
+ const profileDir = path_1.default.join(BROWSER_DATA_DIR, id);
206
+ if (!fs_1.default.existsSync(profileDir)) {
207
+ fs_1.default.mkdirSync(profileDir, { recursive: true });
208
+ }
209
+ return profileDir;
210
+ }
211
+ function cleanOldBrowserProfiles() {
212
+ if (!fs_1.default.existsSync(BROWSER_DATA_DIR))
213
+ return;
214
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 h
215
+ try {
216
+ for (const entry of fs_1.default.readdirSync(BROWSER_DATA_DIR)) {
217
+ const fullPath = path_1.default.join(BROWSER_DATA_DIR, entry);
218
+ try {
219
+ const stat = fs_1.default.statSync(fullPath);
220
+ if (stat.mtimeMs < cutoff) {
221
+ fs_1.default.rmSync(fullPath, { recursive: true, force: true });
222
+ console.log(`[Browser] Cleaned old profile: ${entry}`);
223
+ }
224
+ }
225
+ catch { }
226
+ }
227
+ }
228
+ catch { }
229
+ }
230
+ // Clean stale profiles at module load (non-blocking, errors silently ignored)
231
+ try {
232
+ cleanOldBrowserProfiles();
233
+ }
234
+ catch { }
235
+ // ── Singleton Playwright browser context (isolated profile) ──
236
+ // LEGACY: Direct Playwright management moved to core/playwrightBridge.ts (N+48).
237
+ // Kept here commented for reference — delete after one sprint if bridge is stable.
238
+ //
239
+ // let browserContext: any = null
240
+ // let activeBrowserPage: any = null
241
+ // let browserIdleTimer: any = null
242
+ // function resetBrowserIdleTimer(): void { ... }
243
+ // export function getActiveBrowserPage(): any { return activeBrowserPage }
244
+ // async function getBrowserContext(): Promise<any> { ... }
245
+ /** Returns the currently active Playwright page (delegated to bridge). */
246
+ function getActiveBrowserPage() {
247
+ return (0, playwrightBridge_1.getActiveBrowserPage)();
248
+ }
249
+ // ── Per-tool timeouts (ms) ────────────────────────────────────
250
+ const TOOL_TIMEOUTS = {
251
+ web_search: 15000,
252
+ deep_research: 60000,
253
+ fetch_url: 20000,
254
+ fetch_page: 20000,
255
+ run_python: 60000,
256
+ run_node: 60000,
257
+ shell_exec: 30000,
258
+ run_powershell: 30000,
259
+ cmd: 30000,
260
+ ps: 30000,
261
+ wsl: 30000,
262
+ screenshot: 10000,
263
+ vision_loop: 120000,
264
+ open_browser: 15000,
265
+ browser_extract: 10000,
266
+ browser_screenshot: 8000,
267
+ browser_click: 10000,
268
+ browser_scroll: 8000,
269
+ browser_type: 10000,
270
+ browser_get_url: 5000,
271
+ git_push: 60000,
272
+ git_commit: 30000,
273
+ git_status: 15000,
274
+ wait: 6000,
275
+ get_stocks: 20000,
276
+ get_market_data: 15000,
277
+ get_company_info: 15000,
278
+ social_research: 30000,
279
+ code_interpreter_python: 35000,
280
+ code_interpreter_node: 35000,
281
+ clipboard_read: 5000,
282
+ clipboard_write: 5000,
283
+ window_list: 10000,
284
+ window_focus: 8000,
285
+ app_launch: 10000,
286
+ app_close: 8000,
287
+ watch_folder: 10000,
288
+ watch_folder_list: 5000,
289
+ clarify: 300000, // up to 5 min for human response
290
+ vision_analyze: 45000,
291
+ voice_speak: 60000,
292
+ voice_transcribe: 60000,
293
+ voice_clone: 120000,
294
+ voice_design: 120000,
295
+ };
296
+ // ── NSE symbol normalizer ─────────────────────────────────────
297
+ // Yahoo Finance needs '^NSEI' for NIFTY and '.NS' suffix for NSE stocks.
298
+ function normalizeNSESymbol(symbol) {
299
+ const nseMap = {
300
+ 'NIFTY': '^NSEI',
301
+ 'NIFTY 50': '^NSEI',
302
+ 'NIFTY50': '^NSEI',
303
+ 'BANKNIFTY': '^NSEBANK',
304
+ 'BANK NIFTY': '^NSEBANK',
305
+ 'SENSEX': '^BSESN',
306
+ };
307
+ const upper = symbol.toUpperCase().trim();
308
+ if (nseMap[upper])
309
+ return nseMap[upper];
310
+ // Bare Indian ticker (all caps, no dot/caret suffix) → add .NS
311
+ if (/^[A-Z]{2,20}$/.test(upper))
312
+ return upper + '.NS';
313
+ return symbol;
314
+ }
315
+ // Module-level progress emitter — set once per SSE request by server.ts,
316
+ // cleared on connection close (same pattern as setStatusEmitter in agentLoop).
317
+ let _emitProgress = null;
318
+ function setProgressEmitter(fn) {
319
+ _emitProgress = fn;
320
+ }
321
+ // ── Tool implementations ──────────────────────────────────────
322
+ exports.TOOLS = {
323
+ // ── respond — direct conversational reply (no external tools needed) ──
324
+ respond: async (p) => {
325
+ const message = p.message || p.text || p.response || '';
326
+ if (!message)
327
+ return { success: false, output: '', error: 'No message provided' };
328
+ return { success: true, output: message };
329
+ },
330
+ open_browser: async (p) => {
331
+ const url = p.url || p.command || '';
332
+ if (!url)
333
+ return { success: false, output: '', error: 'No URL provided' };
334
+ const r = await (0, playwrightBridge_1.pwNavigate)(url);
335
+ if (r.ok)
336
+ return { success: true, output: `Opened browser: ${r.url}` };
337
+ // Playwright failed — fall back to system browser open
338
+ // (Legacy path: activeBrowserPage = null; openBrowser(url))
339
+ try {
340
+ const result = await (0, computerControl_1.openBrowser)(url);
341
+ return { success: true, output: result };
342
+ }
343
+ catch (e2) {
344
+ return { success: false, output: '', error: r.error ?? e2.message };
345
+ }
346
+ },
347
+ browser_screenshot: async () => {
348
+ const r = await (0, playwrightBridge_1.pwScreenshot)();
349
+ if (r.ok)
350
+ return { success: true, output: `Screenshot saved: ${r.path}` };
351
+ return { success: false, output: '', error: r.error };
352
+ },
353
+ // ── browser_click — routes through playwrightBridge ──────────────────────
354
+ //
355
+ // Semantic target shortcuts (pass target: 'first_result'):
356
+ // YouTube search → a#video-title (waits for JS render)
357
+ // Google search → div.g h3 a (first organic result)
358
+ // DuckDuckGo → article[data-testid="result"] h2 a
359
+ //
360
+ // For all other selectors: waits for the element to be visible before clicking.
361
+ browser_click: async (p) => {
362
+ const rawTarget = p.target || p.selector || p.text || p.command || '';
363
+ if (rawTarget === 'first_result') {
364
+ const r = await (0, playwrightBridge_1.pwClickFirstResult)();
365
+ if (r.ok)
366
+ return { success: true, output: `Clicked first result → ${r.url ?? ''}` };
367
+ return { success: false, output: '', error: r.error };
368
+ }
369
+ const r = await (0, playwrightBridge_1.pwClick)(rawTarget);
370
+ if (r.ok)
371
+ return { success: true, output: `Clicked: ${rawTarget}` };
372
+ return { success: false, output: '', error: r.error };
373
+ },
374
+ browser_scroll: async (p) => {
375
+ const direction = p.direction || 'down';
376
+ const amount = typeof p.amount === 'number' ? p.amount : 500;
377
+ const selector = p.selector;
378
+ const r = await (0, playwrightBridge_1.pwScroll)(direction, amount, selector);
379
+ if (r.ok)
380
+ return { success: true, output: selector ? `Scrolled ${direction} ${selector}` : `Scrolled ${direction} by ${amount}px` };
381
+ return { success: false, output: '', error: r.error };
382
+ },
383
+ // ── browser_get_url — return the URL of the current browser page ──────────
384
+ browser_get_url: async () => {
385
+ const r = await (0, playwrightBridge_1.pwGetUrl)();
386
+ if (r.ok)
387
+ return { success: true, output: r.url ?? '' };
388
+ return { success: false, output: '', error: r.error };
389
+ },
390
+ // ── LocalSend LAN file transfer ───────────────────────────────
391
+ send_file_local: async (p) => {
392
+ const base = 'http://localhost:53317';
393
+ // Check LocalSend is running
394
+ try {
395
+ await fetch(`${base}/api/v2/info`, { signal: AbortSignal.timeout(2000) });
396
+ }
397
+ catch {
398
+ return {
399
+ success: false,
400
+ output: '',
401
+ error: 'LocalSend is not running. Start LocalSend and try again.',
402
+ install: 'https://localsend.org',
403
+ };
404
+ }
405
+ if (p.op === 'discover') {
406
+ try {
407
+ const res = await fetch(`${base}/api/v2/devices`);
408
+ const devices = await res.json();
409
+ return { success: true, output: JSON.stringify(devices, null, 2) };
410
+ }
411
+ catch (e) {
412
+ return { success: false, output: '', error: e.message };
413
+ }
414
+ }
415
+ if (p.op === 'send') {
416
+ if (!p.filePath && !p.text) {
417
+ return { success: false, output: '', error: 'filePath or text required for send op' };
418
+ }
419
+ const receiver = p.device ? `--receiver "${p.device}"` : '';
420
+ const cmd = p.filePath
421
+ ? `localsend_cli --file "${p.filePath}" ${receiver}`.trim()
422
+ : `localsend_cli --text "${p.text}" ${receiver}`.trim();
423
+ try {
424
+ const result = await execAsync(cmd, { timeout: 30000 });
425
+ return { success: true, output: result.stdout || 'Sent.' };
426
+ }
427
+ catch (e) {
428
+ return { success: false, output: '', error: e.message };
429
+ }
430
+ }
431
+ return { success: false, output: '', error: `Unknown op "${p.op}". Use discover or send.` };
432
+ },
433
+ receive_file_local: async (p) => {
434
+ const base = 'http://localhost:53317';
435
+ const timeout = (p.timeout_seconds || 60) * 1000;
436
+ const saveTo = p.save_to || 'workspace/downloads/';
437
+ // Check LocalSend is running
438
+ try {
439
+ await fetch(`${base}/api/v2/info`, { signal: AbortSignal.timeout(2000) });
440
+ }
441
+ catch {
442
+ return { success: false, output: '', error: 'LocalSend is not running. Start LocalSend and try again.' };
443
+ }
444
+ // Poll for incoming transfers
445
+ const start = Date.now();
446
+ while (Date.now() - start < timeout) {
447
+ try {
448
+ const res = await fetch(`${base}/api/v2/pending`);
449
+ if (res.ok) {
450
+ const pending = await res.json();
451
+ if (pending?.files?.length > 0) {
452
+ await fetch(`${base}/api/v2/accept`, { method: 'POST' });
453
+ return {
454
+ success: true,
455
+ output: `Received ${pending.files.length} file(s) → ${saveTo}`,
456
+ files: pending.files,
457
+ savedTo: saveTo,
458
+ };
459
+ }
460
+ }
461
+ }
462
+ catch { /* ignore poll errors, keep waiting */ }
463
+ await new Promise(r => setTimeout(r, 2000));
464
+ }
465
+ return { success: false, output: '', error: `Timeout — no transfer received in ${p.timeout_seconds || 60}s` };
466
+ },
467
+ browser_type: async (p) => {
468
+ const selector = p.selector || 'input';
469
+ const text = p.text || p.command || '';
470
+ const r = await (0, playwrightBridge_1.pwType)(selector, text);
471
+ if (r.ok)
472
+ return { success: true, output: `Typed "${text}" into ${selector}` };
473
+ return { success: false, output: '', error: r.error };
474
+ },
475
+ browser_extract: async (_p, ctx) => {
476
+ ctx?.emitProgress?.('extracting page content...');
477
+ const r = await (0, playwrightBridge_1.pwSnapshot)();
478
+ if (r.ok) {
479
+ ctx?.emitProgress?.(`extracted ${(r.text ?? '').length.toLocaleString()} chars`);
480
+ return { success: true, output: r.text ?? '' };
481
+ }
482
+ return { success: false, output: '', error: r.error };
483
+ },
484
+ shell_exec: async (p, ctx) => {
485
+ const cmd = p.command || p.cmd || '';
486
+ if (!cmd)
487
+ return { success: false, output: '', error: 'No command' };
488
+ const shellGate = isCommandAllowed(cmd);
489
+ if (!shellGate.allowed) {
490
+ if (shellGate.needsApproval) {
491
+ console.warn(`[AllowList] shell_exec UNKNOWN — approval required: ${cmd.slice(0, 120)}`);
492
+ return { success: false, output: '', error: `CommandGate: This command requires explicit user approval before running: ${cmd.slice(0, 80)}` };
493
+ }
494
+ console.warn(`[Security] shell_exec DENIED: ${cmd.slice(0, 120)}`);
495
+ return { success: false, output: '', error: 'Blocked: this command pattern is not allowed. Dangerous operations require explicit user approval.' };
496
+ }
497
+ // ── N+34: Docker sandbox routing ───────────────────────────
498
+ const _sandboxMode = process.env.AIDEN_SANDBOX_MODE || 'off';
499
+ if (_sandboxMode === 'strict' || _sandboxMode === 'auto') {
500
+ try {
501
+ const sr = await (0, sandboxRunner_1.runInDockerSandbox)({ command: cmd, type: 'shell', timeout: 30000 });
502
+ const out = (sr.stdout + (sr.stderr ? `\nstderr: ${sr.stderr}` : '')).trim() || '(completed)';
503
+ return { success: sr.exitCode === 0, output: out, error: sr.exitCode !== 0 ? `Exit ${sr.exitCode}` : undefined };
504
+ }
505
+ catch (sandboxErr) {
506
+ if (_sandboxMode === 'strict')
507
+ return { success: false, output: '', error: `[Sandbox] ${sandboxErr.message}` };
508
+ console.warn('[Sandbox] auto-mode fell back to host:', sandboxErr.message);
509
+ }
510
+ }
511
+ // ── Host execution — streaming spawn ───────────────────────
512
+ const showProgress = process.env.AIDEN_SHOW_TOOL_OUTPUT !== 'false';
513
+ return new Promise((resolve) => {
514
+ const proc = (0, child_process_1.spawn)('powershell.exe', ['-Command', cmd], {
515
+ cwd: process.cwd(),
516
+ env: { ...process.env, PATH: process.env.PATH },
517
+ });
518
+ let stdout = '';
519
+ let stderr = '';
520
+ proc.stdout.on('data', (data) => {
521
+ const text = data.toString();
522
+ stdout += text;
523
+ if (showProgress && ctx?.emitProgress) {
524
+ for (const line of text.split('\n')) {
525
+ const trimmed = line.trim();
526
+ if (trimmed)
527
+ ctx.emitProgress(trimmed.slice(0, 100));
528
+ }
529
+ }
530
+ });
531
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
532
+ const timer = setTimeout(() => {
533
+ proc.kill();
534
+ resolve({ success: false, output: stdout.trim(), error: 'timeout' });
535
+ }, (p.timeout_seconds || 30) * 1000);
536
+ proc.on('close', (code) => {
537
+ clearTimeout(timer);
538
+ const out = (stdout || stderr || '').trim() || '(completed)';
539
+ resolve({
540
+ success: (code ?? 1) === 0,
541
+ output: out,
542
+ error: (code ?? 1) !== 0 ? `Exit ${code}` : undefined,
543
+ });
544
+ });
545
+ proc.on('error', (e) => {
546
+ clearTimeout(timer);
547
+ resolve({ success: false, output: '', error: e.message });
548
+ });
549
+ });
550
+ },
551
+ run_powershell: async (p) => {
552
+ const script = p.script || p.command || '';
553
+ if (!script)
554
+ return { success: false, output: '', error: 'No script' };
555
+ const psGate = isCommandAllowed(script);
556
+ if (!psGate.allowed) {
557
+ if (psGate.needsApproval) {
558
+ console.warn(`[AllowList] run_powershell UNKNOWN — approval required: ${script.slice(0, 120)}`);
559
+ return { success: false, output: '', error: `CommandGate: This PowerShell command requires explicit user approval before running.` };
560
+ }
561
+ console.warn(`[Security] run_powershell DENIED: ${script.slice(0, 120)}`);
562
+ return { success: false, output: '', error: 'Blocked: this command pattern is not allowed. Dangerous operations require explicit user approval.' };
563
+ }
564
+ const tmpFile = path_1.default.join(process.cwd(), 'workspace', `tmp_${Date.now()}.ps1`);
565
+ fs_1.default.mkdirSync(path_1.default.dirname(tmpFile), { recursive: true });
566
+ fs_1.default.writeFileSync(tmpFile, script);
567
+ try {
568
+ const { stdout, stderr } = await execAsync(`powershell.exe -ExecutionPolicy Bypass -File "${tmpFile}"`, { timeout: 30000 });
569
+ return { success: true, output: (stdout || stderr || '').trim() };
570
+ }
571
+ catch (e) {
572
+ return { success: false, output: '', error: e.message };
573
+ }
574
+ finally {
575
+ try {
576
+ fs_1.default.unlinkSync(tmpFile);
577
+ }
578
+ catch { }
579
+ }
580
+ },
581
+ // ── cmd — Windows cmd.exe shell ────────────────────────────
582
+ cmd: async (p) => {
583
+ const command = p.command || p.cmd || '';
584
+ if (!command)
585
+ return { success: false, output: '', error: 'No command provided' };
586
+ const gate = isCommandAllowed(command);
587
+ if (!gate.allowed) {
588
+ if (gate.needsApproval) {
589
+ console.warn(`[AllowList] cmd UNKNOWN — approval required: ${command.slice(0, 120)}`);
590
+ return { success: false, output: '', error: `CommandGate: This command requires explicit user approval before running: ${command.slice(0, 80)}` };
591
+ }
592
+ console.warn(`[Security] cmd DENIED: ${command.slice(0, 120)}`);
593
+ return { success: false, output: '', error: 'Blocked: this command pattern is not allowed. Dangerous operations require explicit user approval.' };
594
+ }
595
+ try {
596
+ const { stdout, stderr } = await execAsync(`cmd.exe /c ${command}`, {
597
+ timeout: 30000,
598
+ cwd: process.cwd(),
599
+ env: { ...process.env },
600
+ });
601
+ const out = (stdout || stderr || '').trim();
602
+ return { success: true, output: out || '(completed)', exitCode: 0 };
603
+ }
604
+ catch (e) {
605
+ return { success: false, output: e.stdout || '', error: e.message, exitCode: e.code ?? 1 };
606
+ }
607
+ },
608
+ // ── ps — PowerShell (direct, no temp file) ──────────────────
609
+ ps: async (p) => {
610
+ const command = p.command || p.script || '';
611
+ if (!command)
612
+ return { success: false, output: '', error: 'No command provided' };
613
+ const gate = isCommandAllowed(command);
614
+ if (!gate.allowed) {
615
+ if (gate.needsApproval) {
616
+ console.warn(`[AllowList] ps UNKNOWN — approval required: ${command.slice(0, 120)}`);
617
+ return { success: false, output: '', error: `CommandGate: This PowerShell command requires explicit user approval before running.` };
618
+ }
619
+ console.warn(`[Security] ps DENIED: ${command.slice(0, 120)}`);
620
+ return { success: false, output: '', error: 'Blocked: this command pattern is not allowed. Dangerous operations require explicit user approval.' };
621
+ }
622
+ try {
623
+ const { stdout, stderr } = await execAsync(`powershell.exe -NoProfile -NonInteractive -Command "${command.replace(/"/g, '\\"')}"`, { timeout: 30000, cwd: process.cwd() });
624
+ const out = (stdout || stderr || '').trim();
625
+ return { success: true, output: out || '(completed)', exitCode: 0 };
626
+ }
627
+ catch (e) {
628
+ return { success: false, output: e.stdout || '', error: e.message, exitCode: e.code ?? 1 };
629
+ }
630
+ },
631
+ // ── wsl — Windows Subsystem for Linux ───────────────────────
632
+ wsl: async (p) => {
633
+ const command = p.command || p.cmd || '';
634
+ const distro = p.distro || '';
635
+ if (!command)
636
+ return { success: false, output: '', error: 'No command provided' };
637
+ const gate = isCommandAllowed(command);
638
+ if (!gate.allowed) {
639
+ if (gate.needsApproval) {
640
+ console.warn(`[AllowList] wsl UNKNOWN — approval required: ${command.slice(0, 120)}`);
641
+ return { success: false, output: '', error: `CommandGate: This WSL command requires explicit user approval before running.` };
642
+ }
643
+ console.warn(`[Security] wsl DENIED: ${command.slice(0, 120)}`);
644
+ return { success: false, output: '', error: 'Blocked: this command pattern is not allowed. Dangerous operations require explicit user approval.' };
645
+ }
646
+ // Translate Windows paths in the command: C:\foo\bar → /mnt/c/foo/bar
647
+ const translated = command.replace(/([A-Z]):\\([^\s"']*)/gi, (_m, drive, rest) => `/mnt/${drive.toLowerCase()}/${rest.replace(/\\/g, '/')}`);
648
+ const distroFlag = distro ? `-d ${distro}` : '';
649
+ const wslCmd = `wsl ${distroFlag} -- bash -c "${translated.replace(/"/g, '\\"')}"`;
650
+ try {
651
+ const { stdout, stderr } = await execAsync(wslCmd, {
652
+ timeout: 30000,
653
+ cwd: process.cwd(),
654
+ });
655
+ const out = (stdout || stderr || '').trim();
656
+ return { success: true, output: out || '(completed)', exitCode: 0 };
657
+ }
658
+ catch (e) {
659
+ return { success: false, output: e.stdout || '', error: e.message, exitCode: e.code ?? 1 };
660
+ }
661
+ },
662
+ file_write: async (p) => {
663
+ let filePath = p.path || p.file || '';
664
+ const content = p.content || '';
665
+ if (!filePath)
666
+ return { success: false, output: '', error: 'No path' };
667
+ if (isProtectedFile(filePath)) {
668
+ console.warn(`[Security] file_write BLOCKED (protected): ${filePath}`);
669
+ return { success: false, output: '', error: `Protected file: ${filePath} cannot be modified by agents. Use 'devos config' or edit manually.` };
670
+ }
671
+ if (isPathDenied(filePath)) {
672
+ console.warn(`[Security] file_write DENIED: ${filePath}`);
673
+ return { success: false, output: '', error: 'Access denied: protected path. Aiden cannot write credentials, SSH keys, or env files.' };
674
+ }
675
+ try {
676
+ // Expand Desktop and ~ shorthands, and fix any "Aiden" username to actual system user
677
+ const _user = process.env.USERNAME || process.env.USER || require('os').userInfo().username || 'User';
678
+ const _home = require('os').homedir();
679
+ filePath = filePath
680
+ .replace(/^~[\/\\]/i, _home + path_1.default.sep)
681
+ .replace(/^Desktop[\/\\]/i, path_1.default.join(_home, 'Desktop') + path_1.default.sep)
682
+ .replace(/^C:\\Users\\Aiden\\/i, `C:\\Users\\${_user}\\`)
683
+ .replace(/^C:\/Users\/Aiden\//i, `C:/Users/${_user}/`);
684
+ const resolved = filePath.match(/^[A-Z]:/i) || filePath.startsWith('/')
685
+ ? filePath
686
+ : path_1.default.join(process.cwd(), filePath);
687
+ fs_1.default.mkdirSync(path_1.default.dirname(resolved), { recursive: true });
688
+ fs_1.default.writeFileSync(resolved, content, 'utf-8');
689
+ const written = fs_1.default.existsSync(resolved);
690
+ return {
691
+ success: written,
692
+ output: written
693
+ ? `Written and verified: ${resolved} (${content.length} chars)`
694
+ : 'Write failed',
695
+ };
696
+ }
697
+ catch (e) {
698
+ return { success: false, output: '', error: e.message };
699
+ }
700
+ },
701
+ file_read: async (p) => {
702
+ let filePath = p.path || p.file || '';
703
+ if (!filePath)
704
+ return { success: false, output: '', error: 'No path' };
705
+ if (isPathDenied(filePath)) {
706
+ console.warn(`[Security] file_read DENIED: ${filePath}`);
707
+ return { success: false, output: '', error: 'Access denied: protected path. Aiden cannot read credentials, SSH keys, or env files.' };
708
+ }
709
+ try {
710
+ // Expand ~ and Desktop shorthands, and fix any "Aiden" username to actual system user
711
+ const _user = process.env.USERNAME || process.env.USER || require('os').userInfo().username || 'User';
712
+ const _home = require('os').homedir();
713
+ filePath = filePath
714
+ .replace(/^~[\/\\]/i, _home + path_1.default.sep)
715
+ .replace(/^Desktop[\/\\]/i, path_1.default.join(_home, 'Desktop') + path_1.default.sep)
716
+ .replace(/^C:\\Users\\Aiden\\/i, `C:\\Users\\${_user}\\`)
717
+ .replace(/^C:\/Users\/Aiden\//i, `C:/Users/${_user}/`);
718
+ // Resolve path: absolute paths (Windows C:\ or Unix /) used as-is; relative joined with cwd
719
+ const resolved = filePath.match(/^[A-Z]:/i) || filePath.startsWith('/')
720
+ ? filePath
721
+ : path_1.default.join(process.cwd(), filePath);
722
+ if (!fs_1.default.existsSync(resolved))
723
+ return { success: false, output: '', error: `Not found: ${resolved}` };
724
+ return { success: true, output: fs_1.default.readFileSync(resolved, 'utf-8').slice(0, 5000) };
725
+ }
726
+ catch (e) {
727
+ return { success: false, output: '', error: e.message };
728
+ }
729
+ },
730
+ file_list: async (p) => {
731
+ let dirPath = p.path || p.dir || process.cwd();
732
+ try {
733
+ // Expand ~ and Desktop shorthands, and fix any "Aiden" username to actual system user
734
+ const _user = process.env.USERNAME || process.env.USER || require('os').userInfo().username || 'User';
735
+ const _home = require('os').homedir();
736
+ dirPath = dirPath
737
+ .replace(/^~[\/\\]/i, _home + path_1.default.sep)
738
+ .replace(/^Desktop[\/\\]?$/i, path_1.default.join(_home, 'Desktop'))
739
+ .replace(/^Desktop[\/\\]/i, path_1.default.join(_home, 'Desktop') + path_1.default.sep)
740
+ .replace(/^C:\\Users\\Aiden\\/i, `C:\\Users\\${_user}\\`)
741
+ .replace(/^C:\/Users\/Aiden\//i, `C:/Users/${_user}/`);
742
+ const resolved = dirPath.match(/^[A-Z]:/i)
743
+ ? dirPath
744
+ : path_1.default.join(process.cwd(), dirPath);
745
+ return { success: true, output: fs_1.default.readdirSync(resolved).join('\n') };
746
+ }
747
+ catch (e) {
748
+ return { success: false, output: '', error: e.message };
749
+ }
750
+ },
751
+ run_python: async (p, ctx) => {
752
+ const script = p.script || p.code || p.command || '';
753
+ if (!script)
754
+ return { success: false, output: '', error: 'No script' };
755
+ // ── N+34: Docker sandbox routing ───────────────────────────
756
+ const _pyMode = process.env.AIDEN_SANDBOX_MODE || 'off';
757
+ if (_pyMode === 'strict' || _pyMode === 'auto') {
758
+ try {
759
+ const sr = await (0, sandboxRunner_1.runInDockerSandbox)({ command: script, type: 'python', timeout: 60000 });
760
+ const out = (sr.stdout + (sr.stderr ? `\nstderr: ${sr.stderr}` : '')).trim() || 'Script completed with no output';
761
+ return { success: sr.exitCode === 0, output: out, error: sr.exitCode !== 0 ? `Python exit ${sr.exitCode}` : undefined };
762
+ }
763
+ catch (sandboxErr) {
764
+ if (_pyMode === 'strict')
765
+ return { success: false, output: '', error: `[Sandbox] ${sandboxErr.message}` };
766
+ console.warn('[Sandbox] auto-mode fell back to host:', sandboxErr.message);
767
+ }
768
+ }
769
+ // ── Host execution (sandbox off or auto-fallback) ───────────
770
+ const tmp = path_1.default.join(process.cwd(), 'workspace', `py_${Date.now()}.py`);
771
+ fs_1.default.mkdirSync(path_1.default.dirname(tmp), { recursive: true });
772
+ fs_1.default.writeFileSync(tmp, script);
773
+ const showProgress = process.env.AIDEN_SHOW_TOOL_OUTPUT !== 'false';
774
+ return new Promise((resolve) => {
775
+ let stdout = '';
776
+ let stderr = '';
777
+ const proc = (0, child_process_1.spawn)('python', [tmp], { cwd: process.cwd() });
778
+ const timer = setTimeout(() => { proc.kill(); resolve({ success: false, output: stdout, error: 'Python timeout (60s)' }); }, 60000);
779
+ proc.stdout.on('data', (data) => {
780
+ const text = data.toString();
781
+ stdout += text;
782
+ if (showProgress && ctx?.emitProgress) {
783
+ for (const line of text.split('\n')) {
784
+ const trimmed = line.trim();
785
+ if (trimmed)
786
+ ctx.emitProgress(trimmed.slice(0, 100));
787
+ }
788
+ }
789
+ });
790
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
791
+ proc.on('close', (code) => {
792
+ clearTimeout(timer);
793
+ try {
794
+ fs_1.default.unlinkSync(tmp);
795
+ }
796
+ catch { }
797
+ const output = (stdout || stderr || '').trim() || 'Script completed with no output';
798
+ resolve({ success: code === 0, output, error: code !== 0 ? `Python exit ${code}` : undefined });
799
+ });
800
+ });
801
+ },
802
+ run_node: async (p) => {
803
+ const script = p.script || p.code || p.command || '';
804
+ if (!script)
805
+ return { success: false, output: '', error: 'No script' };
806
+ const tmp = path_1.default.join(process.cwd(), 'workspace', `js_${Date.now()}.js`);
807
+ fs_1.default.mkdirSync(path_1.default.dirname(tmp), { recursive: true });
808
+ fs_1.default.writeFileSync(tmp, script);
809
+ try {
810
+ const { stdout, stderr } = await execAsync(`node "${tmp}"`, {
811
+ timeout: 60000,
812
+ cwd: process.cwd(),
813
+ });
814
+ return { success: true, output: (stdout || stderr || '').trim() || 'Script completed with no output' };
815
+ }
816
+ catch (e) {
817
+ return { success: false, output: e.stdout || '', error: `Node error: ${e.message}` };
818
+ }
819
+ finally {
820
+ try {
821
+ fs_1.default.unlinkSync(tmp);
822
+ }
823
+ catch { }
824
+ }
825
+ },
826
+ system_info: async () => {
827
+ try {
828
+ const { stdout } = await execAsync(`@{ CPU=(Get-CimInstance Win32_Processor).Name; RAM_GB=[math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory/1GB,1); OS=(Get-CimInstance Win32_OperatingSystem).Caption; FreeGB=[math]::Round((Get-PSDrive C).Free/1GB,1); User=$env:USERNAME } | ConvertTo-Json`, { shell: 'powershell.exe', timeout: 15000 });
829
+ return { success: true, output: stdout.trim() };
830
+ }
831
+ catch (e) {
832
+ return { success: false, output: '', error: e.message };
833
+ }
834
+ },
835
+ notify: async (p) => {
836
+ const msg = (p.message || p.command || p.title || p.body || '')
837
+ .replace(/'/g, '').replace(/"/g, '').replace(/`/g, '').replace(/\$/g, '').trim();
838
+ if (!msg)
839
+ return { success: false, output: '', error: 'No message provided for notification' };
840
+ try {
841
+ // Windows 10/11 Toast notification via WinRT — fires instantly, no Start-Sleep needed.
842
+ // Run fully detached so the child process never inherits the parent terminal stdio.
843
+ const psCmd = [
844
+ '[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null',
845
+ '$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)',
846
+ '$n = $t.GetElementsByTagName("text")',
847
+ `$n.Item(0).AppendChild($t.CreateTextNode("Aiden")) | Out-Null`,
848
+ `$n.Item(1).AppendChild($t.CreateTextNode("${msg}")) | Out-Null`,
849
+ '$toast = [Windows.UI.Notifications.ToastNotification]::new($t)',
850
+ '[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Aiden").Show($toast)',
851
+ ].join('; ');
852
+ const child = (0, child_process_1.spawn)('powershell', [
853
+ '-WindowStyle', 'Hidden',
854
+ '-NonInteractive',
855
+ '-Command', psCmd,
856
+ ], {
857
+ detached: true,
858
+ stdio: 'ignore',
859
+ windowsHide: true,
860
+ });
861
+ child.unref(); // don't keep Node alive waiting for it
862
+ return { success: true, output: `Desktop notification sent: "${msg}".` };
863
+ }
864
+ catch (e) {
865
+ return { success: false, output: '', error: `Notification failed: ${e.message}` };
866
+ }
867
+ },
868
+ web_search: async (p) => {
869
+ const query = p.query || p.command || p.topic || '';
870
+ if (!query)
871
+ return { success: false, output: '', error: 'No query provided' };
872
+ // Date/time fast-path — answer from system clock without network call
873
+ if (/what\s+(year|date|day|time)|current\s+(year|date|day|time)|today'?s?\s+(date|year|day)|what\s+is\s+today/i.test(query)) {
874
+ const now = new Date();
875
+ const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
876
+ return {
877
+ success: true,
878
+ output: `Current date: ${dateStr}. Year: ${now.getFullYear()}. Time: ${now.toLocaleTimeString('en-US')}.`,
879
+ method: 'system_clock',
880
+ };
881
+ }
882
+ return (0, webSearch_1.reliableWebSearch)(query);
883
+ },
884
+ _web_search_legacy_unused: async (p) => {
885
+ // Legacy implementation preserved for reference — no longer called
886
+ const query = p.query || '';
887
+ if (!query)
888
+ return { success: false, output: '', error: 'No query provided' };
889
+ // ── Weather detection ────────────────────────────────────────
890
+ if (/weather|temperature|forecast|rain|snow|sunny|cloudy|humidity|wind/i.test(query)) {
891
+ const city = query
892
+ .replace(/what(?:'s| is) the weather/gi, '')
893
+ .replace(/\bweather\b/gi, '')
894
+ .replace(/\bforecast\b/gi, '')
895
+ .replace(/\btoday\b/gi, '')
896
+ .replace(/\bcurrent\b/gi, '')
897
+ .replace(/\btemperature\b/gi, '')
898
+ .replace(/\brain\b/gi, '')
899
+ .replace(/\bsnow\b/gi, '')
900
+ .replace(/\bsunny\b/gi, '')
901
+ .replace(/\bcloudy\b/gi, '')
902
+ .replace(/\bhumidity\b/gi, '')
903
+ .replace(/\bwind\b/gi, '')
904
+ .replace(/\bin\b/gi, '')
905
+ .replace(/\bfor\b/gi, '')
906
+ .replace(/\bon\b/gi, '')
907
+ .replace(/\s+/g, ' ')
908
+ .trim() || 'auto';
909
+ console.log(`[Weather] city extracted: "${city}"`);
910
+ try {
911
+ const wr = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=j1`, { signal: AbortSignal.timeout(8000) });
912
+ const data = await wr.json();
913
+ const cc = data.current_condition?.[0];
914
+ const area = data.nearest_area?.[0];
915
+ if (cc && area) {
916
+ const location = [area.areaName?.[0]?.value, area.country?.[0]?.value].filter(Boolean).join(', ');
917
+ const desc = cc.weatherDesc?.[0]?.value || '';
918
+ let out = `Weather for ${location || city}:\n`;
919
+ out += `Condition: ${desc}\n`;
920
+ out += `Temperature: ${cc.temp_C}°C / ${cc.temp_F}°F (feels like ${cc.FeelsLikeC}°C)\n`;
921
+ out += `Humidity: ${cc.humidity}% | Wind: ${cc.windspeedKmph} km/h ${cc.winddir16Point}`;
922
+ out += ` | Visibility: ${cc.visibility} km | UV Index: ${cc.uvIndex}\n`;
923
+ const forecasts = (data.weather || []).slice(0, 3);
924
+ if (forecasts.length) {
925
+ out += '\n3-Day Forecast:\n';
926
+ for (const day of forecasts) {
927
+ const midDesc = day.hourly?.[4]?.weatherDesc?.[0]?.value || '';
928
+ out += ` ${day.date}: High ${day.maxtempC}°C / Low ${day.mintempC}°C${midDesc ? ' — ' + midDesc : ''}\n`;
929
+ }
930
+ }
931
+ console.log(`[web_search] Weather data retrieved for "${city}"`);
932
+ return { success: true, output: out.trim() };
933
+ }
934
+ }
935
+ catch (e) {
936
+ console.warn(`[web_search] Weather fetch failed: ${e.message}`);
937
+ }
938
+ }
939
+ const results = [];
940
+ // ── METHOD 1: DuckDuckGo Instant Answer API ──────────────────
941
+ try {
942
+ console.log(`[web_search] Method 1: DDG Instant API`);
943
+ const ddgUrl = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
944
+ const ddgRes = await fetch(ddgUrl, {
945
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0' },
946
+ signal: AbortSignal.timeout(8000),
947
+ });
948
+ const ddgData = await ddgRes.json();
949
+ const parts = [];
950
+ if (ddgData.Answer)
951
+ parts.push(`Answer: ${ddgData.Answer}`);
952
+ if (ddgData.Abstract)
953
+ parts.push(`Summary: ${ddgData.Abstract}`);
954
+ if (ddgData.AbstractText)
955
+ parts.push(ddgData.AbstractText);
956
+ if (ddgData.RelatedTopics?.length) {
957
+ const topics = ddgData.RelatedTopics
958
+ .slice(0, 8)
959
+ .map((t) => t.Text || t.Result || '')
960
+ .filter(Boolean);
961
+ if (topics.length)
962
+ parts.push(`Related: ${topics.join('. ')}`);
963
+ }
964
+ if (parts.length > 0) {
965
+ console.log(`[web_search] DDG Instant: got ${parts.length} parts`);
966
+ results.push(`[DuckDuckGo Instant]\n${parts.join('\n')}`);
967
+ }
968
+ else {
969
+ console.log(`[web_search] DDG Instant: no usable data`);
970
+ }
971
+ }
972
+ catch (e) {
973
+ console.warn(`[web_search] DDG instant failed: ${e.message}`);
974
+ }
975
+ // ── METHOD 2: Wikipedia Search API + summary ──────────────────
976
+ try {
977
+ console.log(`[web_search] Method 2: Wikipedia Search API`);
978
+ const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&srlimit=5&format=json&origin=*`;
979
+ const searchRes = await fetch(searchUrl, { signal: AbortSignal.timeout(6000) });
980
+ const searchData = await searchRes.json();
981
+ const searchHits = searchData?.query?.search || [];
982
+ console.log(`[web_search] Wikipedia search: ${searchHits.length} results`);
983
+ if (searchHits.length > 0) {
984
+ const topTitle = searchHits[0].title;
985
+ const summaryRes = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(topTitle)}`, { signal: AbortSignal.timeout(6000) });
986
+ if (summaryRes.ok) {
987
+ const wiki = await summaryRes.json();
988
+ if (wiki.extract && wiki.extract.length > 50) {
989
+ const snippets = searchHits
990
+ .slice(1, 4)
991
+ .map((h) => h.snippet?.replace(/<[^>]+>/g, '') || '')
992
+ .filter((s) => s.length > 20);
993
+ const extra = snippets.length > 0 ? `\nOther results: ${snippets.join(' | ')}` : '';
994
+ console.log(`[web_search] Wikipedia summary: ${wiki.extract.length} chars for "${wiki.title}"`);
995
+ results.push(`[Wikipedia: ${wiki.title}]\n${wiki.extract.slice(0, 1200)}${extra}`);
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ catch (e) {
1001
+ console.warn(`[web_search] Wikipedia failed: ${e.message}`);
1002
+ }
1003
+ // ── METHOD 3: DDG HTML scrape + snippet extraction + fetch top 3 pages ──
1004
+ try {
1005
+ console.log(`[web_search] Method 3: DDG HTML scrape`);
1006
+ const searchRes = await fetch(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
1007
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36' },
1008
+ signal: AbortSignal.timeout(10000),
1009
+ });
1010
+ const html = await searchRes.text();
1011
+ console.log(`[web_search] DDG HTML: ${html.length} bytes`);
1012
+ // Extract result snippets via result__snippet class
1013
+ const snippetMatches = [...html.matchAll(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g)];
1014
+ const snippets = snippetMatches
1015
+ .map(m => m[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim())
1016
+ .filter(s => s.length > 30)
1017
+ .slice(0, 5);
1018
+ console.log(`[web_search] DDG HTML snippets: ${snippets.length}`);
1019
+ if (snippets.length > 0) {
1020
+ results.push(`[Search Snippets for "${query}"]\n${snippets.join('\n\n')}`);
1021
+ }
1022
+ // Extract destination URLs via uddg= parameter
1023
+ const urlMatches = [...html.matchAll(/uddg=(https?[^&"]+)/g)];
1024
+ const urls = urlMatches
1025
+ .map(m => decodeURIComponent(m[1]))
1026
+ .filter(url => !url.includes('duckduckgo.com') &&
1027
+ !url.includes('youtube.com') &&
1028
+ url.startsWith('https'))
1029
+ .filter((url, i, arr) => arr.indexOf(url) === i)
1030
+ .slice(0, 3);
1031
+ console.log(`[web_search] DDG HTML urls: ${urls.length}`);
1032
+ // Fetch top 3 pages for real content
1033
+ const pageResults = await Promise.all(urls.map(async (url) => {
1034
+ try {
1035
+ console.log(`[web_search] Fetching page: ${url}`);
1036
+ const r = await fetch(url, {
1037
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
1038
+ signal: AbortSignal.timeout(7000),
1039
+ });
1040
+ if (!r.ok)
1041
+ return null;
1042
+ const text = await r.text();
1043
+ const clean = text
1044
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
1045
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
1046
+ .replace(/<nav[\s\S]*?<\/nav>/gi, '')
1047
+ .replace(/<header[\s\S]*?<\/header>/gi, '')
1048
+ .replace(/<footer[\s\S]*?<\/footer>/gi, '')
1049
+ .replace(/<[^>]+>/g, ' ')
1050
+ .replace(/\s+/g, ' ')
1051
+ .trim();
1052
+ if (clean.length < 200)
1053
+ return null;
1054
+ console.log(`[web_search] Page fetched: ${clean.length} chars from ${url}`);
1055
+ return `[${url}]\n${clean.slice(0, 2000)}`;
1056
+ }
1057
+ catch (e) {
1058
+ console.warn(`[web_search] Page fetch failed ${url}: ${e.message}`);
1059
+ return null;
1060
+ }
1061
+ }));
1062
+ results.push(...pageResults.filter(Boolean));
1063
+ }
1064
+ catch (e) {
1065
+ console.warn(`[web_search] HTML scrape failed: ${e.message}`);
1066
+ }
1067
+ if (results.length === 0) {
1068
+ console.warn(`[web_search] All methods failed for: "${query}"`);
1069
+ return { success: false, output: '', error: `No results found for: ${query}` };
1070
+ }
1071
+ console.log(`[web_search] Done: ${results.length} sections`);
1072
+ return { success: true, output: results.join('\n\n---\n\n').slice(0, 10000) };
1073
+ },
1074
+ fetch_url: async (p) => {
1075
+ const url = p.url || p.command || '';
1076
+ if (!url)
1077
+ return { success: false, output: '', error: 'No URL' };
1078
+ try {
1079
+ const res = await fetch(url, {
1080
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0.0.0' },
1081
+ signal: AbortSignal.timeout(15000),
1082
+ });
1083
+ const status = res.status;
1084
+ const text = await res.text();
1085
+ const clean = text
1086
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
1087
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
1088
+ .replace(/<nav[\s\S]*?<\/nav>/gi, '')
1089
+ .replace(/<header[\s\S]*?<\/header>/gi, '')
1090
+ .replace(/<footer[\s\S]*?<\/footer>/gi, '')
1091
+ .replace(/<[^>]+>/g, ' ')
1092
+ .replace(/\s{3,}/g, ' ')
1093
+ .trim();
1094
+ return { success: true, output: `HTTP ${status} ${res.statusText || 'OK'}\n\n${clean.slice(0, 3000)}` };
1095
+ }
1096
+ catch (e) {
1097
+ return { success: false, output: '', error: e.message };
1098
+ }
1099
+ },
1100
+ // Dedicated page fetcher — strips all HTML, returns clean readable text
1101
+ fetch_page: async (p) => {
1102
+ const url = p.url || p.command || '';
1103
+ if (!url)
1104
+ return { success: false, output: '', error: 'No URL' };
1105
+ try {
1106
+ const r = await fetch(url, {
1107
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
1108
+ signal: AbortSignal.timeout(10000),
1109
+ });
1110
+ const text = await r.text();
1111
+ const clean = text
1112
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
1113
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
1114
+ .replace(/<[^>]+>/g, ' ')
1115
+ .replace(/\s+/g, ' ')
1116
+ .trim();
1117
+ return { success: true, output: clean.slice(0, 3000) };
1118
+ }
1119
+ catch (e) {
1120
+ return { success: false, output: '', error: e.message };
1121
+ }
1122
+ },
1123
+ // 3-pass deep research using reliableWebSearch fallback chain
1124
+ deep_research: async (p) => {
1125
+ const topic = p.topic || p.query || p.command || '';
1126
+ if (!topic)
1127
+ return { success: false, output: '', error: 'No topic provided' };
1128
+ return (0, webSearch_1.deepResearch)(topic);
1129
+ },
1130
+ _deep_research_legacy_unused: async (p) => {
1131
+ // Legacy implementation preserved for reference — no longer called
1132
+ const topic = p.topic || '';
1133
+ if (!topic)
1134
+ return { success: false, output: '', error: 'No topic provided' };
1135
+ const results = [];
1136
+ if (results.length === 0) {
1137
+ return { success: false, output: '', error: `No research results for: ${topic}` };
1138
+ }
1139
+ const combined = results.join('\n\n');
1140
+ console.log(`[deep_research] Complete: ${combined.length} chars across ${results.length} passes`);
1141
+ return { success: true, output: combined.slice(0, 15000) };
1142
+ },
1143
+ // Activate a specialist agent persona — actual synthesis happens in respond phase
1144
+ run_agent: async (p) => {
1145
+ const agentName = (p.agent || 'engineer').toLowerCase();
1146
+ const task = p.task || p.command || '';
1147
+ if (!task)
1148
+ return { success: false, output: '', error: 'No task provided' };
1149
+ // ── Fork guard: only top-level agents can spawn specialists ──
1150
+ // Prevents sub-agents from recursively spawning more agents
1151
+ const FORK_CAPABLE_AGENTS = ['ceo', 'engineer'];
1152
+ const callerAgent = (p._callerAgent || '').toLowerCase();
1153
+ if (callerAgent && !FORK_CAPABLE_AGENTS.includes(callerAgent)) {
1154
+ console.warn(`[run_agent] ${callerAgent} attempted to fork ${agentName} — blocked (non-fork-capable)`);
1155
+ return { success: false, output: '', error: `Agent '${callerAgent}' cannot spawn sub-agents. Only CEO-level agents can delegate.` };
1156
+ }
1157
+ const agentPersonas = {
1158
+ engineer: 'Senior TypeScript/JavaScript engineer — writes clean, working code with full error handling.',
1159
+ security: 'Security auditor — analyzes for OWASP Top 10, provides specific fixes with code examples.',
1160
+ data_analyst: 'Data analyst — provides statistical analysis, patterns, and visualizable insights.',
1161
+ designer: 'UI/UX designer — provides design recommendations with color codes, typography, and layout.',
1162
+ researcher: 'Research specialist — extracts entities, compares systematically, identifies trends, gives conclusions.',
1163
+ debugger: 'Debugger — forms 3 hypotheses, eliminates systematically, provides exact fix with code.',
1164
+ };
1165
+ const persona = agentPersonas[agentName] || agentPersonas.engineer;
1166
+ // ── Context inheritance for complex tasks ─────────────────────
1167
+ // Complex tasks (long description or explicit context request) get conversation history
1168
+ const isComplex = task.length > 100 || p.inheritContext === true;
1169
+ let contextBlock = '';
1170
+ if (isComplex) {
1171
+ const memCtx = conversationMemory_1.conversationMemory.buildContext();
1172
+ if (memCtx && memCtx.trim()) {
1173
+ contextBlock = `\n## Conversation Context\nThe user has been discussing:\n${memCtx.slice(0, 1200)}\n`;
1174
+ console.log(`[run_agent] Injecting conversation context into ${agentName} task (${memCtx.length} chars)`);
1175
+ }
1176
+ }
1177
+ try {
1178
+ const { memoryLayers } = await Promise.resolve().then(() => __importStar(require('../memory/memoryLayers')));
1179
+ memoryLayers.write(`Agent ${agentName} task: ${task}`, ['agent', agentName]);
1180
+ }
1181
+ catch { }
1182
+ const fullTask = contextBlock
1183
+ ? `${contextBlock}\n## Your Task\n${task}`
1184
+ : task;
1185
+ return {
1186
+ success: true,
1187
+ output: `Agent: ${agentName}\nPersona: ${persona}\nTask: ${fullTask}\n\n[This runs inline — synthesize the result directly in your response. Do NOT tell the user results are "being processed", "running in background", or "will be ready soon". The answer must appear in this response turn.]`,
1188
+ };
1189
+ },
1190
+ git_status: async (p) => {
1191
+ const cwd = p.path || p.directory || p.cwd || process.cwd();
1192
+ try {
1193
+ const { stdout, stderr } = await execAsync('git status && git log --oneline -5', { shell: 'powershell.exe', timeout: 15000, cwd });
1194
+ return { success: true, output: stdout || stderr };
1195
+ }
1196
+ catch (e) {
1197
+ return { success: false, output: '', error: e.message };
1198
+ }
1199
+ },
1200
+ git_commit: async (p) => {
1201
+ const msg = (p.message || p.command || 'DevOS auto-commit').replace(/"/g, "'");
1202
+ try {
1203
+ const { stdout, stderr } = await execAsync(`git add -A && git commit -m "${msg}"`, { shell: 'powershell.exe', timeout: 30000, cwd: process.cwd() });
1204
+ return { success: true, output: stdout || stderr };
1205
+ }
1206
+ catch (e) {
1207
+ return { success: false, output: '', error: e.message };
1208
+ }
1209
+ },
1210
+ git_push: async (p) => {
1211
+ const remote = p.remote || 'origin';
1212
+ const branch = p.branch || 'master';
1213
+ try {
1214
+ const { stdout, stderr } = await execAsync(`git push ${remote} ${branch}`, { shell: 'powershell.exe', timeout: 60000, cwd: process.cwd() });
1215
+ return { success: true, output: stdout || stderr };
1216
+ }
1217
+ catch (e) {
1218
+ return { success: false, output: '', error: e.message };
1219
+ }
1220
+ },
1221
+ get_stocks: async (p) => {
1222
+ const market = p.market || p.exchange || 'NSE';
1223
+ const type = p.type || 'gainers'; // gainers | losers | active
1224
+ console.log(`[get_stocks] Fetching ${type} for ${market}`);
1225
+ const results = [];
1226
+ // Method 1: Yahoo Finance screener API — free, no auth needed
1227
+ try {
1228
+ const yahooUrl = `https://query1.finance.yahoo.com/v1/finance/screener/predefined/saved?scrIds=day_gainers&count=10&region=IN&lang=en-IN`;
1229
+ const r = await fetch(yahooUrl, {
1230
+ headers: {
1231
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
1232
+ 'Accept': 'application/json',
1233
+ },
1234
+ signal: AbortSignal.timeout(10000),
1235
+ });
1236
+ if (r.ok) {
1237
+ const data = await r.json();
1238
+ const quotes = data?.finance?.result?.[0]?.quotes || [];
1239
+ if (quotes.length > 0) {
1240
+ const lines = quotes.slice(0, 10).map((q) => `${q.symbol}: ${q.regularMarketPrice} (${q.regularMarketChangePercent?.toFixed(2)}%) — ${q.shortName || q.longName || ''}`);
1241
+ results.push(`Top Gainers (Yahoo Finance India):\n${lines.join('\n')}`);
1242
+ }
1243
+ }
1244
+ }
1245
+ catch (e) {
1246
+ console.warn(`[get_stocks] Yahoo Finance failed: ${e.message}`);
1247
+ }
1248
+ // Method 2: Finology ticker
1249
+ try {
1250
+ const finologyUrl = type === 'gainers'
1251
+ ? 'https://ticker.finology.in/market/top-gainers'
1252
+ : type === 'losers'
1253
+ ? 'https://ticker.finology.in/market/top-losers'
1254
+ : 'https://ticker.finology.in/market/most-active';
1255
+ const r = await fetch(finologyUrl, {
1256
+ headers: {
1257
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
1258
+ 'Accept': 'text/html',
1259
+ },
1260
+ signal: AbortSignal.timeout(10000),
1261
+ });
1262
+ if (r.ok) {
1263
+ const html = await r.text();
1264
+ const rows = [...html.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)];
1265
+ const stocks = [];
1266
+ for (const row of rows.slice(1, 15)) {
1267
+ const cells = [...row[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi)]
1268
+ .map((c) => c[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim())
1269
+ .filter(Boolean);
1270
+ if (cells.length >= 3 && cells[0].length > 1) {
1271
+ stocks.push(cells.slice(0, 5).join(' | '));
1272
+ }
1273
+ }
1274
+ if (stocks.length > 0) {
1275
+ results.push(`${market} Top ${type} (Finology):\n${stocks.slice(0, 10).join('\n')}`);
1276
+ }
1277
+ }
1278
+ }
1279
+ catch (e) {
1280
+ console.warn(`[get_stocks] Finology failed: ${e.message}`);
1281
+ }
1282
+ // Method 3: Economic Times market stats
1283
+ try {
1284
+ const segment = type === 'gainers' ? 'gainers' : type === 'losers' ? 'losers' : 'active-stocks';
1285
+ const etUrl = `https://economictimes.indiatimes.com/stocks/marketstats/top-${segment}/nse`;
1286
+ const r = await fetch(etUrl, {
1287
+ headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' },
1288
+ signal: AbortSignal.timeout(10000),
1289
+ });
1290
+ if (r.ok) {
1291
+ const html = await r.text();
1292
+ const clean = html
1293
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
1294
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
1295
+ .replace(/<[^>]+>/g, ' ')
1296
+ .replace(/\s+/g, ' ')
1297
+ .trim();
1298
+ const stockPattern = /\b([A-Z]{2,10})\b[\s\S]{0,30}?(\d+\.?\d*)\s*[(%]\s*([+-]?\d+\.?\d*)/g;
1299
+ const matches = [...clean.matchAll(stockPattern)].slice(0, 10);
1300
+ if (matches.length > 0) {
1301
+ const lines = matches.map((m) => `${m[1]}: ${m[2]} (${m[3]}%)`);
1302
+ results.push(`ET Market Stats:\n${lines.join('\n')}`);
1303
+ }
1304
+ }
1305
+ }
1306
+ catch (e) {
1307
+ console.warn(`[get_stocks] ET failed: ${e.message}`);
1308
+ }
1309
+ if (results.length === 0) {
1310
+ // All scrapers failed — fall back to web search
1311
+ console.log(`[get_stocks] Scrapers failed — falling back to reliableWebSearch`);
1312
+ try {
1313
+ const searchResult = await (0, webSearch_1.reliableWebSearch)(`${market} top ${type} stocks today NSE BSE Nifty`);
1314
+ if (searchResult.success && searchResult.output) {
1315
+ return { success: true, output: `${market} Top ${type} stocks:\n${searchResult.output}` };
1316
+ }
1317
+ }
1318
+ catch { }
1319
+ // Return a structured placeholder so the response at least has market keywords
1320
+ return {
1321
+ success: true,
1322
+ output: `${market} top ${type} stocks data unavailable right now (market may be closed or data source unreachable). Please check NSE/BSE directly at nseindia.com or bseindia.com for live gainers/losers with % changes.`,
1323
+ };
1324
+ }
1325
+ // Format final output to ensure exchange/percentage keywords are prominent
1326
+ const rawOutput = results.join('\n\n---\n\n').slice(0, 5000);
1327
+ const header = rawOutput.toLowerCase().includes(market.toLowerCase())
1328
+ ? rawOutput
1329
+ : `${market} Market — Top ${type}:\n${rawOutput}`;
1330
+ return {
1331
+ success: true,
1332
+ output: header,
1333
+ };
1334
+ },
1335
+ // ── Financial tools ─────────────────────────────────────
1336
+ get_market_data: async (p) => {
1337
+ const raw = (p.symbol || p.ticker || '').trim();
1338
+ if (!raw)
1339
+ return { success: false, output: '', error: 'No symbol provided. Pass { symbol: "RELIANCE" } or { symbol: "AAPL" }.' };
1340
+ const symbol = normalizeNSESymbol(raw);
1341
+ try {
1342
+ const data = await (0, marketDataTool_1.getMarketData)(symbol);
1343
+ return { success: true, output: JSON.stringify(data, null, 2) };
1344
+ }
1345
+ catch (e) {
1346
+ return { success: false, output: '', error: e.message };
1347
+ }
1348
+ },
1349
+ get_company_info: async (p) => {
1350
+ const symbol = (p.symbol || p.ticker || '').trim();
1351
+ if (!symbol)
1352
+ return { success: false, output: '', error: 'No symbol provided. Pass { symbol: "RELIANCE" } or { symbol: "AAPL" }.' };
1353
+ try {
1354
+ const data = await (0, companyFilingsTool_1.getCompanyInfo)(symbol);
1355
+ return { success: true, output: JSON.stringify(data, null, 2) };
1356
+ }
1357
+ catch (e) {
1358
+ return { success: false, output: '', error: e.message };
1359
+ }
1360
+ },
1361
+ social_research: async (input) => {
1362
+ const { socialResearch } = await Promise.resolve().then(() => __importStar(require('./tools/socialResearchTool')));
1363
+ const result = await socialResearch(input.topic);
1364
+ return { success: true, output: JSON.stringify(result, null, 2) };
1365
+ },
1366
+ // ── Wait ───────────────────────────────────────────────────────
1367
+ wait: async (p) => {
1368
+ const ms = Math.min(Number(p.ms) || 1000, 5000);
1369
+ await new Promise(r => setTimeout(r, ms));
1370
+ return { success: true, output: `Waited ${ms}ms` };
1371
+ },
1372
+ // ── Computer control tools (PowerShell-only, zero native deps) ─
1373
+ mouse_move: async (p) => {
1374
+ try {
1375
+ const result = await (0, computerControl_1.moveMouse)(Number(p.x) || 0, Number(p.y) || 0);
1376
+ return { success: true, output: result };
1377
+ }
1378
+ catch (e) {
1379
+ return { success: false, output: '', error: e.message };
1380
+ }
1381
+ },
1382
+ mouse_click: async (p) => {
1383
+ try {
1384
+ const result = await (0, computerControl_1.clickMouse)(Number(p.x) || 0, Number(p.y) || 0, p.button || 'left', !!p.double);
1385
+ return { success: true, output: result };
1386
+ }
1387
+ catch (e) {
1388
+ return { success: false, output: '', error: e.message };
1389
+ }
1390
+ },
1391
+ keyboard_type: async (p) => {
1392
+ try {
1393
+ const result = await (0, computerControl_1.typeText)(String(p.text || ''));
1394
+ return { success: true, output: result };
1395
+ }
1396
+ catch (e) {
1397
+ return { success: false, output: '', error: e.message };
1398
+ }
1399
+ },
1400
+ keyboard_press: async (p) => {
1401
+ try {
1402
+ const result = await (0, computerControl_1.pressKey)(String(p.key || 'enter'));
1403
+ return { success: true, output: result };
1404
+ }
1405
+ catch (e) {
1406
+ return { success: false, output: '', error: e.message };
1407
+ }
1408
+ },
1409
+ screenshot: async (_p) => {
1410
+ try {
1411
+ const filepath = await (0, computerControl_1.takeScreenshot)();
1412
+ const stats = require('fs').statSync(filepath);
1413
+ return { success: true, output: `Screenshot saved: ${filepath} (${Math.round(stats.size / 1024)}kb)`, path: filepath };
1414
+ }
1415
+ catch (e) {
1416
+ return { success: false, output: '', error: e.message };
1417
+ }
1418
+ },
1419
+ screen_read: async (_p) => {
1420
+ try {
1421
+ const result = await (0, computerControl_1.readScreen)();
1422
+ return { success: true, output: result };
1423
+ }
1424
+ catch (e) {
1425
+ return { success: false, output: '', error: e.message };
1426
+ }
1427
+ },
1428
+ vision_loop: async (p) => {
1429
+ try {
1430
+ // Build a callLLM wrapper using the currently available provider
1431
+ const callLLMWrapper = async (prompt) => {
1432
+ const { getNextAvailableAPI } = await Promise.resolve().then(() => __importStar(require('../providers/router')));
1433
+ const { callLLM: _callLLM } = await Promise.resolve().then(() => __importStar(require('./agentLoop')));
1434
+ const next = getNextAvailableAPI();
1435
+ if (!next)
1436
+ return 'No API available';
1437
+ const key = next.entry.key.startsWith('env:')
1438
+ ? (process.env[next.entry.key.replace('env:', '')] || '')
1439
+ : next.entry.key;
1440
+ return _callLLM(prompt, key, next.entry.model, next.entry.provider);
1441
+ };
1442
+ const result = await (0, computerControl_1.visionLoop)(p.goal, p.max_steps || 10, callLLMWrapper);
1443
+ return { success: true, output: result };
1444
+ }
1445
+ catch (e) {
1446
+ return { success: false, output: '', error: e.message };
1447
+ }
1448
+ },
1449
+ // ── Sprint 16: Code Interpreter Sandbox ───────────────────────
1450
+ code_interpreter_python: async (p) => {
1451
+ const code = p.code || p.script || '';
1452
+ const packages = Array.isArray(p.packages) ? p.packages : undefined;
1453
+ if (!code)
1454
+ return { success: false, output: '', error: 'No code provided' };
1455
+ const result = await (0, codeInterpreter_1.runInSandbox)(code, 'python', packages);
1456
+ const filesNote = result.files && result.files.length > 0
1457
+ ? `\nFiles created: ${result.files.join(', ')}`
1458
+ : '';
1459
+ return {
1460
+ success: result.success,
1461
+ output: (result.output || '') + filesNote,
1462
+ error: result.error,
1463
+ };
1464
+ },
1465
+ code_interpreter_node: async (p) => {
1466
+ const code = p.code || p.script || '';
1467
+ if (!code)
1468
+ return { success: false, output: '', error: 'No code provided' };
1469
+ const result = await (0, codeInterpreter_1.runInSandbox)(code, 'node');
1470
+ const filesNote = result.files && result.files.length > 0
1471
+ ? `\nFiles created: ${result.files.join(', ')}`
1472
+ : '';
1473
+ return {
1474
+ success: result.success,
1475
+ output: (result.output || '') + filesNote,
1476
+ error: result.error,
1477
+ };
1478
+ },
1479
+ // ── Sprint 23: Clipboard + Window + App Launch Tools ──────────
1480
+ clipboard_read: async () => {
1481
+ try {
1482
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1483
+ const text = execSync('powershell.exe -Command "Get-Clipboard"', { timeout: 5000 }).toString().trim();
1484
+ return { success: true, output: text || '(clipboard is empty)' };
1485
+ }
1486
+ catch (e) {
1487
+ return { success: false, output: '', error: e.message };
1488
+ }
1489
+ },
1490
+ clipboard_write: async (p) => {
1491
+ const text = p.text || p.content || p.command || '';
1492
+ if (!text)
1493
+ return { success: false, output: '', error: 'No text provided' };
1494
+ try {
1495
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1496
+ const safe = text.replace(/'/g, "''");
1497
+ execSync(`powershell.exe -Command "Set-Clipboard -Value '${safe}'"`, { timeout: 5000 });
1498
+ return { success: true, output: `Copied to clipboard: "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"` };
1499
+ }
1500
+ catch (e) {
1501
+ return { success: false, output: '', error: e.message };
1502
+ }
1503
+ },
1504
+ window_list: async () => {
1505
+ try {
1506
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1507
+ const out = execSync('powershell.exe -Command "Get-Process | Where-Object {$_.MainWindowTitle -ne \'\'} | Select-Object -Property Id,ProcessName,MainWindowTitle | ConvertTo-Json"', { timeout: 10000 }).toString().trim();
1508
+ return { success: true, output: out || '(no visible windows found)' };
1509
+ }
1510
+ catch (e) {
1511
+ return { success: false, output: '', error: e.message };
1512
+ }
1513
+ },
1514
+ window_focus: async (p) => {
1515
+ const title = p.title || p.window || p.command || '';
1516
+ if (!title)
1517
+ return { success: false, output: '', error: 'No window title provided' };
1518
+ try {
1519
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1520
+ const safe = title.replace(/'/g, "''");
1521
+ execSync(`powershell.exe -Command "Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.Interaction]::AppActivate('${safe}')"`, { timeout: 8000 });
1522
+ return { success: true, output: `Focused window: "${title}"` };
1523
+ }
1524
+ catch (e) {
1525
+ return { success: false, output: '', error: e.message };
1526
+ }
1527
+ },
1528
+ app_launch: async (p) => {
1529
+ const app = p.app || p.path || p.command || '';
1530
+ if (!app)
1531
+ return { success: false, output: '', error: 'No app specified' };
1532
+ if (isShellDangerous(app)) {
1533
+ return { success: false, output: '', error: 'CommandGate: Blocked potentially dangerous app launch.' };
1534
+ }
1535
+ try {
1536
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1537
+ const safe = app.replace(/'/g, "''");
1538
+ execSync(`powershell.exe -Command "Start-Process '${safe}'"`, { timeout: 10000 });
1539
+ return { success: true, output: `Launched: "${app}"` };
1540
+ }
1541
+ catch (e) {
1542
+ return { success: false, output: '', error: e.message };
1543
+ }
1544
+ },
1545
+ app_close: async (p) => {
1546
+ const app = p.app || p.process || p.command || '';
1547
+ if (!app)
1548
+ return { success: false, output: '', error: 'No app/process name provided' };
1549
+ try {
1550
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1551
+ const safe = app.replace(/'/g, "''");
1552
+ execSync(`powershell.exe -Command "Stop-Process -Name '${safe}' -Force -ErrorAction SilentlyContinue"`, { timeout: 8000 });
1553
+ return { success: true, output: `Closed process: "${app}"` };
1554
+ }
1555
+ catch (e) {
1556
+ return { success: false, output: '', error: e.message };
1557
+ }
1558
+ },
1559
+ // ── Sprint 24: Folder Watcher ─────────────────────────────────
1560
+ watch_folder: async (p) => {
1561
+ const rawFolder = p.folder || p.path || p.dir || '';
1562
+ const goal = p.goal || p.command || '';
1563
+ const stop = !!p.stop;
1564
+ if (!rawFolder)
1565
+ return { success: false, output: '', error: 'No folder specified' };
1566
+ const userName = process.env.USERPROFILE || process.env.HOME || '';
1567
+ const folderPath = rawFolder
1568
+ .replace(/%USERPROFILE%/gi, userName)
1569
+ .replace(/^~[\/\\]/, userName + path_1.default.sep);
1570
+ // Stop mode
1571
+ if (stop) {
1572
+ const watcher = activeWatchers.get(folderPath);
1573
+ if (watcher) {
1574
+ watcher.close();
1575
+ activeWatchers.delete(folderPath);
1576
+ return { success: true, output: `Stopped watching: ${folderPath}` };
1577
+ }
1578
+ return { success: false, output: `No active watcher for: ${folderPath}` };
1579
+ }
1580
+ if (!goal)
1581
+ return { success: false, output: '', error: 'No goal specified' };
1582
+ if (!fs_1.default.existsSync(folderPath))
1583
+ return { success: false, output: '', error: `Folder not found: ${folderPath}` };
1584
+ // Close existing watcher on same path before starting a new one
1585
+ const existing = activeWatchers.get(folderPath);
1586
+ if (existing) {
1587
+ existing.close();
1588
+ activeWatchers.delete(folderPath);
1589
+ }
1590
+ const watcher = fs_1.default.watch(folderPath, async (eventType, filename) => {
1591
+ if (eventType !== 'rename' || !filename)
1592
+ return;
1593
+ const fullPath = path_1.default.join(folderPath, filename);
1594
+ // Small delay to let the file finish writing
1595
+ await new Promise(r => setTimeout(r, 500));
1596
+ if (!fs_1.default.existsSync(fullPath))
1597
+ return;
1598
+ let isFile = false;
1599
+ try {
1600
+ isFile = fs_1.default.statSync(fullPath).isFile();
1601
+ }
1602
+ catch {
1603
+ return;
1604
+ }
1605
+ if (!isFile)
1606
+ return;
1607
+ try {
1608
+ await fetch('http://localhost:4200/api/chat', {
1609
+ method: 'POST',
1610
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
1611
+ body: JSON.stringify({ message: `${goal} — new file: ${fullPath}`, history: [] }),
1612
+ });
1613
+ }
1614
+ catch { }
1615
+ });
1616
+ activeWatchers.set(folderPath, watcher);
1617
+ return {
1618
+ success: true,
1619
+ output: `Now watching: ${folderPath}\nWill execute: "${goal}" when new files appear.\nActive watchers: ${activeWatchers.size}`,
1620
+ };
1621
+ },
1622
+ watch_folder_list: async () => {
1623
+ if (activeWatchers.size === 0)
1624
+ return { success: true, output: 'No active folder watchers.' };
1625
+ const list = Array.from(activeWatchers.keys()).map((f, i) => `${i + 1}. ${f}`).join('\n');
1626
+ return { success: true, output: `Active watchers:\n${list}` };
1627
+ },
1628
+ get_briefing: async (_p) => {
1629
+ try {
1630
+ const config = (0, morningBriefing_1.loadBriefingConfig)();
1631
+ const briefing = await (0, morningBriefing_1.generateBriefing)(config);
1632
+ return { success: true, output: briefing };
1633
+ }
1634
+ catch (e) {
1635
+ return { success: false, output: '', error: `Briefing failed: ${e.message}` };
1636
+ }
1637
+ },
1638
+ get_natural_events: async () => {
1639
+ try {
1640
+ const res = await fetch('https://eonet.gsfc.nasa.gov/api/v3/events?limit=10&status=open', { signal: AbortSignal.timeout(8000) });
1641
+ if (!res.ok)
1642
+ throw new Error(`EONET API returned ${res.status}`);
1643
+ const data = await res.json();
1644
+ const events = (data.events ?? []).map((e) => ({
1645
+ id: e.id,
1646
+ title: e.title,
1647
+ category: e.categories?.[0]?.title ?? 'Unknown',
1648
+ date: e.geometry?.[0]?.date ?? null,
1649
+ link: e.sources?.[0]?.url ?? null,
1650
+ }));
1651
+ return { success: true, output: JSON.stringify({ events, count: events.length }, null, 2) };
1652
+ }
1653
+ catch (e) {
1654
+ return { success: false, output: '', error: `NASA EONET fetch failed: ${e.message}` };
1655
+ }
1656
+ },
1657
+ // ── manage_goals — track and manage long-running goals ────────
1658
+ manage_goals: async (p) => {
1659
+ const { loadGoals, saveGoals } = await Promise.resolve().then(() => __importStar(require('./goalTracker')));
1660
+ const goals = loadGoals();
1661
+ const today = new Date().toISOString().split('T')[0];
1662
+ switch (p.action) {
1663
+ case 'list':
1664
+ return { success: true, output: JSON.stringify(goals.filter(g => g.status !== 'done'), null, 2) };
1665
+ case 'add': {
1666
+ if (!p.title)
1667
+ return { success: false, output: '', error: 'Title required' };
1668
+ const { getLimit } = await Promise.resolve().then(() => __importStar(require('./featureGates')));
1669
+ const maxGoals = getLimit('maxGoals');
1670
+ const activeGoals = goals.filter(g => g.status !== 'done');
1671
+ if (activeGoals.length >= maxGoals) {
1672
+ return {
1673
+ success: false, output: '',
1674
+ error: `Goal limit reached (${maxGoals} active goals on Free plan). Complete existing goals or upgrade to Pro for unlimited goals.`,
1675
+ };
1676
+ }
1677
+ goals.push({
1678
+ id: Date.now().toString(),
1679
+ title: p.title,
1680
+ status: 'not_started',
1681
+ target: p.target,
1682
+ nextAction: p.nextAction,
1683
+ lastUpdated: today,
1684
+ });
1685
+ saveGoals(goals);
1686
+ return { success: true, output: `Goal added: ${p.title}` };
1687
+ }
1688
+ case 'update': {
1689
+ const g = goals.find(g => g.title.toLowerCase().includes((p.title || '').toLowerCase()));
1690
+ if (!g)
1691
+ return { success: false, output: '', error: 'Goal not found' };
1692
+ if (p.status)
1693
+ g.status = p.status;
1694
+ if (p.nextAction)
1695
+ g.nextAction = p.nextAction;
1696
+ if (p.target)
1697
+ g.target = p.target;
1698
+ g.lastUpdated = today;
1699
+ saveGoals(goals);
1700
+ return { success: true, output: `Updated: ${g.title}` };
1701
+ }
1702
+ case 'complete': {
1703
+ const idx = goals.findIndex(g => g.title.toLowerCase().includes((p.title || '').toLowerCase()));
1704
+ if (idx < 0)
1705
+ return { success: false, output: '', error: 'Goal not found' };
1706
+ goals[idx].status = 'done';
1707
+ goals[idx].lastUpdated = today;
1708
+ saveGoals(goals);
1709
+ return { success: true, output: `Completed: ${goals[idx].title}` };
1710
+ }
1711
+ case 'suggest': {
1712
+ const active = goals.filter(g => g.status !== 'done');
1713
+ if (active.length === 0)
1714
+ return { success: true, output: 'No active goals. What are you working on?' };
1715
+ return { success: true, output: `Focus on: ${active[0].title} — Next: ${active[0].nextAction || 'Define next step'}` };
1716
+ }
1717
+ case 'remove':
1718
+ case 'delete': {
1719
+ const before = goals.length;
1720
+ const remaining = goals.filter(g => !g.title.toLowerCase().includes((p.title || '').toLowerCase()));
1721
+ if (remaining.length === before)
1722
+ return { success: false, output: '', error: 'Goal not found' };
1723
+ saveGoals(remaining);
1724
+ return { success: true, output: `Removed goal matching: ${p.title}` };
1725
+ }
1726
+ default:
1727
+ return { success: false, output: '', error: `Unknown action: ${p.action}. Use: list, add, update, complete, remove, suggest` };
1728
+ }
1729
+ },
1730
+ // ── ingest_youtube — extract transcript and store in Knowledge Base ──
1731
+ ingest_youtube: async (p) => {
1732
+ const url = String(p.url || '');
1733
+ if (!url)
1734
+ return { success: false, output: '', error: 'URL required' };
1735
+ const result = await (0, youtubeTranscript_1.extractYouTubeTranscript)(url);
1736
+ if (!result) {
1737
+ return {
1738
+ success: false,
1739
+ output: '',
1740
+ error: 'Could not extract transcript. The video may not have captions, ' +
1741
+ 'or YouTube blocked the request. Try installing yt-dlp, or paste ' +
1742
+ 'the transcript text directly into the chat.',
1743
+ };
1744
+ }
1745
+ const ingestResult = knowledgeBase_1.knowledgeBase.ingestText(result.fullText, `youtube_${result.title.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 60)}.txt`, 'transcript', ['youtube', 'video', 'transcript'], 'public');
1746
+ if (!ingestResult.success) {
1747
+ return { success: false, output: '', error: ingestResult.error || 'Knowledge Base ingestion failed' };
1748
+ }
1749
+ console.log(`[YouTube] Ingested: "${result.title}" (${result.transcript.length} segments)`);
1750
+ return {
1751
+ success: true,
1752
+ output: `Ingested transcript for "${result.title}" — ${result.transcript.length} segments, ` +
1753
+ `${result.fullText.length} characters stored in ${ingestResult.chunkCount} chunks. ` +
1754
+ `Now searchable in Knowledge Base.`,
1755
+ };
1756
+ },
1757
+ // ── get_calendar — fetch upcoming events from Google Calendar iCal ──
1758
+ get_calendar: async (p) => {
1759
+ const cfg = (0, index_1.loadConfig)();
1760
+ const icalUrl = cfg.calendar?.icalUrl;
1761
+ if (!icalUrl) {
1762
+ return {
1763
+ success: false,
1764
+ output: '',
1765
+ error: 'Calendar not configured. Add your Google Calendar iCal URL in Settings → Channels.',
1766
+ };
1767
+ }
1768
+ const daysAhead = typeof p.daysAhead === 'number' ? p.daysAhead : 7;
1769
+ try {
1770
+ const events = await (0, calendarTool_1.getCalendarEvents)(icalUrl, daysAhead);
1771
+ if (events.length === 0) {
1772
+ return { success: true, output: `No upcoming events in the next ${daysAhead} day(s).` };
1773
+ }
1774
+ const formatted = events.map(e => {
1775
+ const when = e.start.toLocaleString();
1776
+ const loc = e.location ? ` @ ${e.location}` : '';
1777
+ return `• ${e.title} — ${when}${loc}`;
1778
+ }).join('\n');
1779
+ return { success: true, output: `Upcoming events (next ${daysAhead} days):\n${formatted}` };
1780
+ }
1781
+ catch (err) {
1782
+ return { success: false, output: '', error: `Calendar fetch failed: ${String(err).slice(0, 120)}` };
1783
+ }
1784
+ },
1785
+ // ── read_email — read recent Gmail messages via App Password ──
1786
+ read_email: async (p) => {
1787
+ const cfg = (0, index_1.loadConfig)();
1788
+ const email = cfg.gmail?.email;
1789
+ const appPassword = cfg.gmail?.appPassword;
1790
+ if (!email || !appPassword) {
1791
+ return {
1792
+ success: false,
1793
+ output: '',
1794
+ error: 'Gmail not configured. Add your email and App Password in Settings → Channels.',
1795
+ };
1796
+ }
1797
+ const count = typeof p.count === 'number' ? p.count : 10;
1798
+ const messages = await (0, gmailTool_1.readGmail)({ email, appPassword }, count, p.folder || 'INBOX');
1799
+ if (messages.length === 0) {
1800
+ return {
1801
+ success: true,
1802
+ output: 'No unread messages found, or imap-simple is not yet installed (run: npm install imap-simple).',
1803
+ };
1804
+ }
1805
+ const formatted = messages.map(m => `• From: ${m.from}\n Subject: ${m.subject}\n Date: ${m.date}`).join('\n\n');
1806
+ return { success: true, output: `Recent emails (${messages.length}):\n\n${formatted}` };
1807
+ },
1808
+ // ── send_email — send an email via Gmail App Password ─────────
1809
+ send_email: async (p) => {
1810
+ const cfg = (0, index_1.loadConfig)();
1811
+ const email = cfg.gmail?.email;
1812
+ const appPassword = cfg.gmail?.appPassword;
1813
+ if (!email || !appPassword) {
1814
+ return {
1815
+ success: false,
1816
+ output: '',
1817
+ error: 'Gmail not configured. Add your email and App Password in Settings → Channels.',
1818
+ };
1819
+ }
1820
+ const to = String(p.to || '');
1821
+ const subject = String(p.subject || '');
1822
+ const body = String(p.body || '');
1823
+ if (!to || !subject) {
1824
+ return { success: false, output: '', error: '`to` and `subject` are required.' };
1825
+ }
1826
+ const result = await (0, gmailTool_1.sendGmail)({ email, appPassword }, to, subject, body);
1827
+ if (result.success) {
1828
+ return { success: true, output: `Email sent to ${to}: "${subject}"` };
1829
+ }
1830
+ return { success: false, output: '', error: result.error || 'Send failed' };
1831
+ },
1832
+ // ── compact_context — summarize and compress conversation history ──
1833
+ compact_context: async (p) => {
1834
+ const { sessionMemory } = await Promise.resolve().then(() => __importStar(require('./sessionMemory')));
1835
+ const { memoryExtractor } = await Promise.resolve().then(() => __importStar(require('./memoryExtractor')));
1836
+ const sessionId = p.sessionId || 'default';
1837
+ try {
1838
+ // Trigger session write to persist current conversation state
1839
+ await sessionMemory.writeSession(sessionId);
1840
+ // Extract durable memories from session
1841
+ await memoryExtractor.extractFromSession(sessionId);
1842
+ return { success: true, output: `Context compacted for session ${sessionId}. Memory extracted and persisted.` };
1843
+ }
1844
+ catch (e) {
1845
+ return { success: false, output: '', error: `Compact failed: ${e.message}` };
1846
+ }
1847
+ },
1848
+ // ── lookup_tool_schema — return full description for a named tool ──
1849
+ lookup_tool_schema: async (p) => {
1850
+ const name = (p.toolName || p.name || '').trim();
1851
+ if (!name)
1852
+ return { success: false, output: '', error: 'No toolName provided' };
1853
+ const exists = exports.TOOLS[name];
1854
+ if (!exists)
1855
+ return { success: false, output: '', error: `Tool "${name}" not found` };
1856
+ const desc = exports.TOOL_DESCRIPTIONS[name] || '(no description)';
1857
+ return { success: true, output: JSON.stringify({ name, description: desc }, null, 2) };
1858
+ },
1859
+ // ── lookup_skill — BM25-match a query against learned skills ─────
1860
+ lookup_skill: async (p) => {
1861
+ const query = (p.query || p.task || '').trim();
1862
+ if (!query)
1863
+ return { success: false, output: '', error: 'No query provided' };
1864
+ const cwd = process.cwd();
1865
+ // Token similarity (Dice coefficient)
1866
+ const tok = (s) => new Set(s.toLowerCase().replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter(Boolean));
1867
+ const dice = (a, b) => {
1868
+ const sa = tok(a);
1869
+ const sb = tok(b);
1870
+ let n = 0;
1871
+ sa.forEach(t => { if (sb.has(t))
1872
+ n++; });
1873
+ return (2 * n) / (sa.size + sb.size + 0.001);
1874
+ };
1875
+ // Scan learned/, approved/, and installed/ folders
1876
+ const skillFolders = ['learned', 'approved', 'installed']
1877
+ .map(f => path_1.default.join(cwd, 'workspace', 'skills', f))
1878
+ .filter(d => fs_1.default.existsSync(d));
1879
+ if (skillFolders.length === 0)
1880
+ return { success: false, output: '', error: 'No skills yet' };
1881
+ let best = { score: 0, dir: '', name: '' };
1882
+ for (const folder of skillFolders) {
1883
+ for (const entry of fs_1.default.readdirSync(folder, { withFileTypes: true })) {
1884
+ if (!entry.isDirectory())
1885
+ continue;
1886
+ const metaPath = path_1.default.join(folder, entry.name, 'meta.json');
1887
+ let taskPattern = entry.name;
1888
+ try {
1889
+ const meta = JSON.parse(fs_1.default.readFileSync(metaPath, 'utf-8'));
1890
+ taskPattern = meta.taskPattern || meta.description || entry.name;
1891
+ }
1892
+ catch { }
1893
+ const score = Math.max(dice(query, entry.name), dice(query, taskPattern));
1894
+ if (score > best.score)
1895
+ best = { score, dir: path_1.default.join(folder, entry.name), name: entry.name };
1896
+ }
1897
+ }
1898
+ const THRESHOLD = 0.25;
1899
+ if (best.score < THRESHOLD)
1900
+ return { success: false, output: '', error: `No matching skill found (best: ${best.name} @ ${best.score.toFixed(2)})` };
1901
+ const skillPath = path_1.default.join(best.dir, 'SKILL.md');
1902
+ if (!fs_1.default.existsSync(skillPath))
1903
+ return { success: false, output: '', error: `Skill "${best.name}" has no SKILL.md` };
1904
+ const content = fs_1.default.readFileSync(skillPath, 'utf-8');
1905
+ return { success: true, output: `[Skill: ${best.name} — match score ${best.score.toFixed(2)}]\n\n${content}` };
1906
+ },
1907
+ // ── ▲ run — execute JavaScript/TypeScript in the Aiden SDK sandbox ──────
1908
+ run: async (p) => {
1909
+ const code = p.code || p.script || '';
1910
+ const description = p.description || '';
1911
+ if (!code)
1912
+ return { success: false, output: '', error: 'No code provided' };
1913
+ try {
1914
+ // Lazy import to avoid circular dependency at module init
1915
+ const { runInSandbox } = await Promise.resolve().then(() => __importStar(require('./runSandbox')));
1916
+ const result = await runInSandbox(code, { timeout: p.timeout ?? 30000, maxToolCalls: p.maxToolCalls ?? 20 });
1917
+ const summary = [
1918
+ description ? `// ${description}` : '',
1919
+ result.output.join('\n'),
1920
+ result.error ? `[error] ${result.error}` : '',
1921
+ result.toolCalls.length > 0
1922
+ ? `[tools] ${result.toolCalls.map(c => `${c.tool}(${c.durationMs}ms)`).join(', ')}`
1923
+ : '',
1924
+ `[duration] ${result.durationMs}ms`,
1925
+ ].filter(Boolean).join('\n');
1926
+ return { success: result.success, output: summary, error: result.error };
1927
+ }
1928
+ catch (e) {
1929
+ return { success: false, output: '', error: e.message };
1930
+ }
1931
+ },
1932
+ // ── ▲ spawn — delegate a sub-task to an isolated subagent ────────────────
1933
+ spawn: async (p) => {
1934
+ const task = p.task || p.prompt || '';
1935
+ const context = p.context ?? undefined;
1936
+ const timeout = typeof p.timeout === 'number' ? p.timeout : 60000;
1937
+ if (!task)
1938
+ return { success: false, output: '', error: 'No task provided' };
1939
+ try {
1940
+ const { spawnSubagent } = await Promise.resolve().then(() => __importStar(require('./spawnManager')));
1941
+ const { getBudgetState } = await Promise.resolve().then(() => __importStar(require('./agentLoop')));
1942
+ const budget = getBudgetState() ?? { current: 1, max: 10, remaining: 9 };
1943
+ const result = await spawnSubagent({ task, context, timeout, parentBudget: budget });
1944
+ const out = [
1945
+ result.result ?? '',
1946
+ `[spawn] iterations=${result.iterationsUsed} duration=${result.duration}ms`,
1947
+ result.providerChain.length ? `[providers] ${result.providerChain.join(' → ')}` : '',
1948
+ ].filter(Boolean).join('\n');
1949
+ return { success: result.success, output: out, error: result.error };
1950
+ }
1951
+ catch (e) {
1952
+ return { success: false, output: '', error: e.message };
1953
+ }
1954
+ },
1955
+ // ── spawn_subagent — spec-aligned alias for spawn ────────────────────────
1956
+ spawn_subagent: async (p) => {
1957
+ const task = (p.task || '').trim();
1958
+ const context = p.context ?? undefined;
1959
+ const timeout = typeof p.timeout_seconds === 'number' ? p.timeout_seconds * 1000
1960
+ : typeof p.timeout === 'number' ? p.timeout
1961
+ : 60000;
1962
+ if (!task)
1963
+ return { success: false, output: '', error: 'No task provided' };
1964
+ try {
1965
+ const { spawnSubagent } = await Promise.resolve().then(() => __importStar(require('./spawnManager')));
1966
+ const { getBudgetState } = await Promise.resolve().then(() => __importStar(require('./agentLoop')));
1967
+ const budget = getBudgetState() ?? { current: 1, max: 10, remaining: 9 };
1968
+ const result = await spawnSubagent({ task, context, timeout, parentBudget: budget });
1969
+ if (!result.success)
1970
+ return { success: false, output: '', error: result.error || 'Subagent failed' };
1971
+ const out = [
1972
+ result.result ?? '',
1973
+ `[spawn_subagent] iterations=${result.iterationsUsed} duration=${result.duration}ms`,
1974
+ result.providerChain.length ? `[providers] ${result.providerChain.join(' → ')}` : '',
1975
+ ].filter(Boolean).join('\n');
1976
+ return { success: true, output: out };
1977
+ }
1978
+ catch (e) {
1979
+ return { success: false, output: '', error: e.message };
1980
+ }
1981
+ },
1982
+ // ── ▲ swarm — run parallel subagents and aggregate results ───────────────
1983
+ swarm: async (p) => {
1984
+ const task = p.task || p.prompt || '';
1985
+ const n = typeof p.n === 'number' ? Math.max(2, Math.min(p.n, 5)) : 3;
1986
+ const strategy = p.strategy ?? 'vote';
1987
+ const timeout = typeof p.timeout === 'number' ? p.timeout : 90000;
1988
+ if (!task)
1989
+ return { success: false, output: '', error: 'No task provided' };
1990
+ try {
1991
+ const { swarmSubagents } = await Promise.resolve().then(() => __importStar(require('./swarmManager')));
1992
+ const { getBudgetState } = await Promise.resolve().then(() => __importStar(require('./agentLoop')));
1993
+ const budget = getBudgetState() ?? { current: 1, max: 10, remaining: 9 };
1994
+ const result = await swarmSubagents({ task, n, strategy, timeout, parentBudget: budget });
1995
+ const out = [
1996
+ result.result ?? '',
1997
+ `[swarm] agents=${result.agentsRun} strategy=${result.strategy} duration=${result.duration}ms`,
1998
+ ].filter(Boolean).join('\n');
1999
+ return { success: result.success, output: out, error: result.error };
2000
+ }
2001
+ catch (e) {
2002
+ return { success: false, output: '', error: e.message };
2003
+ }
2004
+ },
2005
+ // ── ▲ search — hybrid BM25 + semantic search over sessions & memory ─────
2006
+ search: async (p) => {
2007
+ const query = p.query || p.q || '';
2008
+ const topK = typeof p.topK === 'number' ? p.topK : 5;
2009
+ if (!query)
2010
+ return { success: false, output: '', error: 'No query provided' };
2011
+ try {
2012
+ const { hybridSearch } = await Promise.resolve().then(() => __importStar(require('./hybridSearch')));
2013
+ const hits = hybridSearch(query, { topK });
2014
+ if (!hits.length)
2015
+ return { success: true, output: 'No results found.' };
2016
+ const out = hits.map((h, i) => `[${i + 1}] (${(h.score * 100).toFixed(0)}%) ${h.title}\n ${h.snippet}`).join('\n\n');
2017
+ return { success: true, output: out };
2018
+ }
2019
+ catch (e) {
2020
+ return { success: false, output: '', error: e.message };
2021
+ }
2022
+ },
2023
+ // ── clarify — ask the user a multi-choice or free-text question mid-task ──
2024
+ clarify: async (p) => {
2025
+ const question = p.question || p.q || '';
2026
+ const options = Array.isArray(p.options) ? p.options : undefined;
2027
+ const allowFreeText = p.allow_free_text !== false;
2028
+ if (!question)
2029
+ return { success: false, output: '', error: 'No question provided' };
2030
+ try {
2031
+ const { ask } = await Promise.resolve().then(() => __importStar(require('./clarifyBus')));
2032
+ const answer = await ask(question, options, allowFreeText);
2033
+ return { success: true, output: answer };
2034
+ }
2035
+ catch (e) {
2036
+ return { success: false, output: '', error: e.message };
2037
+ }
2038
+ },
2039
+ // ── todo — per-session task list ──────────────────────────────────────────
2040
+ todo: async (p) => {
2041
+ const op = (p.op || p.operation || 'list').toLowerCase();
2042
+ try {
2043
+ const { addTodo, completeTodo, removeTodo, clearTodos, listTodos, formatTodoList, } = await Promise.resolve().then(() => __importStar(require('./todoManager')));
2044
+ if (op === 'add') {
2045
+ const text = p.text || p.item || '';
2046
+ if (!text)
2047
+ return { success: false, output: '', error: 'No text provided for add' };
2048
+ const item = addTodo(text, p.priority ?? 'normal');
2049
+ return { success: true, output: `Added [${item.id}]: ${item.text}` };
2050
+ }
2051
+ if (op === 'complete' || op === 'done') {
2052
+ const id = String(p.id ?? '');
2053
+ if (!id)
2054
+ return { success: false, output: '', error: 'No id provided' };
2055
+ const item = completeTodo(id);
2056
+ if (!item)
2057
+ return { success: false, output: '', error: `Todo ${id} not found` };
2058
+ return { success: true, output: `Completed [${item.id}]: ${item.text}` };
2059
+ }
2060
+ if (op === 'remove' || op === 'delete') {
2061
+ const id = String(p.id ?? '');
2062
+ if (!id)
2063
+ return { success: false, output: '', error: 'No id provided' };
2064
+ const ok = removeTodo(id);
2065
+ return { success: ok, output: ok ? `Removed todo ${id}` : `Todo ${id} not found` };
2066
+ }
2067
+ if (op === 'clear') {
2068
+ const n = clearTodos();
2069
+ return { success: true, output: `Cleared ${n} todo(s)` };
2070
+ }
2071
+ // Default: list
2072
+ const filter = (p.filter ?? 'all');
2073
+ const items = listTodos(filter);
2074
+ return { success: true, output: formatTodoList(items) };
2075
+ }
2076
+ catch (e) {
2077
+ return { success: false, output: '', error: e.message };
2078
+ }
2079
+ },
2080
+ // ── cronjob — scheduled task tool ────────────────────────────────────────
2081
+ cronjob: async (p) => {
2082
+ const op = (p.op || p.operation || 'list').toLowerCase();
2083
+ try {
2084
+ const { createJob, listJobs, pauseJob, resumeJob, deleteJob, triggerJob, getJob, } = await Promise.resolve().then(() => __importStar(require('./cronManager')));
2085
+ if (op === 'create') {
2086
+ const description = p.description || p.name || '';
2087
+ const schedule = p.schedule || '';
2088
+ const action = p.action || p.command || '';
2089
+ if (!schedule || !action) {
2090
+ return { success: false, output: '', error: 'schedule and action are required' };
2091
+ }
2092
+ const job = createJob(description || action, schedule, action);
2093
+ return { success: true, output: `Created job [${job.id}]: ${job.description} — ${job.schedule}` };
2094
+ }
2095
+ if (op === 'list') {
2096
+ const jobs = listJobs();
2097
+ if (!jobs.length)
2098
+ return { success: true, output: 'No cron jobs.' };
2099
+ const lines = jobs.map(j => {
2100
+ const status = j.enabled ? '▶' : '⏸';
2101
+ return `[${j.id}] ${status} ${j.description} | ${j.schedule} | runs: ${j.runCount} | next: ${j.nextRun ?? 'n/a'}`;
2102
+ });
2103
+ return { success: true, output: lines.join('\n') };
2104
+ }
2105
+ if (op === 'pause') {
2106
+ const id = String(p.id ?? '');
2107
+ const ok = pauseJob(id);
2108
+ return { success: ok, output: ok ? `Paused job ${id}` : `Job ${id} not found` };
2109
+ }
2110
+ if (op === 'resume') {
2111
+ const id = String(p.id ?? '');
2112
+ const ok = resumeJob(id);
2113
+ return { success: ok, output: ok ? `Resumed job ${id}` : `Job ${id} not found` };
2114
+ }
2115
+ if (op === 'delete' || op === 'remove') {
2116
+ const id = String(p.id ?? '');
2117
+ const ok = deleteJob(id);
2118
+ return { success: ok, output: ok ? `Deleted job ${id}` : `Job ${id} not found` };
2119
+ }
2120
+ if (op === 'trigger' || op === 'run') {
2121
+ const id = String(p.id ?? '');
2122
+ const ok = await triggerJob(id);
2123
+ return { success: ok, output: ok ? `Triggered job ${id}` : `Job ${id} not found` };
2124
+ }
2125
+ if (op === 'get') {
2126
+ const id = String(p.id ?? '');
2127
+ const job = getJob(id);
2128
+ if (!job)
2129
+ return { success: false, output: '', error: `Job ${id} not found` };
2130
+ return { success: true, output: JSON.stringify(job, null, 2) };
2131
+ }
2132
+ return { success: false, output: '', error: `Unknown op: ${op}` };
2133
+ }
2134
+ catch (e) {
2135
+ return { success: false, output: '', error: e.message };
2136
+ }
2137
+ },
2138
+ // ── vision_analyze — image analysis via provider vision APIs ─────────────
2139
+ vision_analyze: async (p) => {
2140
+ const imageSource = p.image || p.path || p.url || p.source || '';
2141
+ const prompt = p.prompt || p.question || 'Describe this image in detail.';
2142
+ if (!imageSource)
2143
+ return { success: false, output: '', error: 'No image source provided (use image, path, or url)' };
2144
+ try {
2145
+ const { analyzeImage } = await Promise.resolve().then(() => __importStar(require('./visionAnalyze')));
2146
+ const result = await analyzeImage(imageSource, prompt);
2147
+ return {
2148
+ success: true,
2149
+ output: `[${result.provider}/${result.modelUsed}] (${result.durationMs}ms)\n\n${result.description}`,
2150
+ };
2151
+ }
2152
+ catch (e) {
2153
+ return { success: false, output: '', error: e.message };
2154
+ }
2155
+ },
2156
+ // ── voice_speak — TTS with provider fallback chain ────────────────────────
2157
+ voice_speak: async (p) => {
2158
+ const text = p.text || p.command || '';
2159
+ if (!text)
2160
+ return { success: false, output: '', error: 'No text provided' };
2161
+ try {
2162
+ const { synthesize } = await Promise.resolve().then(() => __importStar(require('./voice/tts')));
2163
+ const result = await synthesize({
2164
+ text,
2165
+ voice: p.voice,
2166
+ rate: p.rate,
2167
+ volume: p.volume,
2168
+ provider: p.provider,
2169
+ timeoutMs: p.timeoutMs,
2170
+ });
2171
+ if (result.error)
2172
+ return { success: false, output: '', error: result.error };
2173
+ return { success: true, output: `Spoken via ${result.provider} (${result.durationMs}ms)` };
2174
+ }
2175
+ catch (e) {
2176
+ return { success: false, output: '', error: e.message };
2177
+ }
2178
+ },
2179
+ // ── voice_transcribe — STT with provider fallback chain ──────────────────
2180
+ voice_transcribe: async (p) => {
2181
+ const audioFilePath = p.audioFilePath || p.path || p.file || '';
2182
+ if (!audioFilePath)
2183
+ return { success: false, output: '', error: 'No audioFilePath provided' };
2184
+ try {
2185
+ const { transcribe } = await Promise.resolve().then(() => __importStar(require('./voice/stt')));
2186
+ const result = await transcribe({ audioFilePath, language: p.language });
2187
+ if (result.error)
2188
+ return { success: false, output: '', error: result.error };
2189
+ return {
2190
+ success: true,
2191
+ output: JSON.stringify({ text: result.text, provider: result.provider, durationMs: result.durationMs }),
2192
+ };
2193
+ }
2194
+ catch (e) {
2195
+ return { success: false, output: '', error: e.message };
2196
+ }
2197
+ },
2198
+ // ── schedule_reminder — one-shot or recurring desktop notification ──────────
2199
+ schedule_reminder: async (p) => {
2200
+ const message = p.message || p.text || '';
2201
+ const delayMs = typeof p.delayMs === 'number' ? p.delayMs
2202
+ : typeof p.delaySeconds === 'number' ? p.delaySeconds * 1000
2203
+ : typeof p.delayMinutes === 'number' ? p.delayMinutes * 60000
2204
+ : 0;
2205
+ const recurring = p.recurring; // 'hourly' | 'daily' | 'weekly' | undefined
2206
+ const op = (p.op || 'schedule').toLowerCase();
2207
+ try {
2208
+ const { scheduleReminder, listReminders, cancelReminder } = await Promise.resolve().then(() => __importStar(require('./scheduler')));
2209
+ if (op === 'list') {
2210
+ const items = listReminders();
2211
+ if (!items.length)
2212
+ return { success: true, output: 'No pending reminders.' };
2213
+ const lines = items.map(r => {
2214
+ const fireAt = new Date(r.fireAt).toLocaleString();
2215
+ const rec = r.recurring ? ` (${r.recurring})` : '';
2216
+ return `[${r.id}] ${r.message} — fires at ${fireAt}${rec}`;
2217
+ });
2218
+ return { success: true, output: lines.join('\n') };
2219
+ }
2220
+ if (op === 'cancel') {
2221
+ const id = String(p.id ?? '');
2222
+ const ok = cancelReminder(id);
2223
+ return { success: ok, output: ok ? `Cancelled reminder ${id}` : `Reminder ${id} not found` };
2224
+ }
2225
+ // Default: schedule
2226
+ if (!message)
2227
+ return { success: false, output: '', error: 'message is required' };
2228
+ if (delayMs <= 0 && !recurring)
2229
+ return { success: false, output: '', error: 'delayMs (or delaySeconds / delayMinutes) must be > 0' };
2230
+ const effectiveDelay = delayMs > 0 ? delayMs : (recurring === 'hourly' ? 3600000 : recurring === 'daily' ? 86400000 : recurring === 'weekly' ? 604800000 : 60000);
2231
+ const r = scheduleReminder(message, effectiveDelay, recurring);
2232
+ const when = new Date(r.fireAt).toLocaleString();
2233
+ const rec = recurring ? ` (repeats ${recurring})` : '';
2234
+ return { success: true, output: `Reminder [${r.id}] set for ${when}${rec}: "${message}"` };
2235
+ }
2236
+ catch (e) {
2237
+ return { success: false, output: '', error: e.message };
2238
+ }
2239
+ },
2240
+ // ── voice_clone — clone a voice from reference audio (VoxCPM / ElevenLabs) ─
2241
+ voice_clone: async (p) => {
2242
+ const text = p.text || '';
2243
+ const referenceAudioPath = p.referenceAudioPath || p.reference || p.ref || '';
2244
+ if (!text)
2245
+ return { success: false, output: '', error: 'No text provided' };
2246
+ if (!referenceAudioPath)
2247
+ return { success: false, output: '', error: 'No referenceAudioPath provided' };
2248
+ try {
2249
+ const { synthesize } = await Promise.resolve().then(() => __importStar(require('./voice/tts')));
2250
+ const result = await synthesize({
2251
+ text,
2252
+ voice: p.voice,
2253
+ provider: p.provider,
2254
+ referenceAudioPath,
2255
+ timeoutMs: p.timeoutMs ?? 120000,
2256
+ });
2257
+ if (result.error)
2258
+ return { success: false, output: '', error: result.error };
2259
+ return { success: true, output: `Voice cloned via ${result.provider} (${result.durationMs}ms)` };
2260
+ }
2261
+ catch (e) {
2262
+ return { success: false, output: '', error: e.message };
2263
+ }
2264
+ },
2265
+ // ── voice_design — synthesize with a text voice description (VoxCPM) ──────
2266
+ voice_design: async (p) => {
2267
+ const text = p.text || '';
2268
+ const voiceDescription = p.voiceDescription || p.description || p.design || '';
2269
+ if (!text)
2270
+ return { success: false, output: '', error: 'No text provided' };
2271
+ if (!voiceDescription)
2272
+ return { success: false, output: '', error: 'No voiceDescription provided' };
2273
+ try {
2274
+ const { synthesize } = await Promise.resolve().then(() => __importStar(require('./voice/tts')));
2275
+ const result = await synthesize({
2276
+ text: `design:${voiceDescription}\n${text}`,
2277
+ provider: p.provider,
2278
+ timeoutMs: p.timeoutMs ?? 120000,
2279
+ });
2280
+ if (result.error)
2281
+ return { success: false, output: '', error: result.error };
2282
+ return { success: true, output: `Voice designed via ${result.provider} (${result.durationMs}ms)` };
2283
+ }
2284
+ catch (e) {
2285
+ return { success: false, output: '', error: e.message };
2286
+ }
2287
+ },
2288
+ };
2289
+ // ── Plugin-registered tools ───────────────────────────────────
2290
+ const externalTools = {};
2291
+ const externalToolsMeta = {};
2292
+ function registerExternalTool(name, fn, source) {
2293
+ externalTools[name] = async (input) => {
2294
+ const r = await fn(input);
2295
+ return { success: r.success, output: r.output };
2296
+ };
2297
+ externalToolsMeta[name] = { source };
2298
+ if ((process.env.AIDEN_LOG_LEVEL || 'info') === 'debug') {
2299
+ console.log('[ToolRegistry] Plugin "' + source + '" registered tool: ' + name);
2300
+ }
2301
+ }
2302
+ /** Returns a snapshot of all plugin-registered tool metadata (source, etc.). */
2303
+ function getExternalToolsMeta() {
2304
+ return { ...externalToolsMeta };
2305
+ }
2306
+ // ── Internal dispatcher — no retry, no timeout ────────────────
2307
+ async function runTool(tool, input) {
2308
+ // Build per-call context with tool-scoped progress emitter
2309
+ const ctx = {
2310
+ emitProgress: _emitProgress
2311
+ ? (msg) => _emitProgress(tool, msg)
2312
+ : undefined,
2313
+ };
2314
+ // Core tool
2315
+ const fn = exports.TOOLS[tool];
2316
+ if (fn)
2317
+ return fn(input, ctx);
2318
+ // Plugin-registered tool
2319
+ if (externalTools[tool])
2320
+ return externalTools[tool](input);
2321
+ // ── MCP tool dispatch ─────────────────────────────────────
2322
+ // Tool names follow the pattern: mcp_<serverName>_<toolName>
2323
+ if (tool.startsWith('mcp_')) {
2324
+ const withoutPrefix = tool.slice(4); // drop "mcp_"
2325
+ const underIdx = withoutPrefix.indexOf('_');
2326
+ if (underIdx !== -1) {
2327
+ const serverName = withoutPrefix.slice(0, underIdx);
2328
+ const mcpToolName = withoutPrefix.slice(underIdx + 1);
2329
+ const result = await mcpClient_1.mcpClient.callTool(serverName, mcpToolName, input);
2330
+ return { success: result.success, output: result.output };
2331
+ }
2332
+ }
2333
+ // ── New-style colon-prefix MCP tool: 'github:list_issues' ────
2334
+ if (tool.includes(':')) {
2335
+ try {
2336
+ const { callMcpTool } = await Promise.resolve().then(() => __importStar(require('./mcpClient')));
2337
+ const result = await callMcpTool(tool, input);
2338
+ return {
2339
+ success: result.isError !== true,
2340
+ output: typeof result === 'string' ? result
2341
+ : result.content?.map((c) => c.text ?? JSON.stringify(c)).join('\n')
2342
+ ?? JSON.stringify(result),
2343
+ };
2344
+ }
2345
+ catch (e) {
2346
+ return { success: false, output: '', error: `MCP tool "${tool}" failed: ${e.message}` };
2347
+ }
2348
+ }
2349
+ // Last resort: try shell_exec
2350
+ const cmd = input?.command || '';
2351
+ if (cmd)
2352
+ return exports.TOOLS.shell_exec({ command: cmd });
2353
+ throw new Error(`Unknown tool: ${tool}`);
2354
+ }
2355
+ // ── Public executor — retry + per-tool timeout ────────────────
2356
+ // maxRetries: number of retries AFTER the first attempt (default 2 = 3 total tries)
2357
+ // timeoutMs: fallback timeout when tool has no entry in TOOL_TIMEOUTS
2358
+ async function executeTool(tool, input, maxRetries = 2, timeoutMs = 30000) {
2359
+ const start = Date.now();
2360
+ let lastError = '';
2361
+ let retries = 0;
2362
+ // ── Sprint 17: cache check ────────────────────────────────────
2363
+ const cachedOutput = responseCache_1.responseCache.get(tool, input);
2364
+ if (cachedOutput !== null) {
2365
+ return {
2366
+ tool, input,
2367
+ success: true,
2368
+ output: cachedOutput,
2369
+ duration: Date.now() - start,
2370
+ retries: 0,
2371
+ };
2372
+ }
2373
+ const timeout = TOOL_TIMEOUTS[tool] ?? timeoutMs;
2374
+ // Errors that should not be retried (permanent failures)
2375
+ const NO_RETRY_PATTERNS = [
2376
+ 'not found', 'permission denied', 'invalid input',
2377
+ 'file not found', 'syntax error', 'enoent', 'no path', 'no url',
2378
+ 'no query', 'no script', 'no command', 'unknown tool',
2379
+ ];
2380
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2381
+ if (attempt > 0) {
2382
+ retries++;
2383
+ // Exponential backoff: 1s, 2s, 4s
2384
+ await new Promise(r => setTimeout(r, Math.pow(2, attempt - 1) * 1000));
2385
+ console.log(`[Executor] Retry ${attempt}/${maxRetries} for ${tool}`);
2386
+ }
2387
+ try {
2388
+ const raw = await Promise.race([
2389
+ runTool(tool, input),
2390
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool timeout after ${timeout}ms`)), timeout)),
2391
+ ]);
2392
+ const result = {
2393
+ tool, input,
2394
+ success: raw.success,
2395
+ output: String(raw.output || ''),
2396
+ error: raw.error,
2397
+ duration: Date.now() - start,
2398
+ retries,
2399
+ };
2400
+ // ── Sprint 17: cache successful results ───────────────────
2401
+ if (result.success && result.output) {
2402
+ responseCache_1.responseCache.set(tool, input, result.output);
2403
+ }
2404
+ return result;
2405
+ }
2406
+ catch (e) {
2407
+ lastError = e.message || String(e);
2408
+ console.warn(`[Executor] ${tool} attempt ${attempt + 1} failed: ${lastError.slice(0, 120)}`);
2409
+ // Don't retry on permanent errors
2410
+ if (NO_RETRY_PATTERNS.some(p => lastError.toLowerCase().includes(p))) {
2411
+ break;
2412
+ }
2413
+ }
2414
+ }
2415
+ return {
2416
+ tool, input,
2417
+ success: false,
2418
+ output: '',
2419
+ error: lastError,
2420
+ duration: Date.now() - start,
2421
+ retries,
2422
+ };
2423
+ }
2424
+ // ── Sprint 29: TOOL_DESCRIPTIONS ────────────────────────────────
2425
+ // Human-readable descriptions for all tools, used by the MCP server to advertise
2426
+ // capabilities to Claude Desktop and other MCP clients.
2427
+ exports.TOOL_DESCRIPTIONS = {
2428
+ web_search: 'Search the web for current information, news, or any topic',
2429
+ fetch_url: 'Fetch the content of any URL and return the text',
2430
+ fetch_page: 'Fetch a web page and extract its readable text content',
2431
+ deep_research: 'Conduct thorough multi-step research on a topic using multiple sources',
2432
+ open_browser: 'Open a URL in the system browser',
2433
+ browser_click: 'Click on an element in the browser by selector',
2434
+ browser_scroll: '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)',
2435
+ browser_type: 'Type text into a browser input field',
2436
+ browser_extract: 'Extract text content from the current browser page',
2437
+ browser_screenshot: 'Take a screenshot of the current browser window',
2438
+ browser_get_url: 'Return the URL currently loaded in the browser',
2439
+ file_write: 'Write content to a file at the specified path',
2440
+ file_read: 'Read the contents of a file at the specified path',
2441
+ file_list: 'List files in a directory',
2442
+ shell_exec: 'Execute a shell/PowerShell command and return the output',
2443
+ run_powershell: 'Run a PowerShell command on Windows',
2444
+ cmd: 'Run a Windows cmd.exe command and return stdout/stderr/exitCode',
2445
+ ps: 'Run a PowerShell command directly (no temp file) and return stdout/stderr/exitCode',
2446
+ wsl: 'Run a bash command inside WSL (Windows Subsystem for Linux); auto-translates C:\\ paths to /mnt/c/',
2447
+ run_python: 'Execute a Python script and return stdout/stderr',
2448
+ run_node: 'Execute Node.js/JavaScript code and return the output',
2449
+ system_info: 'Get system hardware and OS information (CPU, RAM, disk, OS)',
2450
+ notify: 'Send a desktop notification to the user',
2451
+ get_stocks: 'Get top gainers, losers, or most active stocks from NSE/BSE',
2452
+ get_market_data: 'Get real-time price, change%, and volume for a stock symbol',
2453
+ get_company_info: 'Get company profile, sector, P/E ratio, EPS, and revenue',
2454
+ social_research: 'Research a person or company across social and public sources',
2455
+ mouse_move: 'Move the mouse cursor to screen coordinates',
2456
+ mouse_click: 'Click the mouse at screen coordinates',
2457
+ keyboard_type: 'Type text using the keyboard',
2458
+ keyboard_press: 'Press a keyboard key or shortcut (e.g. ctrl+c)',
2459
+ screenshot: 'Take a screenshot of the entire screen',
2460
+ screen_read: 'Read and describe the current screen contents',
2461
+ vision_loop: 'Autonomously control the computer using vision to complete a goal',
2462
+ wait: 'Pause execution for a specified number of milliseconds',
2463
+ code_interpreter_python: 'Run Python code in a sandboxed interpreter with data science libraries',
2464
+ code_interpreter_node: 'Run Node.js code in a sandboxed interpreter',
2465
+ run_agent: 'Spawn a sub-agent to complete a sub-goal autonomously',
2466
+ git_status: 'Show git status and recent commits for a repository. Provide path parameter for a specific directory.',
2467
+ git_commit: 'Stage and commit files to a local git repository',
2468
+ git_push: 'Push committed changes to a remote git repository',
2469
+ clipboard_read: 'Read the current contents of the system clipboard',
2470
+ clipboard_write: 'Write text to the system clipboard',
2471
+ window_list: 'List all open windows on the desktop',
2472
+ window_focus: 'Bring a specific window to the foreground by title',
2473
+ app_launch: 'Launch an application by name or executable path',
2474
+ app_close: 'Close an application by window title',
2475
+ watch_folder: 'Watch a folder and react automatically when new files appear',
2476
+ watch_folder_list: 'List all currently watched folder paths',
2477
+ get_briefing: 'Run the morning briefing: weather, markets, news, and daily summary',
2478
+ respond: '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.',
2479
+ manage_goals: '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.',
2480
+ get_calendar: 'Get upcoming calendar events from Google Calendar (requires iCal URL in Settings → Channels). Parameters: daysAhead (number, default 7).',
2481
+ read_email: 'Read recent unread emails from Gmail (requires App Password in Settings → Channels). Parameters: count (number, default 10), folder (string, default INBOX).',
2482
+ send_email: 'Send an email via Gmail (requires App Password in Settings → Channels). Parameters: to (string), subject (string), body (string).',
2483
+ compact_context: 'Summarize and compress the current conversation context. Saves session to disk and extracts durable memories. Call when context is getting long.',
2484
+ lookup_skill: '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.',
2485
+ get_natural_events: 'Fetch active natural events from NASA EONET API. Returns current earthquakes, wildfires, storms, floods, and other natural events worldwide.',
2486
+ voice_speak: 'Speak text aloud using the TTS provider chain (VoxCPM → Edge TTS → ElevenLabs → SAPI). Accepts text, voice, rate, volume, provider overrides.',
2487
+ voice_transcribe: 'Transcribe an audio file to text using the STT provider chain (Groq Whisper → OpenAI Whisper → Whisper.cpp). Returns { text, provider, durationMs }.',
2488
+ voice_clone: 'Clone a voice from a reference audio file and synthesize new text. Requires text and referenceAudioPath. Uses VoxCPM when USE_VOXCPM=1.',
2489
+ voice_design: 'Design a custom voice from a text description and synthesize text with it. Requires text and voiceDescription. Uses VoxCPM when USE_VOXCPM=1.',
2490
+ schedule_reminder: '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.',
2491
+ lookup_tool_schema: 'Get the full description for a named tool. Call before using an unfamiliar tool.',
2492
+ spawn: 'Delegate a sub-task to an isolated subagent with its own context and half the remaining iteration budget. Returns the subagent\'s synthesized answer.',
2493
+ spawn_subagent: '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.',
2494
+ 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.',
2495
+ send_file_local: 'Send a file to another device on the local network via LocalSend (op: discover | send)',
2496
+ receive_file_local: 'Wait for an incoming LocalSend file transfer on the local network',
2497
+ };
2498
+ // ── N+28: TOOL_NAMES_ONLY ──────────────────────────────────────
2499
+ // One-liner per tool — first sentence of TOOL_DESCRIPTIONS, truncated to 60 chars.
2500
+ // Used in the planner prompt to list available tools without bloating token count.
2501
+ exports.TOOL_NAMES_ONLY = Object.fromEntries(Object.entries(exports.TOOL_DESCRIPTIONS).map(([name, desc]) => {
2502
+ const first = desc.split(/[.,(]/)[0].trim();
2503
+ return [name, first.length > 60 ? first.slice(0, 57) + '...' : first];
2504
+ }));
2505
+ const TOOL_TIERS = {
2506
+ // Tier 0 — Always-on: scheduling, instant response
2507
+ schedule_reminder: 0,
2508
+ // Tier 1 — APIs, data, search, notify, respond
2509
+ respond: 1,
2510
+ manage_goals: 1,
2511
+ compact_context: 1,
2512
+ lookup_skill: 1,
2513
+ lookup_tool_schema: 1,
2514
+ web_search: 1,
2515
+ fetch_url: 1,
2516
+ fetch_page: 1,
2517
+ deep_research: 1,
2518
+ get_stocks: 1,
2519
+ get_market_data: 1,
2520
+ get_company_info: 1,
2521
+ social_research: 1,
2522
+ system_info: 1,
2523
+ notify: 1,
2524
+ wait: 1,
2525
+ get_briefing: 1,
2526
+ get_natural_events: 1,
2527
+ get_calendar: 1,
2528
+ read_email: 1,
2529
+ send_email: 1,
2530
+ run_agent: 1,
2531
+ spawn: 2,
2532
+ spawn_subagent: 2,
2533
+ swarm: 2,
2534
+ // Tier 2 — File system, shell, code execution
2535
+ file_write: 2,
2536
+ file_read: 2,
2537
+ file_list: 2,
2538
+ shell_exec: 2,
2539
+ run_powershell: 2,
2540
+ cmd: 2,
2541
+ ps: 2,
2542
+ wsl: 2,
2543
+ run_python: 2,
2544
+ run_node: 2,
2545
+ code_interpreter_python: 2,
2546
+ code_interpreter_node: 2,
2547
+ git_status: 2,
2548
+ git_commit: 2,
2549
+ git_push: 2,
2550
+ clipboard_read: 2,
2551
+ clipboard_write: 2,
2552
+ watch_folder: 2,
2553
+ watch_folder_list: 2,
2554
+ send_file_local: 2,
2555
+ receive_file_local: 2,
2556
+ // Tier 3 — Browser automation
2557
+ open_browser: 3,
2558
+ browser_click: 3,
2559
+ browser_scroll: 3,
2560
+ browser_type: 3,
2561
+ browser_extract: 3,
2562
+ browser_screenshot: 3,
2563
+ browser_get_url: 3,
2564
+ window_list: 3,
2565
+ window_focus: 3,
2566
+ app_launch: 3,
2567
+ app_close: 3,
2568
+ // Voice tools — Tier 2 (subprocess / local model)
2569
+ voice_speak: 2,
2570
+ voice_transcribe: 2,
2571
+ voice_clone: 2,
2572
+ voice_design: 2,
2573
+ // Tier 4 — Screen/mouse/keyboard (last resort)
2574
+ mouse_move: 4,
2575
+ mouse_click: 4,
2576
+ keyboard_type: 4,
2577
+ keyboard_press: 4,
2578
+ screenshot: 4,
2579
+ screen_read: 4,
2580
+ vision_loop: 4,
2581
+ };
2582
+ function getToolTier(toolName) {
2583
+ if (toolName.startsWith('mcp_'))
2584
+ return 1;
2585
+ return TOOL_TIERS[toolName] ?? 2;
2586
+ }
2587
+ const TOOL_CATEGORIES = {
2588
+ respond: ['core'],
2589
+ manage_goals: ['core'],
2590
+ compact_context: ['core'],
2591
+ run_agent: ['core'],
2592
+ lookup_skill: ['core'],
2593
+ lookup_tool_schema: ['core'],
2594
+ web_search: ['web', 'data'],
2595
+ deep_research: ['web'],
2596
+ fetch_url: ['web'],
2597
+ fetch_page: ['web'],
2598
+ social_research: ['web', 'data'],
2599
+ file_read: ['files'],
2600
+ file_write: ['files'],
2601
+ file_list: ['files'],
2602
+ watch_folder: ['files', 'system'],
2603
+ watch_folder_list: ['files', 'system'],
2604
+ run_python: ['code'],
2605
+ run_node: ['code'],
2606
+ shell_exec: ['code', 'system'],
2607
+ run_powershell: ['code', 'system'],
2608
+ cmd: ['code', 'system'],
2609
+ ps: ['code', 'system'],
2610
+ wsl: ['code', 'system'],
2611
+ code_interpreter_python: ['code'],
2612
+ code_interpreter_node: ['code'],
2613
+ open_browser: ['browser'],
2614
+ browser_click: ['browser'],
2615
+ browser_scroll: ['browser'],
2616
+ browser_type: ['browser'],
2617
+ browser_extract: ['browser'],
2618
+ browser_screenshot: ['browser'],
2619
+ browser_get_url: ['browser'],
2620
+ window_list: ['browser', 'system'],
2621
+ window_focus: ['browser', 'system'],
2622
+ app_launch: ['browser', 'system'],
2623
+ app_close: ['browser', 'system'],
2624
+ screenshot: ['screen'],
2625
+ mouse_move: ['screen'],
2626
+ mouse_click: ['screen'],
2627
+ keyboard_type: ['screen'],
2628
+ keyboard_press: ['screen'],
2629
+ screen_read: ['screen'],
2630
+ vision_loop: ['screen'],
2631
+ get_market_data: ['data'],
2632
+ get_company_info: ['data'],
2633
+ get_stocks: ['data'],
2634
+ get_briefing: ['data'],
2635
+ get_natural_events: ['data'],
2636
+ notify: ['system'],
2637
+ system_info: ['system'],
2638
+ wait: ['system', 'browser', 'screen'],
2639
+ clipboard_read: ['system', 'code'],
2640
+ clipboard_write: ['system', 'code'],
2641
+ git_status: ['git'],
2642
+ git_commit: ['git'],
2643
+ git_push: ['git'],
2644
+ ingest_youtube: ['web', 'memory'],
2645
+ get_calendar: ['data', 'system'],
2646
+ read_email: ['data', 'system'],
2647
+ send_email: ['data', 'system'],
2648
+ send_file_local: ['files', 'system'],
2649
+ receive_file_local: ['files', 'system'],
2650
+ // slash-mirror introspection tools
2651
+ status: ['introspection'],
2652
+ analytics: ['introspection'],
2653
+ spend: ['introspection'],
2654
+ memory_show: ['introspection', 'memory'],
2655
+ lessons: ['introspection', 'memory'],
2656
+ skills_list: ['introspection'],
2657
+ tools_list: ['introspection'],
2658
+ whoami: ['introspection'],
2659
+ channels_status: ['introspection'],
2660
+ goals: ['introspection', 'memory'],
2661
+ run: ['code'],
2662
+ spawn: ['delegation'],
2663
+ spawn_subagent: ['delegation'],
2664
+ swarm: ['delegation'],
2665
+ search: ['memory', 'introspection'],
2666
+ clarify: ['interaction', 'core'],
2667
+ todo: ['interaction', 'core'],
2668
+ cronjob: ['system', 'core'],
2669
+ vision_analyze: ['screen', 'data'],
2670
+ voice_speak: ['voice'],
2671
+ voice_transcribe: ['voice'],
2672
+ voice_clone: ['voice'],
2673
+ voice_design: ['voice'],
2674
+ };
2675
+ function detectToolCategories(message) {
2676
+ const categories = new Set(['core']);
2677
+ const msg = message.toLowerCase();
2678
+ if (/search|research|find|look up|what is|who is|latest|news|article|google/i.test(msg))
2679
+ categories.add('web');
2680
+ if (/file|read|write|save|create|folder|directory|pdf|document|\.txt|\.csv|\.json|\.md/i.test(msg))
2681
+ categories.add('files');
2682
+ if (/code|script|python|node|run|execute|build|deploy|npm|pip|function|class|powershell/i.test(msg))
2683
+ categories.add('code');
2684
+ if (/open|browse|website|url|http|chrome|click|navigate|youtube|browser|tab/i.test(msg))
2685
+ categories.add('browser');
2686
+ if (/screen|screenshot|mouse|click on|type in|desktop|window|app\b|vision|control/i.test(msg))
2687
+ categories.add('screen');
2688
+ if (/stock|nifty|market|price|nse|bse|sensex|reliance|trading|shares|equity|briefing|weather|natural|earthquake/i.test(msg))
2689
+ categories.add('data');
2690
+ if (/notify|notification|remind|alert|system info|cpu|ram|disk|hardware|clipboard|launch|close app/i.test(msg))
2691
+ categories.add('system');
2692
+ 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))
2693
+ categories.add('voice');
2694
+ if (/play audio|play music|media file|audio file/i.test(msg))
2695
+ categories.add('media');
2696
+ if (/\bgit\b|commit|push|pull|branch|merge|git status|diff|repo|repository/i.test(msg))
2697
+ categories.add('git');
2698
+ if (/remember|memory|forget|knowledge|learn|recall/i.test(msg))
2699
+ categories.add('memory');
2700
+ if (/status|uptime|analytics|how much.*spent|spending|cost|lessons|skills|what tools|tools (do you|available)|who am i|whoami|channels|providers|my goals|active goals/i.test(msg))
2701
+ categories.add('introspection');
2702
+ if (/spawn|swarm|subagent|delegate|parallel agent|fork agent/i.test(msg))
2703
+ categories.add('delegation');
2704
+ if (/todo|task list|add task|complete task|checklist|mark done/i.test(msg))
2705
+ categories.add('interaction');
2706
+ if (/clarify|ask me|which option|confirm|choose|multiple choice/i.test(msg))
2707
+ categories.add('interaction');
2708
+ if (/cron|schedule|every \d|daily|hourly|recurring|repeat|interval/i.test(msg))
2709
+ categories.add('system');
2710
+ if (/analyze image|vision|describe image|what.*image|image.*show|photo|screenshot.*describe/i.test(msg))
2711
+ categories.add('screen');
2712
+ // Travel queries need web (search), browser (agent-browser navigation), and code (shell_exec
2713
+ // to invoke agent-browser CLI commands). Without 'code', shell_exec is absent from plannerTools
2714
+ // and the LLM cannot execute agent-browser commands even when a travel skill is surfaced.
2715
+ if (/flight|flights|airfare|airline|airport|booking|hotel|hotels|\btravel\b|itinerary|visa|pnr|layover|nonstop|stopover/i.test(msg)) {
2716
+ categories.add('web');
2717
+ categories.add('browser');
2718
+ categories.add('code');
2719
+ }
2720
+ return Array.from(categories);
2721
+ }
2722
+ function getToolsForCategories(categories) {
2723
+ const tools = new Set();
2724
+ for (const [toolName, toolCats] of Object.entries(TOOL_CATEGORIES)) {
2725
+ if (toolCats.some(c => categories.includes(c))) {
2726
+ tools.add(toolName);
2727
+ }
2728
+ }
2729
+ return Array.from(tools);
2730
+ }