ft-scout 9.0.3 → 9.0.5

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 (84) hide show
  1. package/.firebase/hosting.d2Vi.cache +7 -7
  2. package/README.md +76 -83
  3. package/bin/src/commands/agent.d.ts.map +1 -1
  4. package/bin/src/commands/agent.js +31 -228
  5. package/bin/src/commands/agent.js.map +1 -1
  6. package/bin/src/commands/plan.d.ts.map +1 -1
  7. package/bin/src/commands/plan.js +45 -18
  8. package/bin/src/commands/plan.js.map +1 -1
  9. package/bin/src/engine/actionVerifier.d.ts.map +1 -1
  10. package/bin/src/engine/actionVerifier.js +61 -9
  11. package/bin/src/engine/actionVerifier.js.map +1 -1
  12. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  13. package/bin/src/engine/agentEngine.js +701 -553
  14. package/bin/src/engine/agentEngine.js.map +1 -1
  15. package/bin/src/engine/agentState.d.ts.map +1 -1
  16. package/bin/src/engine/agentState.js +0 -64
  17. package/bin/src/engine/agentState.js.map +1 -1
  18. package/bin/src/engine/browser/browserController.d.ts.map +1 -1
  19. package/bin/src/engine/browser/browserController.js +8 -0
  20. package/bin/src/engine/browser/browserController.js.map +1 -1
  21. package/bin/src/engine/computer/computerController.d.ts.map +1 -1
  22. package/bin/src/engine/computer/computerController.js +686 -240
  23. package/bin/src/engine/computer/computerController.js.map +1 -1
  24. package/bin/src/engine/computer/computerDiagnostic.d.ts.map +1 -0
  25. package/bin/src/engine/computer/computerDiagnostic.js +71 -0
  26. package/bin/src/engine/computer/computerDiagnostic.js.map +1 -0
  27. package/bin/src/engine/computer/index.d.ts.map +1 -1
  28. package/bin/src/engine/computer/index.js +2 -0
  29. package/bin/src/engine/computer/index.js.map +1 -1
  30. package/bin/src/engine/computer/mockComputer.d.ts.map +1 -0
  31. package/bin/src/engine/computer/mockComputer.js +260 -0
  32. package/bin/src/engine/computer/mockComputer.js.map +1 -0
  33. package/bin/src/engine/computer/scoutDriver.cs +873 -0
  34. package/bin/src/engine/computer/scout_driver.exe +0 -0
  35. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -0
  36. package/bin/src/engine/runtime/agentRuntime.js +1160 -0
  37. package/bin/src/engine/runtime/agentRuntime.js.map +1 -0
  38. package/bin/src/engine/runtime/index.d.ts.map +1 -0
  39. package/bin/src/engine/runtime/index.js +2 -0
  40. package/bin/src/engine/runtime/index.js.map +1 -0
  41. package/bin/src/engine/safety.d.ts.map +1 -1
  42. package/bin/src/engine/safety.js +59 -25
  43. package/bin/src/engine/safety.js.map +1 -1
  44. package/bin/src/engine/toolRegistry.d.ts.map +1 -1
  45. package/bin/src/engine/toolRegistry.js +293 -68
  46. package/bin/src/engine/toolRegistry.js.map +1 -1
  47. package/bin/src/engine/voiceEngine.d.ts.map +1 -1
  48. package/bin/src/engine/voiceEngine.js +0 -1
  49. package/bin/src/engine/voiceEngine.js.map +1 -1
  50. package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -0
  51. package/bin/src/engine/workspace/agentWorkspace.js +505 -0
  52. package/bin/src/engine/workspace/agentWorkspace.js.map +1 -0
  53. package/bin/src/engine/workspace/index.d.ts.map +1 -0
  54. package/bin/src/engine/workspace/index.js +2 -0
  55. package/bin/src/engine/workspace/index.js.map +1 -0
  56. package/bin/src/index.js +4 -5
  57. package/bin/src/index.js.map +1 -1
  58. package/bin/src/server/server.d.ts.map +1 -1
  59. package/bin/src/server/server.js +25 -4
  60. package/bin/src/server/server.js.map +1 -1
  61. package/bin/src/utils/auth.d.ts.map +1 -1
  62. package/bin/src/utils/auth.js +81 -50
  63. package/bin/src/utils/auth.js.map +1 -1
  64. package/bin/src/utils/branding.js +1 -1
  65. package/bin/src/utils/payment.d.ts.map +1 -1
  66. package/bin/src/utils/payment.js +15 -1
  67. package/bin/src/utils/payment.js.map +1 -1
  68. package/bin/src/utils/pricing.js +1 -1
  69. package/bin/src/utils/pricing.js.map +1 -1
  70. package/bin/src/utils/razorpay.d.ts.map +1 -1
  71. package/bin/src/utils/razorpay.js +10 -2
  72. package/bin/src/utils/razorpay.js.map +1 -1
  73. package/downloads/invoice-2026-03.pdf +1 -0
  74. package/package.json +1 -1
  75. package/web/app.js +54 -135
  76. package/web/styles.css +2 -2
  77. package/bin/src/engine/appControl.d.ts.map +0 -1
  78. package/bin/src/engine/appControl.js +0 -2413
  79. package/bin/src/engine/appControl.js.map +0 -1
  80. package/bin/src/engine/liveScreenEngine.d.ts.map +0 -1
  81. package/bin/src/engine/liveScreenEngine.js +0 -421
  82. package/bin/src/engine/liveScreenEngine.js.map +0 -1
  83. package/bin/src/engine/scripts/winCapture.ps1 +0 -248
  84. package/downloads/INVOICE_2026_ACME.pdf +0 -1
@@ -0,0 +1,1160 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { spinner, confirm, isCancel, text } from '@clack/prompts';
5
+ import { safeNote, renderMarkdown } from '../../utils/markdown.js';
6
+ import { openai, callOpenAIWithRetry, sanitizeMessage, sanitizeMessages } from '../llm.js';
7
+ import { checkFileSyntax, verifyAndSelfHealFiles } from '../verifier.js';
8
+ import { speakText } from '../voiceEngine.js';
9
+ import { getDirectoryFiles, recordHistoryAction } from '../../utils/config.js';
10
+ import { saveAgentSession, getRecentSessionsSummary } from '../../utils/session.js';
11
+ import { AgentWorkspace, resolveDesktopPath } from '../workspace/agentWorkspace.js';
12
+ import { SystemComputerController } from '../computer/computerController.js';
13
+ import { MockComputerController } from '../computer/mockComputer.js';
14
+ import { SemanticBrowserController } from '../browser/index.js';
15
+ import { ActionVerifier } from '../actionVerifier.js';
16
+ import { ApprovalManager, RiskClassifier } from '../safety.js';
17
+ import { TaskPlanner } from '../planner.js';
18
+ import { UnifiedToolRegistry } from '../toolRegistry.js';
19
+ import { robustSnippetReplace, buildDirectoryTree } from '../agentEngine.js';
20
+ import { loadAuthConfig, isProOrHigher } from '../../utils/auth.js';
21
+ export class AgentRuntime {
22
+ cwd;
23
+ autoApprove;
24
+ maxSteps;
25
+ mockMode;
26
+ language;
27
+ abortController = new AbortController();
28
+ isCancelled = false;
29
+ cancelReason;
30
+ workspace;
31
+ computerController;
32
+ browserController;
33
+ actionVerifier;
34
+ approvalManager;
35
+ planner;
36
+ toolRegistry;
37
+ historyMessages = [];
38
+ modifiedFiles = new Set();
39
+ backups = [];
40
+ totalToolCalls = 0;
41
+ currentStep = 0;
42
+ startTime = Date.now();
43
+ readFilesHistory = new Set();
44
+ toolCallHistory = [];
45
+ screenshotCount = 0;
46
+ userTier;
47
+ static MAX_SCREENSHOTS_IN_HISTORY = 5;
48
+ static SCREENSHOT_MAX_DIMENSION = 1280;
49
+ constructor(options) {
50
+ this.cwd = options?.workspaceDir || process.cwd();
51
+ this.autoApprove = Boolean(options?.autoApprove);
52
+ this.mockMode = Boolean(options?.mockMode);
53
+ this.language = options?.language;
54
+ this.userTier = (options?.tier || (options?.mockMode ? 'pro' : loadAuthConfig().currentTier) || 'free').toLowerCase();
55
+ // Free tier is bounded to basic direct actions (max 5 steps). Full autonomous multi-step loops require Pro/Enterprise.
56
+ const isPaid = this.mockMode || this.userTier === 'pro' || this.userTier === 'enterprise';
57
+ const requestedSteps = options?.maxSteps || 30;
58
+ this.maxSteps = isPaid ? requestedSteps : Math.min(requestedSteps, 5);
59
+ this.workspace = new AgentWorkspace(options?.taskId, '', this.cwd);
60
+ this.computerController = options?.computerController || (this.mockMode ? new MockComputerController() : new SystemComputerController(this.cwd));
61
+ this.browserController = options?.browserController || new SemanticBrowserController(this.cwd);
62
+ this.actionVerifier = new ActionVerifier(this.cwd);
63
+ this.approvalManager = new ApprovalManager(this.autoApprove);
64
+ this.planner = new TaskPlanner();
65
+ this.toolRegistry = new UnifiedToolRegistry();
66
+ }
67
+ isProOrEnterprise() {
68
+ if (this.mockMode || (this.computerController && this.computerController.constructor.name === 'MockComputerController')) {
69
+ return true;
70
+ }
71
+ return isProOrHigher(this.userTier);
72
+ }
73
+ getWorkspace() {
74
+ return this.workspace;
75
+ }
76
+ getComputerController() {
77
+ return this.computerController;
78
+ }
79
+ getBrowserController() {
80
+ return this.browserController;
81
+ }
82
+ getActionVerifier() {
83
+ return this.actionVerifier;
84
+ }
85
+ getApprovalManager() {
86
+ return this.approvalManager;
87
+ }
88
+ getPlanner() {
89
+ return this.planner;
90
+ }
91
+ getToolRegistry() {
92
+ return this.toolRegistry;
93
+ }
94
+ cancel(reason = 'User cancelled task') {
95
+ this.isCancelled = true;
96
+ this.cancelReason = reason;
97
+ this.abortController.abort();
98
+ this.workspace.setStatus('cancelled');
99
+ this.workspace.recordError(`Task execution cancelled: ${reason}`, 'runtime', undefined, false);
100
+ }
101
+ isTaskCancelled() {
102
+ return this.isCancelled;
103
+ }
104
+ getModifiedFiles() {
105
+ return Array.from(this.modifiedFiles);
106
+ }
107
+ getBackups() {
108
+ return this.backups;
109
+ }
110
+ getStats() {
111
+ return {
112
+ totalSteps: this.currentStep,
113
+ totalToolCalls: this.totalToolCalls,
114
+ modifiedFilesCount: this.modifiedFiles.size,
115
+ elapsedMs: Date.now() - this.startTime,
116
+ taskId: this.workspace.getData().taskId,
117
+ status: this.workspace.getData().status,
118
+ };
119
+ }
120
+ resetContext() {
121
+ this.historyMessages = [];
122
+ this.modifiedFiles.clear();
123
+ this.readFilesHistory.clear();
124
+ this.backups = [];
125
+ this.startTime = Date.now();
126
+ this.totalToolCalls = 0;
127
+ this.currentStep = 0;
128
+ this.toolCallHistory = [];
129
+ this.isCancelled = false;
130
+ this.cancelReason = undefined;
131
+ this.abortController = new AbortController();
132
+ }
133
+ buildSystemPrompt() {
134
+ const filesList = getDirectoryFiles(this.cwd).slice(0, 100);
135
+ const workspaceState = this.workspace.compactWorkspaceForPrompt();
136
+ const tierNotice = !this.isProOrEnterprise()
137
+ ? `\nTIER RESTRICTION (FREE DEVELOPER TIER):
138
+ - You are operating under the Free Developer Tier.
139
+ - Free developers have access to basic developer operations: code editing, creating new files and folders, reading files, and running terminal commands.
140
+ - Scratchpad reasoning and autonomous computer/browser automation tools are locked to Pro & Enterprise plan users.
141
+ - Do NOT attempt to use scratchpad, computer_* or browser_* tools. Complete coding and task requests using direct workspace tools (edit_file, write_file, read_file, run_command, list_dir, grep_search, etc.).`
142
+ : `\nTIER: PRO/ENTERPRISE (Full Autonomous Computer Control, Browser Automation & AI Scratchpad Unlocked).`;
143
+ return `You are Scout Agent, an elite AI Principal Software Engineer & Autonomous Computer-Use Operator (developed by FrontTerrain).
144
+ You do not merely explain or output chat answers—you autonomously operate the computer, applications, browser, and workspace tools to fulfill the user's objective end-to-end.
145
+
146
+ Execution Cycle:
147
+ UNDERSTAND ➔ PLAN ➔ ACT ➔ OBSERVE ➔ UPDATE WORKSPACE ➔ REASON ABOUT STATE ➔ ACT AGAIN ➔ VERIFY ➔ COMPLETE
148
+
149
+ Working Directory: "${this.cwd}"
150
+ Host OS Platform: "${process.platform}"
151
+ Current Year: ${new Date().getFullYear()}
152
+
153
+ ${tierNotice}
154
+
155
+ ${workspaceState}
156
+
157
+ Available Workspace Files (sample):
158
+ ${filesList.join('\n')}
159
+
160
+ CORE ARCHITECTURAL PRINCIPLES:
161
+ 1. USER INTENT > TOOL CONVENIENCE (MANDATORY):
162
+ The easiest tool is NOT necessarily the correct tool. Scout optimizes for completing the user's ACTUAL REQUESTED WORKFLOW, not merely producing something that looks like the final artifact.
163
+ - Distinguish OUTCOME requirements (e.g. "Create a file containing X") from EXECUTION requirements (e.g. "Open Notepad, type X, save it as Y on Desktop").
164
+ - When the user explicitly specifies an application (e.g. Notepad, Excel, Chrome, VS Code) or computer interaction method, computer interaction tools (computer_open_app, computer_screenshot, computer_type, computer_hotkey, computer_switch_window, computer_click) MUST be used.
165
+ - NEVER silently substitute direct filesystem API calls (write_file) when computer/application interaction is required, unless the user explicitly permits alternative tools ("using any method you prefer").
166
+ - NEVER assume the repository root is the user's Desktop! If the user requests saving to Desktop, the file MUST be saved to the actual Desktop through the requested workflow.
167
+
168
+ 2. COMPUTER ACTION FAILURE RECOVERY:
169
+ When a computer action fails or encounters a problem:
170
+ DO NOT immediately switch to an unrelated tool (like write_file).
171
+ Instead, follow the bounded computer recovery loop:
172
+ ACTION FAILED ➔ OBSERVE SCREEN (computer_screenshot / computer_inspect_ui) ➔ UNDERSTAND FAILURE (check if application is active/focused) ➔ RETRY / ALTERNATIVE COMPUTER ACTION (switch window, click text area, send hotkey/keystrokes) ➔ OBSERVE ➔ CONTINUE.
173
+ Only after repeated bounded computer recovery attempts fail should you report the failure to the user.
174
+
175
+ 3. ACTIVE APPLICATION TRACKING:
176
+ Before sending keyboard or mouse input (computer_type, computer_click, computer_hotkey), ensure that the expected application (e.g. Notepad) is the active foreground window. If not, use computer_switch_window or computer_open_app to focus it first.
177
+
178
+ Tool Selection Guidelines:
179
+ - Level 1 - COMPUTER & DESKTOP APPS: When interacting with desktop GUI applications (VS Code, Notepad, File Explorer, Terminal), observing the screen, clicking desktop UI coordinates, or switching windows, use: computer_screenshot, computer_click, computer_type, computer_hotkey, computer_open_app, computer_switch_window, computer_inspect_ui.
180
+ - Level 2 - BROWSER AUTOMATION: For web browsing, scraping, filling web forms, or downloading online files, use browser tools: browser_navigate, browser_click, browser_type, browser_download, browser_content.
181
+ - Level 3 - DIRECT TOOLS: For file editing, creating, reading, searching, or shell commands in coding/repository tasks (when computer GUI is NOT required), use: write_file, edit_file, read_file, run_command, glob_search, grep_search.
182
+
183
+ Action Rules:
184
+ - Structured Tool Invocations: Return exactly one tool call per turn via native function calling or \`\`\`json { "tool": "tool_name", "args": { ... } } \`\`\`.
185
+ - Screen Observations: Request \`computer_screenshot\` when you need to inspect desktop application UI or verify visible screen state.
186
+ - Verification Mandate: Never assume an action succeeded. After creating files, verify them. After opening an app, verify its window appeared.
187
+ - Termination: Call \`task_completed\` as soon as the objective has been achieved with a full summary.
188
+ `.trim();
189
+ }
190
+ async executeGoal(userGoal) {
191
+ this.resetContext();
192
+ this.workspace.setObjective(userGoal);
193
+ const expectedApp = this.workspace.getData().environment.expectedApplication;
194
+ if (expectedApp && this.computerController.setExpectedApplication) {
195
+ this.computerController.setExpectedApplication(expectedApp);
196
+ }
197
+ const structuredPlan = this.planner.createPlan(userGoal);
198
+ this.workspace.setPlan(structuredPlan.steps);
199
+ const taskId = this.workspace.getData().taskId;
200
+ if (this.mockMode) {
201
+ for (let i = 1; i <= structuredPlan.steps.length; i++) {
202
+ this.workspace.startStep(i);
203
+ this.workspace.completeStep(i, `Completed step ${i} in mock mode`);
204
+ }
205
+ this.workspace.setStatus('completed');
206
+ return {
207
+ success: true,
208
+ summary: `Mock execution completed successfully for goal: "${userGoal}".`,
209
+ taskId,
210
+ stepsTaken: structuredPlan.steps.length,
211
+ modifiedFiles: [],
212
+ status: 'completed',
213
+ };
214
+ }
215
+ this.historyMessages.push({ role: 'system', content: this.buildSystemPrompt() });
216
+ // Memory persistence
217
+ const recentMemory = getRecentSessionsSummary(3);
218
+ if (recentMemory) {
219
+ this.historyMessages.push({ role: 'user', content: recentMemory });
220
+ }
221
+ // Add user goal
222
+ this.historyMessages.push({ role: 'user', content: userGoal });
223
+ let step = 0;
224
+ let finalSummary = '';
225
+ while (step < this.maxSteps) {
226
+ if (this.isCancelled) {
227
+ return {
228
+ success: false,
229
+ summary: `Task cancelled: ${this.cancelReason || 'User abort'}`,
230
+ taskId,
231
+ stepsTaken: step,
232
+ modifiedFiles: Array.from(this.modifiedFiles),
233
+ status: 'cancelled',
234
+ };
235
+ }
236
+ step++;
237
+ this.currentStep = step;
238
+ this.workspace.startStep(step);
239
+ // Context Compaction check: If history is long, compact middle turns using workspace state
240
+ if (this.historyMessages.length > 10) {
241
+ this.historyMessages = this.workspace.compactHistoryMessages(this.historyMessages, 6);
242
+ }
243
+ const stepSpinner = spinner();
244
+ stepSpinner.start(chalk.cyan(`Scout Operating (Step ${step}/${this.maxSteps})`));
245
+ try {
246
+ let response;
247
+ const tierTools = this.toolRegistry.getToolsForTier(this.userTier);
248
+ try {
249
+ response = await callOpenAIWithRetry(async (model) => {
250
+ return await openai.chat.completions.create({
251
+ model,
252
+ messages: sanitizeMessages(this.historyMessages),
253
+ tools: tierTools.map((t) => ({ type: 'function', function: t })),
254
+ tool_choice: 'auto',
255
+ temperature: 0.1,
256
+ });
257
+ });
258
+ }
259
+ catch (err) {
260
+ const errStr = String(err?.message || err || '').toLowerCase();
261
+ if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported')) {
262
+ const fallbackMsg = {
263
+ role: 'user',
264
+ content: `Format your tool call as a JSON code block:\n\`\`\`json\n{ "tool": "tool_name", "args": { ... } }\n\`\`\`\nAvailable tools: ${tierTools.map((t) => t.name).join(', ')}`,
265
+ };
266
+ response = await callOpenAIWithRetry(async (model) => {
267
+ return await openai.chat.completions.create({
268
+ model,
269
+ messages: sanitizeMessages([...this.historyMessages, fallbackMsg]),
270
+ temperature: 0.1,
271
+ });
272
+ });
273
+ }
274
+ else {
275
+ throw err;
276
+ }
277
+ }
278
+ const choice = response.choices[0];
279
+ if (!choice) {
280
+ stepSpinner.stop(chalk.yellow('No response from model. Retrying step...'));
281
+ continue;
282
+ }
283
+ const msg = sanitizeMessage(choice.message);
284
+ this.historyMessages.push(msg);
285
+ if (msg.content && msg.content.trim()) {
286
+ safeNote(renderMarkdown(msg.content), `Scout Step ${step}`);
287
+ }
288
+ let toolCallsToExecute = [];
289
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
290
+ for (const tc of msg.tool_calls) {
291
+ if (tc.type === 'function' && tc.function) {
292
+ try {
293
+ toolCallsToExecute.push({
294
+ name: tc.function.name,
295
+ args: JSON.parse(tc.function.arguments || '{}'),
296
+ id: tc.id,
297
+ });
298
+ }
299
+ catch {
300
+ toolCallsToExecute.push({ name: tc.function.name, args: {}, id: tc.id });
301
+ }
302
+ }
303
+ }
304
+ }
305
+ else if (msg.content) {
306
+ const parsed = this.extractJsonToolCall(msg.content);
307
+ if (parsed) {
308
+ toolCallsToExecute.push({ name: parsed.tool, args: parsed.args });
309
+ }
310
+ }
311
+ stepSpinner.stop(chalk.green(`Step ${step}: Executing ${toolCallsToExecute.length} action(s).`));
312
+ for (const tc of toolCallsToExecute) {
313
+ if (this.isCancelled)
314
+ break;
315
+ const toolResult = await this.dispatchTool(tc.name, tc.args);
316
+ // Build the tool result message with correct role
317
+ if (tc.id) {
318
+ // Native tool call: use 'tool' role with matching tool_call_id
319
+ this.historyMessages.push({
320
+ role: 'tool',
321
+ tool_call_id: tc.id,
322
+ content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
323
+ });
324
+ }
325
+ else {
326
+ // JSON-extracted tool call (no tc.id): use 'user' role
327
+ this.historyMessages.push({
328
+ role: 'user',
329
+ content: `Tool Execution Result (${tc.name}):\n${toolResult.result}`,
330
+ });
331
+ }
332
+ // If the tool returned screenshot data, append a vision message so the model can see the screen
333
+ if (toolResult.screenshotBase64) {
334
+ this.pruneOldScreenshots();
335
+ this.historyMessages.push({
336
+ role: 'user',
337
+ content: [
338
+ { type: 'text', text: `[Screenshot after ${tc.name}]: ${toolResult.screenshotSummary || 'Current screen state.'}` },
339
+ { type: 'image_url', image_url: { url: `data:image/png;base64,${toolResult.screenshotBase64}`, detail: 'low' } },
340
+ ],
341
+ });
342
+ }
343
+ if (tc.name === 'task_completed') {
344
+ finalSummary = tc.args.summary || toolResult.result;
345
+ if (this.modifiedFiles.size > 0) {
346
+ const filesList = getDirectoryFiles(this.cwd);
347
+ await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, path.basename(this.cwd), filesList, { maxRetries: 3 });
348
+ }
349
+ this.workspace.setStatus('completed');
350
+ this.workspace.completeStep(step, finalSummary);
351
+ saveAgentSession({
352
+ goal: userGoal,
353
+ summary: finalSummary,
354
+ modifiedFiles: Array.from(this.modifiedFiles),
355
+ });
356
+ return {
357
+ success: true,
358
+ summary: finalSummary,
359
+ taskId,
360
+ stepsTaken: step,
361
+ modifiedFiles: Array.from(this.modifiedFiles),
362
+ status: 'completed',
363
+ };
364
+ }
365
+ }
366
+ }
367
+ catch (err) {
368
+ stepSpinner.stop(chalk.red(`Step ${step} error: ${err?.message || String(err)}`));
369
+ this.workspace.recordError(err?.message || String(err), 'runtime', undefined, false);
370
+ this.historyMessages.push({
371
+ role: 'user',
372
+ content: `System Error encountered on step ${step}: ${err?.message || String(err)}. Diagnose and adapt action.`,
373
+ });
374
+ }
375
+ }
376
+ this.workspace.setStatus('failed');
377
+ return {
378
+ success: false,
379
+ summary: `Reached max iteration steps limit (${this.maxSteps}). Modified files: ${Array.from(this.modifiedFiles).join(', ')}`,
380
+ taskId,
381
+ stepsTaken: step,
382
+ modifiedFiles: Array.from(this.modifiedFiles),
383
+ status: 'failed',
384
+ };
385
+ }
386
+ async executeTool(name, args) {
387
+ return await this.dispatchTool(name, args);
388
+ }
389
+ async dispatchTool(name, args) {
390
+ this.totalToolCalls++;
391
+ const risk = this.approvalManager.checkAction(name, args);
392
+ if (risk.requiresApproval && !this.autoApprove) {
393
+ this.workspace.setPendingApproval({
394
+ id: `act_${Date.now()}`,
395
+ tool: name,
396
+ args,
397
+ riskLevel: risk.level,
398
+ riskReason: risk.reason,
399
+ requestedAt: new Date().toISOString(),
400
+ });
401
+ const decision = await this.approvalManager.requestApproval(this.workspace.getData().taskId, name, args, risk);
402
+ this.workspace.setPendingApproval(null);
403
+ if (!decision.approved) {
404
+ this.workspace.addObservation({
405
+ tool: name,
406
+ resultSummary: `[ACTION BLOCKED: User rejected approval: ${decision.reason || 'Not approved'}]`,
407
+ verified: false,
408
+ });
409
+ return { result: `[ACTION REJECTED BY USER]: ${decision.reason || 'User rejected this action.'}` };
410
+ }
411
+ }
412
+ // Validate against execution intent (USER INTENT > TOOL CONVENIENCE)
413
+ const validation = this.workspace.validateToolExecution(name, args);
414
+ if (!validation.allowed) {
415
+ const blockedSummary = `[EXECUTION INTENT VIOLATION BLOCKED]: ${validation.reason}`;
416
+ this.workspace.addObservation({
417
+ tool: name,
418
+ resultSummary: blockedSummary,
419
+ verified: false,
420
+ });
421
+ return {
422
+ result: JSON.stringify({
423
+ success: false,
424
+ action: name,
425
+ error: 'ExecutionIntentViolation',
426
+ message: validation.reason,
427
+ recoverable: true,
428
+ suggestedAction: 'Preserve the user\'s required execution method using computer interaction tools (computer_open_app, computer_switch_window, computer_click, computer_type, computer_hotkey). Do NOT substitute direct filesystem APIs.'
429
+ }, null, 2)
430
+ };
431
+ }
432
+ // 1. Computer Control Tools
433
+ if (name.startsWith('computer_') || name === 'computer') {
434
+ if (!this.isProOrEnterprise()) {
435
+ const upgradeMsg = `[PLAN RESTRICTION]: Autonomous Computer Control tools (${name}) are exclusively available to Pro and Enterprise plan developers. Free tier developers can perform code editing, creating files/folders, and running terminal commands. Upgrade to Pro at https://tryscout.web.app to unlock autonomous computer & app control.`;
436
+ this.workspace.addObservation({
437
+ tool: name,
438
+ resultSummary: upgradeMsg,
439
+ verified: false,
440
+ });
441
+ return { result: upgradeMsg };
442
+ }
443
+ return await this.dispatchComputerTool(name, args);
444
+ }
445
+ // 2. Browser Automation Tools
446
+ if (name.startsWith('browser_')) {
447
+ if (!this.isProOrEnterprise()) {
448
+ const upgradeMsg = `[PLAN RESTRICTION]: Autonomous Browser Control tools (${name}) are exclusively available to Pro and Enterprise plan developers. Free tier developers can perform code editing, creating files/folders, and running terminal commands. Upgrade to Pro at https://tryscout.web.app to unlock browser automation.`;
449
+ this.workspace.addObservation({
450
+ tool: name,
451
+ resultSummary: upgradeMsg,
452
+ verified: false,
453
+ });
454
+ return { result: upgradeMsg };
455
+ }
456
+ return await this.dispatchBrowserTool(name, args);
457
+ }
458
+ // 3. Core Direct Workspace Tools
459
+ return await this.dispatchCoreTool(name, args);
460
+ }
461
+ async dispatchComputerTool(name, args) {
462
+ if (name === 'computer') {
463
+ const action = String(args.action || 'screenshot').toLowerCase().trim();
464
+ if (action === 'click' || action === 'left_click') {
465
+ name = 'computer_click';
466
+ }
467
+ else if (action === 'right_click') {
468
+ name = 'computer_right_click';
469
+ }
470
+ else if (action === 'double_click') {
471
+ name = 'computer_double_click';
472
+ }
473
+ else if (action === 'move' || action === 'mouse_move') {
474
+ name = 'computer_mouse_move';
475
+ }
476
+ else if (action === 'mouse_down') {
477
+ name = 'computer_mouse_down';
478
+ }
479
+ else if (action === 'mouse_up') {
480
+ name = 'computer_mouse_up';
481
+ }
482
+ else if (action === 'drag') {
483
+ name = 'computer_drag';
484
+ args.fromX = args.startX ?? args.fromX;
485
+ args.fromY = args.startY ?? args.fromY;
486
+ args.toX = args.endX ?? args.toX;
487
+ args.toY = args.endY ?? args.toY;
488
+ }
489
+ else if (action === 'scroll') {
490
+ name = 'computer_scroll';
491
+ }
492
+ else if (action === 'screenshot') {
493
+ name = 'computer_screenshot';
494
+ }
495
+ else if (action === 'type') {
496
+ name = 'computer_type';
497
+ }
498
+ else if (action === 'key_press' || action === 'key') {
499
+ name = 'computer_key_press';
500
+ }
501
+ else if (action === 'hotkey') {
502
+ name = 'computer_hotkey';
503
+ }
504
+ else if (action === 'open_app') {
505
+ name = 'computer_open_app';
506
+ }
507
+ else if (action === 'close_app') {
508
+ name = 'computer_close_app';
509
+ }
510
+ else if (action === 'switch_window') {
511
+ name = 'computer_switch_window';
512
+ }
513
+ else if (action === 'inspect_ui') {
514
+ name = 'computer_inspect_ui';
515
+ }
516
+ else if (action === 'diagnostic') {
517
+ name = 'computer_diagnostic';
518
+ }
519
+ }
520
+ switch (name) {
521
+ case 'computer_screenshot': {
522
+ const res = await this.computerController.screenshot();
523
+ if (res.path) {
524
+ this.workspace.recordArtifact({
525
+ filename: path.basename(res.path),
526
+ path: res.path,
527
+ type: 'image',
528
+ description: `Screen observation (${res.width}x${res.height})`,
529
+ application: res.activeWindow || 'Desktop',
530
+ });
531
+ }
532
+ if (res.activeWindow) {
533
+ this.workspace.setActiveWindow(res.activeWindow);
534
+ }
535
+ const summary = `Screen captured: ${res.width}x${res.height}, Active window: "${res.activeWindow || 'Desktop'}"`;
536
+ this.workspace.addObservation({ tool: name, resultSummary: summary, verified: true });
537
+ // Encode the screenshot as base64 for model vision
538
+ const base64Data = res.path ? this.readScreenshotAsBase64(res.path) : undefined;
539
+ this.screenshotCount++;
540
+ return {
541
+ result: summary,
542
+ screenshotBase64: base64Data,
543
+ screenshotSummary: summary,
544
+ };
545
+ }
546
+ case 'computer_click': {
547
+ const x = Number(args.x);
548
+ const y = Number(args.y);
549
+ const btn = args.button || 'left';
550
+ const res = await this.computerController.click(x, y, { button: btn, clicks: 1 });
551
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
552
+ const structuredRes = {
553
+ success: res.success,
554
+ action: 'click',
555
+ coordinates: res.coordinates || { x, y },
556
+ button: btn,
557
+ output: res.output,
558
+ ...(res.error ? { error: res.error } : {}),
559
+ recoverable: res.recoverable ?? true,
560
+ ...(res.activeWindow ? { activeWindow: res.activeWindow } : {}),
561
+ };
562
+ // Auto-capture screenshot after click so model sees the result
563
+ const postSnap = await this.capturePostActionScreenshot('click');
564
+ return { result: JSON.stringify(structuredRes, null, 2), ...postSnap };
565
+ }
566
+ case 'computer_right_click': {
567
+ const x = Number(args.x);
568
+ const y = Number(args.y);
569
+ const res = await this.computerController.rightClick(x, y);
570
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
571
+ const structuredRes = {
572
+ success: res.success,
573
+ action: 'right_click',
574
+ coordinates: res.coordinates || { x, y },
575
+ button: 'right',
576
+ output: res.output,
577
+ ...(res.error ? { error: res.error } : {}),
578
+ recoverable: res.recoverable ?? true,
579
+ ...(res.activeWindow ? { activeWindow: res.activeWindow } : {}),
580
+ };
581
+ return { result: JSON.stringify(structuredRes, null, 2) };
582
+ }
583
+ case 'computer_double_click': {
584
+ const x = Number(args.x);
585
+ const y = Number(args.y);
586
+ const res = await this.computerController.doubleClick(x, y);
587
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
588
+ const structuredRes = {
589
+ success: res.success,
590
+ action: 'double_click',
591
+ coordinates: res.coordinates || { x, y },
592
+ button: 'left',
593
+ output: res.output,
594
+ ...(res.error ? { error: res.error } : {}),
595
+ recoverable: res.recoverable ?? true,
596
+ };
597
+ return { result: JSON.stringify(structuredRes, null, 2) };
598
+ }
599
+ case 'computer_mouse_down': {
600
+ const x = args.x !== undefined ? Number(args.x) : undefined;
601
+ const y = args.y !== undefined ? Number(args.y) : undefined;
602
+ const btn = args.button || 'left';
603
+ const res = await this.computerController.mouseDown(x, y, btn);
604
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
605
+ const structuredRes = {
606
+ success: res.success,
607
+ action: 'mouse_down',
608
+ coordinates: res.coordinates || (x !== undefined && y !== undefined ? { x, y } : undefined),
609
+ button: btn,
610
+ output: res.output,
611
+ ...(res.error ? { error: res.error } : {}),
612
+ recoverable: res.recoverable ?? true,
613
+ };
614
+ return { result: JSON.stringify(structuredRes, null, 2) };
615
+ }
616
+ case 'computer_mouse_up': {
617
+ const x = args.x !== undefined ? Number(args.x) : undefined;
618
+ const y = args.y !== undefined ? Number(args.y) : undefined;
619
+ const btn = args.button || 'left';
620
+ const res = await this.computerController.mouseUp(x, y, btn);
621
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
622
+ const structuredRes = {
623
+ success: res.success,
624
+ action: 'mouse_up',
625
+ coordinates: res.coordinates || (x !== undefined && y !== undefined ? { x, y } : undefined),
626
+ button: btn,
627
+ output: res.output,
628
+ ...(res.error ? { error: res.error } : {}),
629
+ recoverable: res.recoverable ?? true,
630
+ };
631
+ return { result: JSON.stringify(structuredRes, null, 2) };
632
+ }
633
+ case 'computer_type': {
634
+ const textVal = String(args.text || '');
635
+ const res = await this.computerController.type(textVal);
636
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
637
+ const structuredRes = {
638
+ success: res.success,
639
+ action: 'type',
640
+ output: res.output,
641
+ ...(res.error ? { error: res.error } : {}),
642
+ recoverable: res.recoverable ?? true,
643
+ ...(res.activeWindow ? { activeWindow: res.activeWindow } : {}),
644
+ ...(res.expectedWindow ? { expectedWindow: res.expectedWindow } : {}),
645
+ };
646
+ // Auto-capture screenshot after typing so model sees the result
647
+ const postSnap = await this.capturePostActionScreenshot('type');
648
+ return { result: JSON.stringify(structuredRes, null, 2), ...postSnap };
649
+ }
650
+ case 'computer_key_press': {
651
+ const key = String(args.key || '');
652
+ const res = await this.computerController.keyPress(key);
653
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
654
+ const structuredRes = {
655
+ success: res.success,
656
+ action: 'key_press',
657
+ output: res.output,
658
+ ...(res.error ? { error: res.error } : {}),
659
+ recoverable: res.recoverable ?? true,
660
+ };
661
+ return { result: JSON.stringify(structuredRes, null, 2) };
662
+ }
663
+ case 'computer_hotkey': {
664
+ const keys = Array.isArray(args.keys) ? args.keys.map(String) : [String(args.keys)];
665
+ const res = await this.computerController.hotkey(keys);
666
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
667
+ const structuredRes = {
668
+ success: res.success,
669
+ action: 'hotkey',
670
+ output: res.output,
671
+ ...(res.error ? { error: res.error } : {}),
672
+ recoverable: res.recoverable ?? true,
673
+ ...(res.activeWindow ? { activeWindow: res.activeWindow } : {}),
674
+ };
675
+ // Auto-capture screenshot after hotkey so model sees the result
676
+ const postSnap = await this.capturePostActionScreenshot('hotkey');
677
+ return { result: JSON.stringify(structuredRes, null, 2), ...postSnap };
678
+ }
679
+ case 'computer_scroll': {
680
+ const deltaY = Number(args.deltaY || 0);
681
+ const deltaX = Number(args.deltaX || 0);
682
+ const res = await this.computerController.scroll(deltaX, deltaY);
683
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
684
+ return { result: res.output };
685
+ }
686
+ case 'computer_mouse_move': {
687
+ const x = Number(args.x);
688
+ const y = Number(args.y);
689
+ const res = await this.computerController.move(x, y);
690
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
691
+ const structuredRes = {
692
+ success: res.success,
693
+ action: 'move',
694
+ coordinates: res.coordinates || { x, y },
695
+ output: res.output,
696
+ ...(res.error ? { error: res.error } : {}),
697
+ recoverable: res.recoverable ?? true,
698
+ ...(res.activeWindow ? { activeWindow: res.activeWindow } : {}),
699
+ };
700
+ return { result: JSON.stringify(structuredRes, null, 2) };
701
+ }
702
+ case 'computer_drag': {
703
+ const fromX = Number(args.fromX ?? args.startX ?? 0);
704
+ const fromY = Number(args.fromY ?? args.startY ?? 0);
705
+ const toX = Number(args.toX ?? args.endX ?? 0);
706
+ const toY = Number(args.toY ?? args.endY ?? 0);
707
+ const res = await this.computerController.drag(fromX, fromY, toX, toY);
708
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
709
+ const structuredRes = {
710
+ success: res.success,
711
+ action: 'drag',
712
+ from: { x: fromX, y: fromY },
713
+ to: { x: toX, y: toY },
714
+ coordinates: res.coordinates || { x: toX, y: toY },
715
+ output: res.output,
716
+ ...(res.error ? { error: res.error } : {}),
717
+ recoverable: res.recoverable ?? true,
718
+ };
719
+ return { result: JSON.stringify(structuredRes, null, 2) };
720
+ }
721
+ case 'computer_wait': {
722
+ const ms = Math.min(10000, Math.max(100, Number(args.ms) || 1000));
723
+ await this.computerController.wait(ms);
724
+ return { result: `Waited for ${ms}ms` };
725
+ }
726
+ case 'computer_open_app': {
727
+ const appName = String(args.appName || args.app || '');
728
+ const res = await this.computerController.openApplication(appName, args.args);
729
+ this.workspace.setActiveApplication(appName);
730
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
731
+ const structuredRes = {
732
+ success: res.success,
733
+ action: 'open_app',
734
+ output: res.output,
735
+ ...(res.error ? { error: res.error } : {}),
736
+ recoverable: res.recoverable ?? true,
737
+ activeWindow: res.activeWindow || appName,
738
+ expectedWindow: res.expectedWindow || appName,
739
+ };
740
+ // Auto-capture screenshot after opening app so model sees the window
741
+ const postSnap = await this.capturePostActionScreenshot('open_app');
742
+ return { result: JSON.stringify(structuredRes, null, 2), ...postSnap };
743
+ }
744
+ case 'computer_close_app': {
745
+ const appName = String(args.appName || args.app || '');
746
+ const res = await this.computerController.closeApplication(appName);
747
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
748
+ return { result: res.output };
749
+ }
750
+ case 'computer_switch_window': {
751
+ const targetWin = String(args.windowTitleOrApp || args.target || '');
752
+ const res = await this.computerController.switchWindow(targetWin);
753
+ if (res.success) {
754
+ this.workspace.setActiveWindow(targetWin);
755
+ }
756
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success });
757
+ const structuredRes = {
758
+ success: res.success,
759
+ action: 'switch_window',
760
+ output: res.output,
761
+ ...(res.error ? { error: res.error } : {}),
762
+ recoverable: res.recoverable ?? true,
763
+ activeWindow: res.activeWindow || targetWin,
764
+ expectedWindow: res.expectedWindow,
765
+ };
766
+ // Auto-capture screenshot after switching so model sees the new window
767
+ const postSnap = await this.capturePostActionScreenshot('switch_window');
768
+ return { result: JSON.stringify(structuredRes, null, 2), ...postSnap };
769
+ }
770
+ case 'computer_inspect_ui': {
771
+ const ui = await this.computerController.inspectUI();
772
+ this.workspace.setActiveWindow(ui.activeWindow);
773
+ const outStr = `Active Window: "${ui.activeWindow}"\nVisible Windows: ${ui.windows.map((w) => `"${w}"`).join(', ')}`;
774
+ this.workspace.addObservation({ tool: name, resultSummary: outStr, verified: true });
775
+ const structuredRes = {
776
+ success: true,
777
+ action: 'inspect_ui',
778
+ activeWindow: ui.activeWindow,
779
+ windows: ui.windows,
780
+ output: outStr,
781
+ };
782
+ return { result: JSON.stringify(structuredRes, null, 2) };
783
+ }
784
+ case 'computer_diagnostic': {
785
+ const diag = await this.computerController.runDiagnostic();
786
+ return { result: JSON.stringify(diag, null, 2) };
787
+ }
788
+ default:
789
+ return { result: `Unknown computer tool: "${name}"` };
790
+ }
791
+ }
792
+ /**
793
+ * Read a screenshot file from disk, resize it to save tokens, and return as base64.
794
+ * Returns undefined if the file cannot be read.
795
+ */
796
+ readScreenshotAsBase64(screenshotPath) {
797
+ try {
798
+ if (!fs.existsSync(screenshotPath))
799
+ return undefined;
800
+ const raw = fs.readFileSync(screenshotPath);
801
+ return raw.toString('base64');
802
+ }
803
+ catch {
804
+ return undefined;
805
+ }
806
+ }
807
+ /**
808
+ * Capture a post-action screenshot automatically.
809
+ * This gives the model vision of what happened after a screen-modifying action.
810
+ * Includes a brief delay to let the UI settle.
811
+ */
812
+ async capturePostActionScreenshot(actionName) {
813
+ try {
814
+ // Brief delay to let the UI settle after the action
815
+ await this.computerController.wait(350);
816
+ const snap = await this.computerController.screenshot();
817
+ if (!snap.path)
818
+ return {};
819
+ const base64Data = this.readScreenshotAsBase64(snap.path);
820
+ if (!base64Data)
821
+ return {};
822
+ this.screenshotCount++;
823
+ const summary = `Post-action screenshot (after ${actionName}): ${snap.width}x${snap.height}, Active window: "${snap.activeWindow || 'Desktop'}"`;
824
+ return { screenshotBase64: base64Data, screenshotSummary: summary };
825
+ }
826
+ catch {
827
+ return {};
828
+ }
829
+ }
830
+ /**
831
+ * Remove old screenshot vision messages from history to prevent context overflow.
832
+ * Keeps only the most recent MAX_SCREENSHOTS_IN_HISTORY screenshots.
833
+ */
834
+ pruneOldScreenshots() {
835
+ const maxKeep = AgentRuntime.MAX_SCREENSHOTS_IN_HISTORY;
836
+ // Find all vision messages (those with image_url content)
837
+ const visionIndices = [];
838
+ for (let i = 0; i < this.historyMessages.length; i++) {
839
+ const msg = this.historyMessages[i];
840
+ if (Array.isArray(msg.content)) {
841
+ const hasImage = msg.content.some((part) => part.type === 'image_url');
842
+ if (hasImage)
843
+ visionIndices.push(i);
844
+ }
845
+ }
846
+ // If we have more than maxKeep, remove the oldest ones
847
+ if (visionIndices.length >= maxKeep) {
848
+ const toRemove = visionIndices.slice(0, visionIndices.length - maxKeep + 1);
849
+ // Remove in reverse order to preserve indices
850
+ for (let i = toRemove.length - 1; i >= 0; i--) {
851
+ const idx = toRemove[i];
852
+ if (idx !== undefined) {
853
+ this.historyMessages.splice(idx, 1);
854
+ }
855
+ }
856
+ }
857
+ }
858
+ async dispatchBrowserTool(name, args) {
859
+ switch (name) {
860
+ case 'browser_navigate': {
861
+ const url = String(args.url || '');
862
+ this.workspace.setActiveUrl(url);
863
+ this.workspace.setActiveApplication('Browser');
864
+ const res = await this.browserController.navigate(url);
865
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success, target: url });
866
+ return { result: res.output };
867
+ }
868
+ case 'browser_inspect': {
869
+ const res = await this.browserController.inspect_page();
870
+ this.workspace.addObservation({ tool: name, resultSummary: res.summary, verified: res.success, target: res.url });
871
+ return { result: res.summary };
872
+ }
873
+ case 'browser_click': {
874
+ const target = String(args.selectorOrText || args.target || '');
875
+ const res = await this.browserController.click(target);
876
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success, target });
877
+ return { result: res.output };
878
+ }
879
+ case 'browser_type': {
880
+ const target = String(args.selectorOrText || args.target || '');
881
+ const textVal = String(args.text || '');
882
+ const res = await this.browserController.type(target, textVal);
883
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: res.success, target });
884
+ return { result: res.output };
885
+ }
886
+ case 'browser_download': {
887
+ const target = String(args.urlOrSelector || args.url || '');
888
+ const res = await this.browserController.download_file(target, args.destPath);
889
+ if (res.success && res.details?.path) {
890
+ const vRes = await this.actionVerifier.verifyFileCreated(res.details.path);
891
+ const artifact = this.workspace.recordArtifact({
892
+ filename: res.details.filename || path.basename(res.details.path),
893
+ path: res.details.path,
894
+ application: 'Browser',
895
+ verified: vRes.verified,
896
+ sizeBytes: res.details.sizeBytes,
897
+ });
898
+ this.workspace.addObservation({ tool: name, resultSummary: `${res.output} (Verified on disk: ${vRes.verified})`, verified: vRes.verified });
899
+ return { result: `${res.output}\nTracked Artifact: ${artifact.filename} (${artifact.path})` };
900
+ }
901
+ this.workspace.addObservation({ tool: name, resultSummary: res.output, verified: false });
902
+ return { result: res.output };
903
+ }
904
+ case 'browser_content': {
905
+ const res = await this.browserController.extract_content(args.selector);
906
+ this.workspace.addObservation({ tool: name, resultSummary: `Extracted content (${res.output.length} chars)`, verified: res.success });
907
+ return { result: res.output };
908
+ }
909
+ default:
910
+ return { result: `Unknown browser tool: "${name}"` };
911
+ }
912
+ }
913
+ async dispatchCoreTool(name, args) {
914
+ switch (name) {
915
+ case 'scratchpad': {
916
+ if (!this.isProOrEnterprise()) {
917
+ const upgradeMsg = `[PLAN RESTRICTION]: AI Scratchpad & Autonomous reasoning is an exclusive feature for Pro and Enterprise plan subscribers. Free tier developers can perform code editing, creating files/folders, and running commands. Upgrade to Pro at https://tryscout.web.app to unlock the AI Scratchpad.`;
918
+ this.workspace.addObservation({
919
+ tool: name,
920
+ resultSummary: upgradeMsg,
921
+ verified: false,
922
+ });
923
+ return { result: upgradeMsg };
924
+ }
925
+ const action = String(args.action || 'read').toLowerCase();
926
+ const scratchpadPath = path.resolve(this.cwd, '.ft', 'scratchpad.md');
927
+ fs.mkdirSync(path.dirname(scratchpadPath), { recursive: true });
928
+ if (action === 'write') {
929
+ const content = String(args.content || '');
930
+ fs.writeFileSync(scratchpadPath, content, 'utf-8');
931
+ this.workspace.addObservation({ tool: name, resultSummary: `Scratchpad updated (${content.length} chars)`, verified: true });
932
+ return { result: `Scratchpad updated successfully.` };
933
+ }
934
+ else if (action === 'append') {
935
+ const content = String(args.content || '');
936
+ fs.appendFileSync(scratchpadPath, `\n${content}`, 'utf-8');
937
+ this.workspace.addObservation({ tool: name, resultSummary: `Appended to scratchpad`, verified: true });
938
+ return { result: `Appended notes to scratchpad.` };
939
+ }
940
+ else if (action === 'clear') {
941
+ fs.writeFileSync(scratchpadPath, '', 'utf-8');
942
+ this.workspace.addObservation({ tool: name, resultSummary: `Cleared scratchpad`, verified: true });
943
+ return { result: `Scratchpad cleared.` };
944
+ }
945
+ else {
946
+ // read
947
+ if (!fs.existsSync(scratchpadPath)) {
948
+ return { result: `Scratchpad is currently empty.` };
949
+ }
950
+ const content = fs.readFileSync(scratchpadPath, 'utf-8');
951
+ return { result: `Scratchpad content:\n${content || '(empty)'}` };
952
+ }
953
+ }
954
+ case 'read_file': {
955
+ const targetPath = String(args.path || '').trim();
956
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
957
+ if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
958
+ return { result: `Error: File not found at path "${targetPath}"` };
959
+ }
960
+ const content = fs.readFileSync(fullPath, 'utf-8');
961
+ const lines = content.split('\n');
962
+ const start = Math.max(1, Number(args.startLine) || 1);
963
+ const end = Math.min(lines.length, Number(args.endLine) || lines.length);
964
+ const sliced = lines.slice(start - 1, end).map((l, i) => `${start + i} | ${l}`).join('\n');
965
+ this.workspace.addObservation({ tool: name, resultSummary: `Read ${targetPath} (${lines.length} lines)`, verified: true, target: targetPath });
966
+ return { result: `File: ${targetPath} (Lines ${start}-${end}/${lines.length}):\n${sliced}` };
967
+ }
968
+ case 'write_file': {
969
+ let targetPath = String(args.path || '').trim();
970
+ const intent = this.workspace.getExecutionIntent();
971
+ if (intent.targetDestination === 'Desktop' || targetPath.toLowerCase().includes('desktop')) {
972
+ const fileName = path.basename(targetPath);
973
+ targetPath = resolveDesktopPath(fileName);
974
+ }
975
+ const content = String(args.content || '');
976
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
977
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
978
+ fs.writeFileSync(fullPath, content, 'utf-8');
979
+ this.modifiedFiles.add(targetPath);
980
+ this.workspace.recordArtifact({
981
+ filename: path.basename(targetPath),
982
+ path: targetPath,
983
+ type: 'file',
984
+ verified: true,
985
+ sizeBytes: Buffer.byteLength(content, 'utf-8'),
986
+ });
987
+ this.workspace.addObservation({ tool: name, resultSummary: `Wrote file ${targetPath} (${content.length} chars)`, verified: true, target: targetPath });
988
+ return { result: `Successfully wrote file "${targetPath}".` };
989
+ }
990
+ case 'edit_file': {
991
+ const targetPath = String(args.path || '').trim();
992
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
993
+ if (!fs.existsSync(fullPath))
994
+ return { result: `Error: File "${targetPath}" does not exist.` };
995
+ const currentContent = fs.readFileSync(fullPath, 'utf-8');
996
+ const replaceRes = robustSnippetReplace(currentContent, args.target, args.replacement);
997
+ if (!replaceRes.success) {
998
+ return { result: `Error: ${replaceRes.error}` };
999
+ }
1000
+ fs.writeFileSync(fullPath, replaceRes.updatedContent, 'utf-8');
1001
+ this.modifiedFiles.add(targetPath);
1002
+ this.workspace.addObservation({ tool: name, resultSummary: `Edited file ${targetPath}`, verified: true, target: targetPath });
1003
+ return { result: `Successfully updated "${targetPath}".` };
1004
+ }
1005
+ case 'run_command': {
1006
+ const cmd = String(args.command || args.cmd || '').trim();
1007
+ try {
1008
+ const { execSync } = await import('child_process');
1009
+ const out = execSync(cmd, { cwd: this.cwd, encoding: 'utf-8', maxBuffer: 1024 * 1024 * 5 });
1010
+ const preview = out.slice(0, 5000);
1011
+ this.workspace.addObservation({ tool: name, resultSummary: `Ran command "${cmd}"`, verified: true, target: cmd });
1012
+ return { result: `Command Output:\n${preview}` };
1013
+ }
1014
+ catch (err) {
1015
+ const errOutput = (err?.stdout || '') + '\n' + (err?.stderr || err?.message || String(err));
1016
+ this.workspace.addObservation({ tool: name, resultSummary: `Command "${cmd}" failed`, verified: false, target: cmd });
1017
+ return { result: `Command Failed:\n${errOutput.slice(0, 5000)}` };
1018
+ }
1019
+ }
1020
+ case 'list_dir': {
1021
+ const relPath = String(args.path || '.').trim();
1022
+ const fullPath = path.isAbsolute(relPath) ? relPath : path.resolve(this.cwd, relPath);
1023
+ if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
1024
+ return { result: `Error: Directory not found "${relPath}"` };
1025
+ }
1026
+ const entries = fs.readdirSync(fullPath);
1027
+ return { result: `Directory contents of "${relPath}":\n${entries.slice(0, 100).join('\n')}` };
1028
+ }
1029
+ case 'grep_search': {
1030
+ const query = String(args.query || '').trim();
1031
+ const files = getDirectoryFiles(this.cwd);
1032
+ const matches = [];
1033
+ for (const f of files.slice(0, 200)) {
1034
+ try {
1035
+ const c = fs.readFileSync(path.resolve(this.cwd, f), 'utf-8');
1036
+ if (c.includes(query))
1037
+ matches.push(f);
1038
+ }
1039
+ catch { }
1040
+ }
1041
+ return { result: `Files containing "${query}" (${matches.length}):\n${matches.slice(0, 30).join('\n')}` };
1042
+ }
1043
+ case 'glob_search': {
1044
+ const pattern = String(args.pattern || '').trim().toLowerCase();
1045
+ const files = getDirectoryFiles(this.cwd);
1046
+ const matched = files.filter((f) => f.toLowerCase().includes(pattern.replace(/\*/g, '')));
1047
+ return { result: `Matching files (${matched.length}):\n${matched.slice(0, 30).join('\n')}` };
1048
+ }
1049
+ case 'tree_view': {
1050
+ const maxDepth = Number(args.maxDepth) || 3;
1051
+ const tree = buildDirectoryTree(this.cwd, '', 0, maxDepth);
1052
+ return { result: `Workspace Tree:\n${tree}` };
1053
+ }
1054
+ case 'file_info': {
1055
+ const targetPath = String(args.path || '').trim();
1056
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
1057
+ if (!fs.existsSync(fullPath))
1058
+ return { result: `Error: File not found "${targetPath}"` };
1059
+ const st = fs.statSync(fullPath);
1060
+ return {
1061
+ result: `File Info for "${targetPath}": Size: ${st.size} bytes, Modified: ${st.mtime.toISOString()}, IsDirectory: ${st.isDirectory()}`
1062
+ };
1063
+ }
1064
+ case 'multi_edit_file': {
1065
+ const targetPath = String(args.path || '').trim();
1066
+ const edits = Array.isArray(args.edits) ? args.edits : [];
1067
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
1068
+ if (!fs.existsSync(fullPath))
1069
+ return { result: `Error: File not found "${targetPath}"` };
1070
+ let content = fs.readFileSync(fullPath, 'utf-8');
1071
+ let count = 0;
1072
+ for (const ed of edits) {
1073
+ const res = robustSnippetReplace(content, ed.target, ed.replacement);
1074
+ if (res.success) {
1075
+ content = res.updatedContent;
1076
+ count++;
1077
+ }
1078
+ }
1079
+ fs.writeFileSync(fullPath, content, 'utf-8');
1080
+ this.modifiedFiles.add(targetPath);
1081
+ return { result: `Applied ${count}/${edits.length} edits to "${targetPath}"` };
1082
+ }
1083
+ case 'fetch_url': {
1084
+ const url = String(args.url || '').trim();
1085
+ const resp = await fetch(url);
1086
+ const textOut = await resp.text();
1087
+ return { result: `Fetched ${url} (${textOut.length} chars):\n${textOut.slice(0, 4000)}` };
1088
+ }
1089
+ case 'git_diff': {
1090
+ const { execSync } = await import('child_process');
1091
+ try {
1092
+ const diff = execSync('git diff', { cwd: this.cwd, encoding: 'utf-8' });
1093
+ return { result: diff ? `Git diff:\n${diff.slice(0, 5000)}` : 'No git changes' };
1094
+ }
1095
+ catch {
1096
+ return { result: 'git diff not available' };
1097
+ }
1098
+ }
1099
+ case 'ask_user': {
1100
+ const q = String(args.question || 'Input required:');
1101
+ safeNote(q, 'User Input Required');
1102
+ const ans = await text({ message: chalk.cyan(q) });
1103
+ const ansStr = typeof ans === 'symbol' ? 'cancelled' : String(ans);
1104
+ return { result: `User answered: ${ansStr}` };
1105
+ }
1106
+ case 'speak_text': {
1107
+ const txt = String(args.text || '');
1108
+ speakText(txt, { async: true });
1109
+ return { result: `Spoke: "${txt}"` };
1110
+ }
1111
+ case 'task_completed': {
1112
+ const summary = String(args.summary || 'Completed');
1113
+ const intent = this.workspace.getExecutionIntent();
1114
+ if (intent.targetDestination === 'Desktop' && intent.targetArtifactName) {
1115
+ const checkRes = await this.actionVerifier.verifyFileCreated(intent.targetArtifactName, 1, 'Desktop');
1116
+ if (!checkRes.verified) {
1117
+ return {
1118
+ result: JSON.stringify({
1119
+ success: false,
1120
+ error: 'VerificationFailed',
1121
+ message: `Cannot conclude task as completed: ${checkRes.message}`,
1122
+ recoverable: true,
1123
+ suggestedAction: 'The file was not found on the Desktop (or was placed in a substitute path). Save the file to the user\'s Desktop via the application interface before completing.'
1124
+ }, null, 2)
1125
+ };
1126
+ }
1127
+ }
1128
+ speakText(summary, { async: true });
1129
+ return { result: summary };
1130
+ }
1131
+ default:
1132
+ return { result: `Unknown tool call: "${name}"` };
1133
+ }
1134
+ }
1135
+ extractJsonToolCall(content) {
1136
+ if (!content)
1137
+ return null;
1138
+ const funcMatch = content.match(/<function=([a-zA-Z0-9_]+)>([\s\S]*?)<\/function>/i);
1139
+ if (funcMatch && funcMatch[1] && funcMatch[2]) {
1140
+ try {
1141
+ return { tool: funcMatch[1].trim(), args: JSON.parse(funcMatch[2].trim()) };
1142
+ }
1143
+ catch {
1144
+ return { tool: funcMatch[1].trim(), args: { raw: funcMatch[2].trim() } };
1145
+ }
1146
+ }
1147
+ const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) || content.match(/\{[\s\S]*"tool"[\s\S]*\}/);
1148
+ if (jsonMatch) {
1149
+ try {
1150
+ const obj = JSON.parse(jsonMatch[1] || jsonMatch[0]);
1151
+ if (obj && typeof obj.tool === 'string') {
1152
+ return { tool: obj.tool, args: obj.args || obj.parameters || {} };
1153
+ }
1154
+ }
1155
+ catch { }
1156
+ }
1157
+ return null;
1158
+ }
1159
+ }
1160
+ //# sourceMappingURL=agentRuntime.js.map