ft-scout 6.0.4 → 6.0.8

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 (91) hide show
  1. package/.firebase/hosting.d2Vi.cache +2 -2
  2. package/README.md +68 -44
  3. package/bin/ft.js +1 -1
  4. package/bin/src/commands/agent.d.ts.map +1 -1
  5. package/bin/src/commands/agent.js +264 -92
  6. package/bin/src/commands/agent.js.map +1 -1
  7. package/bin/src/commands/audit.js +1 -1
  8. package/bin/src/commands/audit.js.map +1 -1
  9. package/bin/src/commands/authCommand.js +4 -4
  10. package/bin/src/commands/authCommand.js.map +1 -1
  11. package/bin/src/commands/brief.js +4 -4
  12. package/bin/src/commands/brief.js.map +1 -1
  13. package/bin/src/commands/configCommand.d.ts.map +1 -1
  14. package/bin/src/commands/configCommand.js +7 -7
  15. package/bin/src/commands/configCommand.js.map +1 -1
  16. package/bin/src/commands/dashboard.js +4 -4
  17. package/bin/src/commands/dashboard.js.map +1 -1
  18. package/bin/src/commands/history.js +5 -5
  19. package/bin/src/commands/history.js.map +1 -1
  20. package/bin/src/commands/init.js +26 -26
  21. package/bin/src/commands/init.js.map +1 -1
  22. package/bin/src/commands/login.d.ts.map +1 -1
  23. package/bin/src/commands/login.js +21 -21
  24. package/bin/src/commands/login.js.map +1 -1
  25. package/bin/src/commands/logout.js +1 -1
  26. package/bin/src/commands/logout.js.map +1 -1
  27. package/bin/src/commands/owners.js +5 -5
  28. package/bin/src/commands/owners.js.map +1 -1
  29. package/bin/src/commands/quota.d.ts.map +1 -0
  30. package/bin/src/commands/quota.js +48 -0
  31. package/bin/src/commands/quota.js.map +1 -0
  32. package/bin/src/commands/recommend.js +1 -1
  33. package/bin/src/commands/recommend.js.map +1 -1
  34. package/bin/src/commands/risky.d.ts.map +1 -1
  35. package/bin/src/commands/risky.js +9 -9
  36. package/bin/src/commands/risky.js.map +1 -1
  37. package/bin/src/commands/setup.js +24 -24
  38. package/bin/src/commands/setup.js.map +1 -1
  39. package/bin/src/commands/signup.d.ts.map +1 -1
  40. package/bin/src/commands/signup.js +7 -7
  41. package/bin/src/commands/signup.js.map +1 -1
  42. package/bin/src/commands/undo.js +8 -8
  43. package/bin/src/commands/undo.js.map +1 -1
  44. package/bin/src/commands/web.js +8 -8
  45. package/bin/src/commands/web.js.map +1 -1
  46. package/bin/src/discovery/config.d.ts.map +1 -1
  47. package/bin/src/discovery/config.js.map +1 -1
  48. package/bin/src/discovery/display.js +1 -1
  49. package/bin/src/discovery/display.js.map +1 -1
  50. package/bin/src/discovery/messages.js.map +1 -1
  51. package/bin/src/discovery/signals.d.ts.map +1 -1
  52. package/bin/src/discovery/signals.js.map +1 -1
  53. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  54. package/bin/src/engine/agentEngine.js +414 -77
  55. package/bin/src/engine/agentEngine.js.map +1 -1
  56. package/bin/src/engine/git.d.ts.map +1 -1
  57. package/bin/src/engine/git.js.map +1 -1
  58. package/bin/src/engine/llm.d.ts.map +1 -1
  59. package/bin/src/engine/llm.js +174 -78
  60. package/bin/src/engine/llm.js.map +1 -1
  61. package/bin/src/engine/verifier.d.ts.map +1 -1
  62. package/bin/src/engine/verifier.js +6 -6
  63. package/bin/src/engine/verifier.js.map +1 -1
  64. package/bin/src/engine/web.d.ts.map +1 -1
  65. package/bin/src/engine/web.js +1 -1
  66. package/bin/src/engine/web.js.map +1 -1
  67. package/bin/src/index.js +78 -68
  68. package/bin/src/index.js.map +1 -1
  69. package/bin/src/server/server.js.map +1 -1
  70. package/bin/src/utils/auth.d.ts.map +1 -1
  71. package/bin/src/utils/auth.js.map +1 -1
  72. package/bin/src/utils/branding.js +7 -7
  73. package/bin/src/utils/branding.js.map +1 -1
  74. package/bin/src/utils/config.d.ts.map +1 -1
  75. package/bin/src/utils/config.js.map +1 -1
  76. package/bin/src/utils/email.d.ts.map +1 -1
  77. package/bin/src/utils/email.js +42 -42
  78. package/bin/src/utils/email.js.map +1 -1
  79. package/bin/src/utils/firebaseAuth.d.ts.map +1 -1
  80. package/bin/src/utils/firebaseAuth.js.map +1 -1
  81. package/bin/src/utils/holoseneAuth.d.ts.map +1 -1
  82. package/bin/src/utils/holoseneAuth.js +22 -22
  83. package/bin/src/utils/holoseneAuth.js.map +1 -1
  84. package/bin/src/utils/language.d.ts.map +1 -1
  85. package/bin/src/utils/language.js.map +1 -1
  86. package/bin/src/utils/markdown.js +4 -4
  87. package/bin/src/utils/markdown.js.map +1 -1
  88. package/bin/src/utils/updateCheck.js +5 -5
  89. package/bin/src/utils/updateCheck.js.map +1 -1
  90. package/package.json +2 -2
  91. package/web/app.js +927 -927
@@ -186,6 +186,75 @@ export const AGENT_TOOLS = [
186
186
  required: ['pattern'],
187
187
  },
188
188
  },
189
+ {
190
+ name: 'tree_view',
191
+ description: 'Generate a visual ASCII/Unicode directory tree of a workspace path to visualize folder structures.',
192
+ parameters: {
193
+ type: 'object',
194
+ properties: {
195
+ path: { type: 'string', description: 'Relative path to directory (default "." for workspace root)' },
196
+ maxDepth: { type: 'number', description: 'Maximum depth limit for tree traversal (default 3)' },
197
+ },
198
+ required: [],
199
+ },
200
+ },
201
+ {
202
+ name: 'file_info',
203
+ description: 'Get file statistics including file size, total line count, file extension, and last modification timestamp.',
204
+ parameters: {
205
+ type: 'object',
206
+ properties: {
207
+ path: { type: 'string', description: 'Relative path to the target file' },
208
+ },
209
+ required: ['path'],
210
+ },
211
+ },
212
+ {
213
+ name: 'multi_edit_file',
214
+ description: 'Apply multiple code snippet replacements across a file in a single step to execute complex refactoring efficiently.',
215
+ parameters: {
216
+ type: 'object',
217
+ properties: {
218
+ path: { type: 'string', description: 'Relative path to the target file' },
219
+ edits: {
220
+ type: 'array',
221
+ description: 'Array of target and replacement pairs to apply sequentially',
222
+ items: {
223
+ type: 'object',
224
+ properties: {
225
+ target: { type: 'string', description: 'Exact code snippet to replace' },
226
+ replacement: { type: 'string', description: 'New code replacement snippet' },
227
+ },
228
+ required: ['target', 'replacement'],
229
+ },
230
+ },
231
+ reason: { type: 'string', description: 'Explanation of why these multi-edits are being applied' },
232
+ },
233
+ required: ['path', 'edits'],
234
+ },
235
+ },
236
+ {
237
+ name: 'fetch_url',
238
+ description: 'Fetch content or documentation from an external HTTP/HTTPS URL for API docs, technical references, or specs.',
239
+ parameters: {
240
+ type: 'object',
241
+ properties: {
242
+ url: { type: 'string', description: 'The absolute HTTP or HTTPS URL to fetch' },
243
+ },
244
+ required: ['url'],
245
+ },
246
+ },
247
+ {
248
+ name: 'git_diff',
249
+ description: 'View current uncommitted git changes across the workspace or for a target file path.',
250
+ parameters: {
251
+ type: 'object',
252
+ properties: {
253
+ path: { type: 'string', description: 'Optional relative file path filter' },
254
+ },
255
+ required: [],
256
+ },
257
+ },
189
258
  {
190
259
  name: 'ask_user',
191
260
  description: 'ONLY use for missing API keys or secret credentials. DO NOT use to ask the user what code to write or to re-confirm their prompt—take immediate autonomous tool action instead.',
@@ -209,6 +278,51 @@ export const AGENT_TOOLS = [
209
278
  },
210
279
  },
211
280
  ];
281
+ export function buildDirectoryTree(dirPath, prefix = '', currentDepth = 0, maxDepth = 3) {
282
+ if (currentDepth >= maxDepth)
283
+ return `${prefix}└── ... [depth limit reached]\n`;
284
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory())
285
+ return '';
286
+ let result = '';
287
+ let entries = [];
288
+ try {
289
+ entries = fs.readdirSync(dirPath).filter((e) => !['.git', 'node_modules', '.ft', 'dist', 'bin/src/index.js'].includes(e));
290
+ }
291
+ catch {
292
+ return '';
293
+ }
294
+ entries.sort((a, b) => {
295
+ try {
296
+ const aIsDir = fs.statSync(path.join(dirPath, a)).isDirectory();
297
+ const bIsDir = fs.statSync(path.join(dirPath, b)).isDirectory();
298
+ if (aIsDir && !bIsDir)
299
+ return -1;
300
+ if (!aIsDir && bIsDir)
301
+ return 1;
302
+ }
303
+ catch { }
304
+ return a.localeCompare(b);
305
+ });
306
+ entries.forEach((entry, idx) => {
307
+ const isLast = idx === entries.length - 1;
308
+ const connector = isLast ? '└── ' : '├── ';
309
+ const childPath = path.join(dirPath, entry);
310
+ let isDir = false;
311
+ try {
312
+ isDir = fs.statSync(childPath).isDirectory();
313
+ }
314
+ catch { }
315
+ if (isDir) {
316
+ result += `${prefix}${connector}${entry}/\n`;
317
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
318
+ result += buildDirectoryTree(childPath, childPrefix, currentDepth + 1, maxDepth);
319
+ }
320
+ else {
321
+ result += `${prefix}${connector}${entry}\n`;
322
+ }
323
+ });
324
+ return result;
325
+ }
212
326
  export class AgentExecutionLoop {
213
327
  cwd;
214
328
  historyMessages = [];
@@ -217,6 +331,9 @@ export class AgentExecutionLoop {
217
331
  autoApprove;
218
332
  maxSteps;
219
333
  projectName;
334
+ startTime = Date.now();
335
+ totalToolCalls = 0;
336
+ currentStep = 0;
220
337
  constructor(options) {
221
338
  this.cwd = process.cwd();
222
339
  this.autoApprove = Boolean(options?.autoApprove);
@@ -229,10 +346,21 @@ export class AgentExecutionLoop {
229
346
  getBackups() {
230
347
  return this.backups;
231
348
  }
349
+ getStats() {
350
+ return {
351
+ totalSteps: this.currentStep,
352
+ totalToolCalls: this.totalToolCalls,
353
+ modifiedFilesCount: this.modifiedFiles.size,
354
+ elapsedMs: Date.now() - this.startTime,
355
+ };
356
+ }
232
357
  resetContext() {
233
358
  this.historyMessages = [];
234
359
  this.modifiedFiles.clear();
235
360
  this.backups = [];
361
+ this.startTime = Date.now();
362
+ this.totalToolCalls = 0;
363
+ this.currentStep = 0;
236
364
  }
237
365
  systemPrompt() {
238
366
  const filesList = getDirectoryFiles(this.cwd).slice(0, 100);
@@ -248,29 +376,32 @@ Core Directives & Behavioral Guidelines:
248
376
  1. HIGH AGENCY & AUTONOMY: You act autonomously through step-by-step tool invocation to solve complex coding tasks, debug issues, build features, create projects from scratch, or answer architectural questions.
249
377
  2. MULTILINGUAL & INTENT COMPREHENSION: Understand user intent in any language (English, Hinglish like "likho", "bnao", "code karo", "fix karo", "samjha do", Hindi, Spanish, etc.).
250
378
  3. AUTONOMOUS TARGET FILE DISCOVERY: You have full autonomy to locate, inspect, and modify target workspace files. When given a goal without an explicit file path, use auto-discovered target files, \`grep_search\`, or \`glob_search\` to locate relevant code before editing.
251
- 4. COMPLETE FILE CREATION & EDITING: Whenever asked to write, edit, create, implement, or fix code, YOU MUST CREATE / MODIFY ALL NECESSARY FILES required to completely fulfill the prompt (e.g. main source files, component files, styles, configs, tests, documentation). CALL \`write_file\` or \`edit_file\` for EVERY necessary file to save changes directly to disk in the workspace! Always provide a clear \`reason\` argument for why the edit is made.
252
- 5. RUN & VERIFY COMMANDS: Execute shell commands, tests, builds, and formatters using \`run_command\`. If a command or build fails, read the full error log, diagnose the root cause, and auto-fix the failing code immediately.
253
- 6. NO CODE TRUNCATION OR PLACEHOLDERS: Maintain 100% code integrity. NEVER replace existing code or HTML tags with placeholder comments (e.g. \`<!-- remaining content -->\` or \`// TODO: rest of code\`) or stripped skeleton structures. Always output complete, functional code.
254
- 7. REFORMATTING MINIFIED / LARGE FILES: When asked to format, beautify, or un-minify HTML, JS, CSS, or JSON files, DO NOT attempt to rewrite the file content manually using \`write_file\` or \`edit_file\` (which causes token truncation and content loss). ALWAYS use \`run_command\` to execute CLI formatters (e.g. \`npx prettier --write <file>\`, \`npx html-beautify -r <file>\`, \`npx js-beautify <file>\`, or run a quick Node script) to reformat files in-place on disk.
255
- 8. FROM-SCRATCH PROJECT CREATION & AUTOMATED TESTING PROTOCOL: When asked to build/create a project from scratch:
256
- a. Scaffold Architecture: Create configuration/manifest files (\`package.json\`, \`tsconfig.json\`, \`pyproject.toml\`, etc.) and folder structure using \`write_file\` or \`run_command\`.
257
- b. Write Source Implementation: Create all necessary source files with fully functional, complete, production-ready code (no skeleton placeholders or TODOs).
258
- c. Write Automated Tests: Create comprehensive unit/integration test suite files (e.g. \`test/*.test.ts\`, \`tests/test_*.py\`, etc.).
259
- d. Execute Tests & Build Verification: Run test suites and builds via \`run_command\` (e.g. \`npm test\`, \`npx vitest run\`, \`pytest\`, \`cargo test\`, \`go test ./...\`).
260
- e. Self-Heal & Pass: If tests or builds fail, inspect output logs, edit files using \`edit_file\` or \`write_file\` to resolve errors, re-run tests until 100% passing, and only call \`task_completed\` when all checks pass cleanly.
261
- 9. CRITICAL TERMINATION RULE: As soon as you have finished creating/modifying the necessary files, running verification checks, or answering the user request, YOU MUST CALL \`task_completed\` tool immediately with a clear summary of your work.
262
- 10. PRODUCTION-GRADE CODE QUALITY: Every line of code you write or edit MUST be clean, elegant, modular, production-ready, highly readable, self-documenting, formatted, and strictly typed.
263
- 11. HIGH AUTONOMY & NO DOUBLE PROMPTING: NEVER call \`ask_user\` to ask what code to write, what features to implement, or to re-confirm the prompt. Take immediate autonomous action using \`write_file\`, \`edit_file\`, \`run_command\`, \`read_file\`, or \`grep_search\` using best software engineering practices. ONLY call \`ask_user\` if a critical secret credential (API key/password) is strictly required.
264
- 12. EDITING & DEBUGGING MASTERY PROTOCOL:
265
- a. Full Context Reading: When editing or fixing bugs in existing files, inspect full code context. For files under 250 lines, reference the complete file content before modifying.
266
- b. Guaranteed Edit Success: Use \`edit_file\` for precise snippet updates. If \`edit_file\` returns a target snippet mismatch error, IMMEDIATELY call \`write_file\` with the complete corrected file code so the edit is 100% applied without getting stuck!
267
- c. Automated Error Diagnostics: After editing, check compiler/syntax outputs and test runner logs. If an error is detected, auto-heal the code immediately before concluding.
379
+ 4. ACTION-FIRST & DIRECT EXECUTION PRIORITY: When given a goal to fix, edit, modify, or create files, TAKE CODE MODIFICATION ACTIONS (\`write_file\`, \`edit_file\`, or \`run_command\`) IMMEDIATELY within the first 1-3 steps! DO NOT waste multiple steps reading unrelated asset files, sound generators, raw text files, init files, or requirements files unless specifically requested by the user.
380
+ 5. PRE-LOADED CONTEXT RULE: If target files or auto-discovered files are already previewed in your context window history, DO NOT call \`read_file\` or \`list_dir\` on them again! Proceed directly to writing/editing code.
381
+ 6. COMPLETE FILE CREATION & EDITING: Whenever asked to write, edit, create, implement, or fix code, YOU MUST CREATE / MODIFY ALL NECESSARY FILES required to completely fulfill the prompt (e.g. main source files, component files, styles, configs, tests, documentation). CALL \`write_file\` or \`edit_file\` for EVERY necessary file to save changes directly to disk in the workspace! Always provide a clear \`reason\` argument for why the edit is made.
382
+ 7. RUN & VERIFY COMMANDS: Execute shell commands, tests, builds, and formatters using \`run_command\`. If a command or build fails, read the full error log, diagnose the root cause, and auto-fix the failing code immediately.
383
+ 8. NO CODE TRUNCATION OR PLACEHOLDERS: Maintain 100% code integrity. NEVER replace existing code or HTML tags with placeholder comments (e.g. \`<!-- remaining content -->\` or \`// TODO: rest of code\`) or stripped skeleton structures. Always output complete, functional code.
384
+ 9. REFORMATTING MINIFIED / LARGE FILES: When asked to format, beautify, or un-minify HTML, JS, CSS, or JSON files, DO NOT attempt to rewrite the file content manually using \`write_file\` or \`edit_file\` (which causes token truncation and content loss). ALWAYS use \`run_command\` to execute CLI formatters (e.g. \`npx prettier --write <file>\`, \`npx html-beautify -r <file>\`, \`npx js-beautify <file>\`, or run a quick Node script) to reformat files in-place on disk.
385
+ 10. FROM-SCRATCH PROJECT CREATION & AUTOMATED TESTING PROTOCOL: When asked to build/create a project from scratch:
386
+ a. Scaffold Architecture: Create configuration/manifest files (\`package.json\`, \`tsconfig.json\`, \`pyproject.toml\`, etc.) and folder structure using \`write_file\` or \`run_command\`.
387
+ b. Write Source Implementation: Create all necessary source files with fully functional, complete, production-ready code (no skeleton placeholders or TODOs).
388
+ c. Write Automated Tests: Create comprehensive unit/integration test suite files (e.g. \`test/*.test.ts\`, \`tests/test_*.py\`, etc.).
389
+ d. Execute Tests & Build Verification: Run test suites and builds via \`run_command\` (e.g. \`npm test\`, \`npx vitest run\`, \`pytest\`, \`cargo test\`, \`go test ./...\`).
390
+ e. Self-Heal & Pass: If tests or builds fail, inspect output logs, edit files using \`edit_file\` or \`write_file\` to resolve errors, re-run tests until 100% passing, and only call \`task_completed\` when all checks pass cleanly.
391
+ 11. CRITICAL TERMINATION RULE: As soon as you have finished creating/modifying the necessary files, running verification checks, or answering the user request, YOU MUST CALL \`task_completed\` tool immediately with a clear summary of your work.
392
+ 12. PRODUCTION-GRADE CODE QUALITY: Every line of code you write or edit MUST be clean, elegant, modular, production-ready, highly readable, self-documenting, formatted, and strictly typed.
393
+ 13. HIGH AUTONOMY & NO DOUBLE PROMPTING: NEVER call \`ask_user\` to ask what code to write, what features to implement, or to re-confirm the prompt. Take immediate autonomous action using \`write_file\`, \`edit_file\`, \`run_command\`, \`read_file\`, or \`grep_search\` using best software engineering practices. ONLY call \`ask_user\` if a critical secret credential (API key/password) is strictly required.
394
+ 14. EDITING & DEBUGGING MASTERY PROTOCOL:
395
+ a. Full Context Reading: When editing or fixing bugs in existing files, inspect full code context. For files under 250 lines, reference the complete file content before modifying.
396
+ b. Guaranteed Edit Success: Use \`edit_file\` for precise snippet updates. If \`edit_file\` returns a target snippet mismatch error, IMMEDIATELY call \`write_file\` with the complete corrected file code so the edit is 100% applied without getting stuck!
397
+ c. Automated Error Diagnostics: After editing, check compiler/syntax outputs and test runner logs. If an error is detected, auto-heal the code immediately before concluding.
398
+ 15. THOUGHT TRANSPARENCY PROTOCOL: In EVERY step before calling any tools or ending a turn, you MUST provide a clear 1-2 sentence explanation in your text response describing your current reasoning, what file/action you are taking, and why you are taking it.
268
399
 
269
400
  When returning tool calls, use standard OpenAI function calling format or JSON tool call payload format:
270
401
  \`\`\`json
271
402
  {
272
- "tool": "tool_name",
273
- "args": { ... }
403
+ "tool": "tool_name",
404
+ "args": { ... }
274
405
  }
275
406
  \`\`\`
276
407
  `.trim();
@@ -278,33 +409,61 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
278
409
  async executeGoal(userGoal) {
279
410
  this.resetContext();
280
411
  this.historyMessages.push({ role: 'system', content: this.systemPrompt() });
281
- // Autonomous Target File Discovery: Find relevant workspace files matching terms in userGoal
412
+ // Robust Target File Discovery: Extract file paths/names directly from userGoal without destroying backslashes
282
413
  const filesList = getDirectoryFiles(this.cwd);
283
- const stopWords = new Set([
284
- 'the', 'a', 'an', 'in', 'is', 'for', 'to', 'of', 'and', 'or', 'on', 'at', 'by', 'from',
285
- 'with', 'this', 'that', 'it', 'code', 'file', 'files', 'fix', 'add', 'make', 'create',
286
- 'update', 'change', 'modify', 'edit', 'write', 'implement', 'bro', 'please', 'scout',
287
- 'agent', 'bug', 'error', 'issue', 'help', 'app', 'project', 'script', 'function',
288
- 'folder', 'dir', 'directory', 'testing', 'inside', 'open', 'vs', 'vscode', 'py', 'python',
289
- 'new', 'build', 'run', 'test', 'suite'
290
- ]);
291
- const words = userGoal.split(/[\s,;:\/\\]+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
292
414
  const matchedFiles = [];
293
- // 1. Check direct path or filename matches
294
- for (const word of words) {
295
- if (word && word.includes('.') && !word.startsWith('.')) {
296
- const fullP = path.resolve(this.cwd, word);
297
- if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
298
- const relP = path.relative(this.cwd, fullP).replace(/\\/g, '/');
299
- if (!matchedFiles.includes(relP))
300
- matchedFiles.push(relP);
415
+ const isIgnoredFile = (filePath) => {
416
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
417
+ const ext = path.extname(norm);
418
+ const ignoredExts = ['.pyc', '.pyo', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.exe', '.bin', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz'];
419
+ return (norm.includes('__pycache__/') ||
420
+ norm.includes('.git/') ||
421
+ norm.includes('node_modules/') ||
422
+ norm.includes('.ft/') ||
423
+ ignoredExts.includes(ext));
424
+ };
425
+ // Match potential paths (e.g. C:\path\to\file.py, pacman_game/game_map.py, game_map.py)
426
+ const pathMatches = userGoal.match(/(?:[a-zA-Z]:[\\/][^\s"':;,]+|\.?[\\/][^\s"':;,]+\.[a-zA-Z0-9]+|[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)/g) || [];
427
+ for (const rawPath of pathMatches) {
428
+ const cleanP = rawPath.replace(/^["'()]+|["'()]+$/g, '').trim();
429
+ if (!cleanP || cleanP.length < 3)
430
+ continue;
431
+ const fullP = path.isAbsolute(cleanP) ? cleanP : path.resolve(this.cwd, cleanP);
432
+ if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
433
+ const relP = path.relative(this.cwd, fullP).replace(/\\/g, '/');
434
+ if (!isIgnoredFile(relP) && !matchedFiles.includes(relP))
435
+ matchedFiles.push(relP);
436
+ }
437
+ else {
438
+ // Check if any file in filesList matches or ends with the extracted relative path or filename
439
+ const normToken = cleanP.replace(/\\/g, '/').toLowerCase();
440
+ const baseName = path.basename(normToken);
441
+ for (const f of filesList) {
442
+ if (isIgnoredFile(f))
443
+ continue;
444
+ const normF = f.replace(/\\/g, '/').toLowerCase();
445
+ if (normF === normToken || normF.endsWith('/' + normToken) || (baseName.includes('.') && normF.endsWith('/' + baseName))) {
446
+ if (!matchedFiles.includes(f))
447
+ matchedFiles.push(f);
448
+ }
301
449
  }
302
450
  }
303
451
  }
304
- // 2. If no direct path matched, search workspace file names for high-confidence keyword matches
452
+ // Keyword fallback if no direct path matched
305
453
  if (matchedFiles.length === 0) {
454
+ const stopWords = new Set([
455
+ 'the', 'a', 'an', 'in', 'is', 'for', 'to', 'of', 'and', 'or', 'on', 'at', 'by', 'from',
456
+ 'with', 'this', 'that', 'it', 'code', 'file', 'files', 'fix', 'add', 'make', 'create',
457
+ 'update', 'change', 'modify', 'edit', 'write', 'implement', 'bro', 'please', 'scout',
458
+ 'agent', 'bug', 'error', 'issue', 'help', 'app', 'project', 'script', 'function',
459
+ 'folder', 'dir', 'directory', 'testing', 'inside', 'open', 'vs', 'vscode', 'py', 'python',
460
+ 'new', 'build', 'run', 'test', 'suite'
461
+ ]);
462
+ const words = userGoal.split(/[\s,;:\/\\]+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
306
463
  const keywords = words.filter((w) => w.length >= 3 && !stopWords.has(w.toLowerCase()));
307
464
  for (const f of filesList) {
465
+ if (isIgnoredFile(f))
466
+ continue;
308
467
  if (matchedFiles.length >= 3)
309
468
  break;
310
469
  const normF = f.replace(/\\/g, '/').toLowerCase();
@@ -324,17 +483,17 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
324
483
  const fullP = path.resolve(this.cwd, file);
325
484
  if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
326
485
  const analyzeSpin = spinner();
327
- analyzeSpin.start(chalk.cyan(`🔍 Auto-discovered target file: ${file}. Analyzing...`));
486
+ analyzeSpin.start(chalk.cyan(` Auto-discovered target file: ${file}. Analyzing...`));
328
487
  try {
329
488
  const content = fs.readFileSync(fullP, 'utf-8');
330
489
  const lines = content.split('\n');
331
- analyzeSpin.stop(chalk.green(`✓ Auto-discovered & analyzed ${file} (${lines.length} lines)`));
490
+ analyzeSpin.stop(chalk.green(` Auto-discovered & analyzed ${file} (${lines.length} lines)`));
332
491
  const previewSnippet = lines.length <= 250
333
492
  ? lines.map((l, i) => `${i + 1} | ${l}`).join('\n')
334
493
  : lines.slice(0, 100).map((l, i) => `${i + 1} | ${l}`).join('\n') + `\n... [truncated, file has ${lines.length} lines. Call read_file to view remaining lines]`;
335
494
  this.historyMessages.push({
336
495
  role: 'user',
337
- content: `[Reference Workspace Context - Auto-discovered file "${file}" (${lines.length} lines)]:\n\`\`\`\n${previewSnippet}\n\`\`\`\nNote: Use this complete file content as reference context for editing, debugging, or fulfilling the user goal.`,
496
+ content: `[Reference Workspace Context - Pre-loaded target file "${file}" (${lines.length} lines)]:\n\`\`\`\n${previewSnippet}\n\`\`\`\nCRITICAL DIRECTIVE: The target file "${file}" is ALREADY fully pre-loaded into your reference context above. DO NOT call read_file or list_dir for this file again! Immediately begin calling write_file, edit_file, or run_command to satisfy the user goal.`,
338
497
  });
339
498
  }
340
499
  catch {
@@ -350,12 +509,14 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
350
509
  }
351
510
  const estTotalSecs = Math.round(this.maxSteps * 12);
352
511
  const estTimeText = estTotalSecs >= 60 ? `~${(estTotalSecs / 60).toFixed(1)}m` : `~${estTotalSecs}s`;
353
- console.log(chalk.gray(`⏱Estimated completion time: ${estTimeText} (Max ${this.maxSteps} steps)`));
512
+ console.log(chalk.gray(`Estimated completion time: ${estTimeText} (Max ${this.maxSteps} steps)`));
354
513
  let step = 0;
355
514
  let finalSummary = '';
515
+ let consecutiveReads = 0;
356
516
  const stepDurations = [];
357
517
  while (step < this.maxSteps) {
358
518
  step++;
519
+ this.currentStep = step;
359
520
  const stepStartTime = Date.now();
360
521
  const avgStepMs = stepDurations.length > 0
361
522
  ? stepDurations.reduce((a, b) => a + b, 0) / stepDurations.length
@@ -400,6 +561,11 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
400
561
  }
401
562
  const msg = sanitizeMessage(choice.message);
402
563
  this.historyMessages.push(msg);
564
+ // Render assistant's thought/reasoning text if provided on this turn
565
+ const textContent = msg.content || '';
566
+ if (textContent.trim()) {
567
+ safeNote(renderMarkdown(textContent), `Scout Thought (Step ${step})`);
568
+ }
403
569
  // Check native tool calls
404
570
  if (msg.tool_calls && msg.tool_calls.length > 0) {
405
571
  stepSpinner.stop(chalk.green(`Step ${step}: Scout issued ${msg.tool_calls.length} tool call(s).`));
@@ -417,16 +583,28 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
417
583
  tool_call_id: tc.id,
418
584
  content: `Tool Execution Result (${fnName}):\n${toolResult.result}`,
419
585
  });
586
+ if (fnName === 'read_file' || fnName === 'list_dir' || fnName === 'glob_search' || fnName === 'grep_search') {
587
+ consecutiveReads++;
588
+ if (consecutiveReads >= 2) {
589
+ this.historyMessages.push({
590
+ role: 'user',
591
+ content: 'SYSTEM ACTION NUDGE: You have issued multiple read/exploration tool calls in a row. Stop reading files now and execute write_file, edit_file, or run_command immediately to satisfy the user goal.',
592
+ });
593
+ }
594
+ }
595
+ else if (fnName === 'write_file' || fnName === 'edit_file' || fnName === 'run_command') {
596
+ consecutiveReads = 0;
597
+ }
420
598
  if (fnName === 'task_completed') {
421
599
  finalSummary = args.summary || toolResult.result;
422
600
  if (this.modifiedFiles.size > 0) {
423
601
  const filesList = getDirectoryFiles(this.cwd);
424
602
  const healRes = await verifyAndSelfHealFiles(Array.from(this.modifiedFiles), this.cwd, this.projectName, filesList, { maxRetries: 3 });
425
603
  if (healRes.verifiedFiles.length > 0) {
426
- safeNote(chalk.green(`✓ Self-Healing Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build clean!`), '✅ Code Verification Clean');
604
+ safeNote(chalk.green(` Self-Healing Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build clean!`), ' Code Verification Clean');
427
605
  }
428
606
  if (healRes.remainingErrors.length > 0) {
429
- safeNote(chalk.yellow(`⚠️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '⚠️ Verification Warning');
607
+ safeNote(chalk.yellow(`️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '️ Verification Warning');
430
608
  }
431
609
  }
432
610
  return { success: true, summary: finalSummary };
@@ -436,7 +614,6 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
436
614
  continue;
437
615
  }
438
616
  // Check text response or fallback JSON tool call
439
- const textContent = msg.content || '';
440
617
  stepSpinner.stop(chalk.blue(`Step ${step} thinking complete.`));
441
618
  const parsedJsonTool = this.extractJsonToolCall(textContent);
442
619
  if (parsedJsonTool) {
@@ -452,7 +629,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
452
629
  continue;
453
630
  }
454
631
  if (textContent.trim()) {
455
- safeNote(renderMarkdown(textContent), `🤖 Scout Agent Thought (Step ${step})`);
632
+ safeNote(renderMarkdown(textContent), ` Scout Agent Thought (Step ${step})`);
456
633
  const lowerText = textContent.toLowerCase();
457
634
  const isExplicitCompletion = lowerText.includes('task is complete') ||
458
635
  lowerText.includes('task complete') ||
@@ -483,14 +660,15 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
483
660
  catch (err) {
484
661
  stepDurations.push(Date.now() - stepStartTime);
485
662
  if (isQuotaExceededError(err)) {
486
- stepSpinner.stop(chalk.red(`❌ API Quota Exceeded (Step ${step}).`));
487
- safeNote(`${chalk.bold.red('❌ FrontTerrain API Quota Exceeded!')}\n\n` +
488
- `${chalk.yellow('You have reached the API usage limit or rate cap for FrontTerrain AI service.')}\n\n` +
489
- `${chalk.bold.cyan('⏳ Please come back and try again in a few hours (or check back later today).')}\n\n` +
490
- `${chalk.dim('Scout Agent session has ended gracefully to protect remaining workflow.')}`, '⚠️ API Quota Limit Reached');
663
+ stepSpinner.stop(chalk.red(`Oops! it\'s not you, it\'s us`));
664
+ safeNote(`${chalk.bold.red(`Something went wrong in (Step ${step}), please try again in a moment`)}\n\n` +
665
+ `${chalk.yellow('The configured AI provider has reached its API usage limit or rate cap.')}\n` +
666
+ `${chalk.dim('This is separate from your Scout credit balance shown by `scout quota`.')}\n\n` +
667
+ `${chalk.bold.cyan(' Please come back and try again in a few hours (or check back later today).')}\n\n` +
668
+ `${chalk.dim('Scout Agent session has ended gracefully to protect remaining workflow.')}`, '️ API Quota Limit Reached');
491
669
  return {
492
670
  success: false,
493
- summary: 'FrontTerrain API Quota Exceeded. Please come back in a few hours.',
671
+ summary: 'Something went wrong, please try again in a moment.',
494
672
  };
495
673
  }
496
674
  stepSpinner.stop(chalk.red(`Step ${step} execution error: ${err?.message || String(err)}`));
@@ -520,7 +698,11 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
520
698
  }
521
699
  return { tool, args };
522
700
  }
523
- const knownTools = ['read_file', 'write_file', 'edit_file', 'run_command', 'list_dir', 'grep_search', 'glob_search', 'ask_user', 'task_completed'];
701
+ const knownTools = [
702
+ 'read_file', 'write_file', 'edit_file', 'run_command', 'list_dir',
703
+ 'grep_search', 'glob_search', 'tree_view', 'file_info', 'multi_edit_file',
704
+ 'fetch_url', 'git_diff', 'ask_user', 'task_completed'
705
+ ];
524
706
  for (const toolName of knownTools) {
525
707
  const tagRegex = new RegExp(`<${toolName}>([\\s\\S]*?)<\\/${toolName}>`, 'i');
526
708
  const match = content.match(tagRegex);
@@ -549,13 +731,42 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
549
731
  return null;
550
732
  }
551
733
  async dispatchToolCall(name, args) {
734
+ this.totalToolCalls++;
735
+ let actionDesc = '';
736
+ if (name === 'read_file')
737
+ actionDesc = `Reading file: ${args.path || ''}`;
738
+ else if (name === 'write_file')
739
+ actionDesc = `Writing/Creating file: ${args.path || ''}`;
740
+ else if (name === 'edit_file')
741
+ actionDesc = `Editing file: ${args.path || ''}`;
742
+ else if (name === 'multi_edit_file')
743
+ actionDesc = `Multi-editing file: ${args.path || ''}`;
744
+ else if (name === 'run_command')
745
+ actionDesc = `Executing command: ${args.command || args.cmd || ''}`;
746
+ else if (name === 'list_dir')
747
+ actionDesc = `Listing directory: ${args.path || '.'}`;
748
+ else if (name === 'tree_view')
749
+ actionDesc = `Generating directory tree: ${args.path || '.'}`;
750
+ else if (name === 'file_info')
751
+ actionDesc = `Inspecting file stats: ${args.path || ''}`;
752
+ else if (name === 'grep_search')
753
+ actionDesc = `Searching pattern: "${args.query || args.pattern || ''}"`;
754
+ else if (name === 'glob_search')
755
+ actionDesc = `Finding files: "${args.pattern || ''}"`;
756
+ else if (name === 'fetch_url')
757
+ actionDesc = `Fetching URL: ${args.url || ''}`;
758
+ else if (name === 'git_diff')
759
+ actionDesc = `Retrieving workspace git diff`;
760
+ else if (name === 'task_completed')
761
+ actionDesc = `Task finished`;
552
762
  const badge = chalk.bgCyan.black.bold(` 🛠️ TOOL: ${name} `);
553
- console.log(`\n${badge} ${chalk.dim(JSON.stringify(args))}`);
763
+ const descText = actionDesc ? chalk.yellow.bold(` → ${actionDesc}`) : '';
764
+ console.log(`\n${badge}${descText} ${chalk.dim(JSON.stringify(args))}`);
554
765
  switch (name) {
555
766
  case 'read_file': {
556
767
  const targetPath = String(args.path || '').trim();
557
768
  const readSpin = spinner();
558
- readSpin.start(chalk.cyan(`🔍 Analyzing file ${targetPath}...`));
769
+ readSpin.start(chalk.cyan(` Analyzing file ${targetPath}...`));
559
770
  const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
560
771
  if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
561
772
  readSpin.stop(chalk.red(`Failed to analyze file ${targetPath}: File not found.`));
@@ -565,7 +776,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
565
776
  const lines = fileContent.split('\n');
566
777
  const start = Math.max(1, Number(args.startLine) || 1);
567
778
  const end = Math.min(lines.length, Number(args.endLine) || lines.length);
568
- readSpin.stop(chalk.green(`✓ Analyzed file ${targetPath} (${lines.length} lines)`));
779
+ readSpin.stop(chalk.green(` Analyzed file ${targetPath} (${lines.length} lines)`));
569
780
  const sliced = lines.slice(start - 1, end).map((l, i) => `${start + i} | ${l}`).join('\n');
570
781
  let minifiedNotice = '';
571
782
  if (lines.length <= 5 && fileContent.length > 500) {
@@ -584,7 +795,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
584
795
  const fileExists = fs.existsSync(fullPath);
585
796
  const actionText = fileExists ? 'Editing file' : 'Creating file';
586
797
  const writeSpin = spinner();
587
- writeSpin.start(chalk.yellow(`✏️ ${actionText} ${targetPath}...`));
798
+ writeSpin.start(chalk.yellow(`️ ${actionText} ${targetPath}...`));
588
799
  if (!this.autoApprove && process.stdout.isTTY) {
589
800
  writeSpin.stop();
590
801
  const ok = await confirm({
@@ -594,7 +805,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
594
805
  if (isCancel(ok) || !ok) {
595
806
  return { result: `User rejected write_file to "${targetPath}"` };
596
807
  }
597
- writeSpin.start(chalk.yellow(`✏️ ${actionText} ${targetPath}...`));
808
+ writeSpin.start(chalk.yellow(`️ ${actionText} ${targetPath}...`));
598
809
  }
599
810
  let origContent = '__FT_WAS_NEW__';
600
811
  if (fileExists) {
@@ -610,20 +821,20 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
610
821
  const oldLineCount = origContent !== '__FT_WAS_NEW__' ? origContent.split('\n').length : 0;
611
822
  const newLineCount = content.split('\n').length;
612
823
  const numberOfLinesChanged = origContent === '__FT_WAS_NEW__' ? newLineCount : Math.abs(newLineCount - oldLineCount) || newLineCount;
613
- writeSpin.stop(chalk.green(`✓ ${actionText} ${targetPath}`));
824
+ writeSpin.stop(chalk.green(` ${actionText} ${targetPath}`));
614
825
  safeNote(` • File: ${chalk.bold.cyan(targetPath)}\n` +
615
826
  ` • Changed: ${chalk.bold.yellow(numberOfLinesChanged + ' line(s)')}\n` +
616
- ` • Why: ${reason}`, `📝 Changed ${numberOfLinesChanged} line(s) in ${targetPath}`);
827
+ ` • Why: ${reason}`, ` Changed ${numberOfLinesChanged} line(s) in ${targetPath}`);
617
828
  const checkSpin = spinner();
618
- checkSpin.start(chalk.cyan(`🔍 Checking for error: edited ${targetPath}...`));
829
+ checkSpin.start(chalk.cyan(` Checking for error: edited ${targetPath}...`));
619
830
  const syntaxCheck = await checkFileSyntax(targetPath, this.cwd);
620
831
  if (!syntaxCheck.valid) {
621
- checkSpin.stop(chalk.red(`⚠️ Checking for error: edited ${targetPath} — Compiler/Syntax Error!`));
832
+ checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} — Compiler/Syntax Error!`));
622
833
  return {
623
- result: `Successfully wrote ${content.length} characters (${numberOfLinesChanged} lines changed) to "${targetPath}".\n\n⚠️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
834
+ result: `Successfully wrote ${content.length} characters (${numberOfLinesChanged} lines changed) to "${targetPath}".\n\n️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
624
835
  };
625
836
  }
626
- checkSpin.stop(chalk.green(`✓ Checking for error: edited ${targetPath} — Syntax & build clean.`));
837
+ checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} — Syntax & build clean.`));
627
838
  return { result: `Successfully wrote ${content.length} characters (${numberOfLinesChanged} lines changed) to "${targetPath}". Rationale: ${reason}. Syntax check passed cleanly.` };
628
839
  }
629
840
  case 'edit_file': {
@@ -639,7 +850,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
639
850
  }
640
851
  const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
641
852
  const editSpin = spinner();
642
- editSpin.start(chalk.yellow(`✏️ Editing file ${targetPath}...`));
853
+ editSpin.start(chalk.yellow(`️ Editing file ${targetPath}...`));
643
854
  if (!fs.existsSync(fullPath)) {
644
855
  editSpin.stop(chalk.red(`Failed to edit file ${targetPath}: File does not exist.`));
645
856
  return { result: `Error: File "${targetPath}" does not exist.` };
@@ -659,7 +870,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
659
870
  if (isCancel(ok) || !ok) {
660
871
  return { result: `User rejected edit_file to "${targetPath}"` };
661
872
  }
662
- editSpin.start(chalk.yellow(`✏️ Editing file ${targetPath}...`));
873
+ editSpin.start(chalk.yellow(`️ Editing file ${targetPath}...`));
663
874
  }
664
875
  this.backups.push({ filePath: targetPath, originalContent: origContent });
665
876
  fs.writeFileSync(fullPath, replaceRes.updatedContent, 'utf-8');
@@ -667,20 +878,20 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
667
878
  const targetLines = target.split('\n').length;
668
879
  const replacementLines = replacement.split('\n').length;
669
880
  const numberOfLinesChanged = Math.max(targetLines, replacementLines);
670
- editSpin.stop(chalk.green(`✓ Edited file ${targetPath}`));
881
+ editSpin.stop(chalk.green(` Edited file ${targetPath}`));
671
882
  safeNote(` • File: ${chalk.bold.cyan(targetPath)}\n` +
672
883
  ` • Changed: ${chalk.bold.yellow(numberOfLinesChanged + ' line(s)')}\n` +
673
- ` • Why: ${reason}`, `📝 Changed ${numberOfLinesChanged} line(s) in ${targetPath}`);
884
+ ` • Why: ${reason}`, ` Changed ${numberOfLinesChanged} line(s) in ${targetPath}`);
674
885
  const checkSpin = spinner();
675
- checkSpin.start(chalk.cyan(`🔍 Checking for error: edited ${targetPath}...`));
886
+ checkSpin.start(chalk.cyan(` Checking for error: edited ${targetPath}...`));
676
887
  const syntaxCheck = await checkFileSyntax(targetPath, this.cwd);
677
888
  if (!syntaxCheck.valid) {
678
- checkSpin.stop(chalk.red(`⚠️ Checking for error: edited ${targetPath} — Compiler/Syntax Error!`));
889
+ checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} — Compiler/Syntax Error!`));
679
890
  return {
680
- result: `Successfully replaced snippet (${numberOfLinesChanged} lines changed) in "${targetPath}".\n\n⚠️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
891
+ result: `Successfully replaced snippet (${numberOfLinesChanged} lines changed) in "${targetPath}".\n\n️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
681
892
  };
682
893
  }
683
- checkSpin.stop(chalk.green(`✓ Checking for error: edited ${targetPath} — Syntax & build clean.`));
894
+ checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} — Syntax & build clean.`));
684
895
  return { result: `Successfully replaced target snippet (${numberOfLinesChanged} lines changed) in "${targetPath}". Rationale: ${reason}. Syntax check passed cleanly.` };
685
896
  }
686
897
  case 'run_command': {
@@ -697,20 +908,20 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
697
908
  }
698
909
  }
699
910
  const runSpin = spinner();
700
- runSpin.start(chalk.cyan(`⚡ ${actionLabel}...`));
911
+ runSpin.start(chalk.cyan(` ${actionLabel}...`));
701
912
  const res = await executeSmartCommand(cmd, this.cwd, 3000);
702
913
  if (res.isBackgroundServer) {
703
- runSpin.stop(chalk.green(`✓ ${actionLabel} started in background (PID ${res.pid}).`));
914
+ runSpin.stop(chalk.green(` ${actionLabel} started in background (PID ${res.pid}).`));
704
915
  return {
705
916
  result: `Command started and running in background (PID ${res.pid || 'active'}). Output:\n${res.output.slice(0, 8000)}\n\n[Process is running as background server. Scout Agent can proceed directly to next step.]`
706
917
  };
707
918
  }
708
919
  if (res.exitCode === 0) {
709
- runSpin.stop(chalk.green(`✓ ${actionLabel} executed successfully (Exit Code 0).`));
920
+ runSpin.stop(chalk.green(` ${actionLabel} executed successfully (Exit Code 0).`));
710
921
  return { result: `Command Exit Code 0 Output:\n${res.output.slice(0, 8000)}` };
711
922
  }
712
923
  else {
713
- runSpin.stop(chalk.red(`❌ ${actionLabel} failed with code ${res.exitCode}.`));
924
+ runSpin.stop(chalk.red(` ${actionLabel} failed with code ${res.exitCode}.`));
714
925
  return { result: `Command Exit Code ${res.exitCode} Error:\n${res.output.slice(0, 8000)}` };
715
926
  }
716
927
  }
@@ -747,11 +958,137 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
747
958
  const matched = allFiles.filter((f) => f.toLowerCase().includes(pattern) || path.basename(f).toLowerCase().endsWith(pattern.replace('*', '')));
748
959
  return { result: `Glob search "${pattern}" matched ${matched.length} file(s):\n${matched.slice(0, 30).join('\n')}` };
749
960
  }
961
+ case 'tree_view': {
962
+ const relPath = String(args.path || '.').trim();
963
+ const maxDepth = Number(args.maxDepth) || 3;
964
+ const fullDir = path.resolve(this.cwd, relPath);
965
+ if (!fs.existsSync(fullDir) || !fs.statSync(fullDir).isDirectory()) {
966
+ return { result: `Directory not found for tree_view: "${relPath}"` };
967
+ }
968
+ const treeStr = buildDirectoryTree(fullDir, '', 0, maxDepth);
969
+ return { result: `Directory Tree for "${relPath}":\n${relPath}/\n${treeStr}` };
970
+ }
971
+ case 'file_info': {
972
+ const targetPath = String(args.path || '').trim();
973
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
974
+ if (!fs.existsSync(fullPath)) {
975
+ return { result: `Error: Path "${targetPath}" does not exist.` };
976
+ }
977
+ const stat = fs.statSync(fullPath);
978
+ if (stat.isDirectory()) {
979
+ const entries = fs.readdirSync(fullPath);
980
+ return { result: `Directory Stats: "${targetPath}"\n • Total Entries: ${entries.length}\n • Created: ${stat.birthtime.toISOString()}\n • Modified: ${stat.mtime.toISOString()}` };
981
+ }
982
+ const content = fs.readFileSync(fullPath, 'utf-8');
983
+ const lineCount = content.split('\n').length;
984
+ const ext = path.extname(targetPath) || 'none';
985
+ return {
986
+ result: `File Info: "${targetPath}"\n` +
987
+ ` • Size: ${(stat.size / 1024).toFixed(2)} KB (${stat.size} bytes)\n` +
988
+ ` • Total Lines: ${lineCount}\n` +
989
+ ` • Extension: ${ext}\n` +
990
+ ` • Last Modified: ${stat.mtime.toISOString()}\n` +
991
+ ` • Absolute Path: ${fullPath}`
992
+ };
993
+ }
994
+ case 'multi_edit_file': {
995
+ const targetPath = String(args.path || '').trim();
996
+ const edits = Array.isArray(args.edits) ? args.edits : [];
997
+ const reason = String(args.reason || args.description || 'Applied multiple code snippet edits.');
998
+ if (edits.length === 0) {
999
+ return { result: `Error: No edits provided for multi_edit_file on "${targetPath}".` };
1000
+ }
1001
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(this.cwd, targetPath);
1002
+ if (!fs.existsSync(fullPath)) {
1003
+ return { result: `Error: File "${targetPath}" does not exist.` };
1004
+ }
1005
+ let currentContent = fs.readFileSync(fullPath, 'utf-8');
1006
+ const origContent = currentContent;
1007
+ let appliedCount = 0;
1008
+ const errors = [];
1009
+ for (let i = 0; i < edits.length; i++) {
1010
+ const ed = edits[i];
1011
+ let target = String(ed.target || '');
1012
+ let replacement = String(ed.replacement || '');
1013
+ if (target.includes('\\n') && !target.includes('\n'))
1014
+ target = target.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
1015
+ if (replacement.includes('\\n') && !replacement.includes('\n'))
1016
+ replacement = replacement.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
1017
+ const replaceRes = robustSnippetReplace(currentContent, target, replacement);
1018
+ if (replaceRes.success) {
1019
+ currentContent = replaceRes.updatedContent;
1020
+ appliedCount++;
1021
+ }
1022
+ else {
1023
+ errors.push(`Edit #${i + 1} failed: ${replaceRes.error}`);
1024
+ }
1025
+ }
1026
+ if (appliedCount === 0) {
1027
+ return { result: `Error: All ${edits.length} multi-edits failed on "${targetPath}":\n${errors.join('\n')}` };
1028
+ }
1029
+ this.backups.push({ filePath: targetPath, originalContent: origContent });
1030
+ fs.writeFileSync(fullPath, currentContent, 'utf-8');
1031
+ this.modifiedFiles.add(targetPath);
1032
+ safeNote(` • File: ${chalk.bold.cyan(targetPath)}\n` +
1033
+ ` • Multi-edits applied: ${chalk.bold.green(appliedCount + '/' + edits.length)}\n` +
1034
+ ` • Why: ${reason}`, ` Applied ${appliedCount} edits to ${targetPath}`);
1035
+ const syntaxCheck = await checkFileSyntax(targetPath, this.cwd);
1036
+ let syntaxNotice = syntaxCheck.valid ? 'Syntax check passed cleanly.' : `\n\n️ AUTOMATED SYNTAX ERROR DETECTED:\n${syntaxCheck.errors.join('\n')}`;
1037
+ return {
1038
+ result: `Successfully applied ${appliedCount}/${edits.length} edit snippet(s) to "${targetPath}". Rationale: ${reason}.${errors.length > 0 ? ` Warnings: ${errors.join('; ')}` : ''} ${syntaxNotice}`
1039
+ };
1040
+ }
1041
+ case 'fetch_url': {
1042
+ const targetUrl = String(args.url || '').trim();
1043
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
1044
+ return { result: `Error: Invalid URL scheme. URL must start with http:// or https://` };
1045
+ }
1046
+ const fetchSpin = spinner();
1047
+ fetchSpin.start(chalk.cyan(` Fetching web URL ${targetUrl}...`));
1048
+ try {
1049
+ const resp = await fetch(targetUrl, { headers: { 'User-Agent': 'Scout-CLI-Agent/6.0' } });
1050
+ if (!resp.ok) {
1051
+ fetchSpin.stop(chalk.red(`Failed to fetch URL: HTTP ${resp.status} ${resp.statusText}`));
1052
+ return { result: `HTTP Error ${resp.status}: ${resp.statusText}` };
1053
+ }
1054
+ let bodyText = await resp.text();
1055
+ fetchSpin.stop(chalk.green(` Fetched URL ${targetUrl} (${bodyText.length} chars)`));
1056
+ if (bodyText.includes('<html') || bodyText.includes('<body')) {
1057
+ bodyText = bodyText
1058
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
1059
+ .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
1060
+ .replace(/<[^>]+>/g, ' ')
1061
+ .replace(/\s+/g, ' ')
1062
+ .trim();
1063
+ }
1064
+ const preview = bodyText.slice(0, 6000) + (bodyText.length > 6000 ? `\n... [truncated, ${bodyText.length} total chars]` : '');
1065
+ return { result: `Content fetched from ${targetUrl}:\n\n${preview}` };
1066
+ }
1067
+ catch (err) {
1068
+ fetchSpin.stop(chalk.red(`Fetch error for ${targetUrl}: ${err?.message || String(err)}`));
1069
+ return { result: `Fetch error: ${err?.message || String(err)}` };
1070
+ }
1071
+ }
1072
+ case 'git_diff': {
1073
+ const targetPath = args.path ? String(args.path).trim() : '';
1074
+ try {
1075
+ const { execSync } = await import('child_process');
1076
+ const diffCmd = targetPath ? `git diff -- "${targetPath}"` : 'git diff';
1077
+ const diffOutput = execSync(diffCmd, { encoding: 'utf-8', cwd: this.cwd });
1078
+ if (!diffOutput || !diffOutput.trim()) {
1079
+ return { result: 'No uncommitted git changes found in workspace.' };
1080
+ }
1081
+ return { result: `Git Diff Output:\n${diffOutput.slice(0, 8000)}` };
1082
+ }
1083
+ catch (err) {
1084
+ return { result: `Could not retrieve git diff: ${err?.message || String(err)}` };
1085
+ }
1086
+ }
750
1087
  case 'ask_user': {
751
1088
  const questionText = String(args.question || args.prompt || 'Please provide input:').trim();
752
1089
  const askSpin = spinner();
753
1090
  askSpin.stop();
754
- safeNote(questionText, '❓ Scout Agent Question for User');
1091
+ safeNote(questionText, ' Scout Agent Question for User');
755
1092
  const userInput = await text({
756
1093
  message: chalk.cyan.bold('Your response:'),
757
1094
  placeholder: 'Type your answer or instruction here...',