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.
- package/.firebase/hosting.d2Vi.cache +2 -2
- package/README.md +68 -44
- package/bin/ft.js +1 -1
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +264 -92
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/audit.js +1 -1
- package/bin/src/commands/audit.js.map +1 -1
- package/bin/src/commands/authCommand.js +4 -4
- package/bin/src/commands/authCommand.js.map +1 -1
- package/bin/src/commands/brief.js +4 -4
- package/bin/src/commands/brief.js.map +1 -1
- package/bin/src/commands/configCommand.d.ts.map +1 -1
- package/bin/src/commands/configCommand.js +7 -7
- package/bin/src/commands/configCommand.js.map +1 -1
- package/bin/src/commands/dashboard.js +4 -4
- package/bin/src/commands/dashboard.js.map +1 -1
- package/bin/src/commands/history.js +5 -5
- package/bin/src/commands/history.js.map +1 -1
- package/bin/src/commands/init.js +26 -26
- package/bin/src/commands/init.js.map +1 -1
- package/bin/src/commands/login.d.ts.map +1 -1
- package/bin/src/commands/login.js +21 -21
- package/bin/src/commands/login.js.map +1 -1
- package/bin/src/commands/logout.js +1 -1
- package/bin/src/commands/logout.js.map +1 -1
- package/bin/src/commands/owners.js +5 -5
- package/bin/src/commands/owners.js.map +1 -1
- package/bin/src/commands/quota.d.ts.map +1 -0
- package/bin/src/commands/quota.js +48 -0
- package/bin/src/commands/quota.js.map +1 -0
- package/bin/src/commands/recommend.js +1 -1
- package/bin/src/commands/recommend.js.map +1 -1
- package/bin/src/commands/risky.d.ts.map +1 -1
- package/bin/src/commands/risky.js +9 -9
- package/bin/src/commands/risky.js.map +1 -1
- package/bin/src/commands/setup.js +24 -24
- package/bin/src/commands/setup.js.map +1 -1
- package/bin/src/commands/signup.d.ts.map +1 -1
- package/bin/src/commands/signup.js +7 -7
- package/bin/src/commands/signup.js.map +1 -1
- package/bin/src/commands/undo.js +8 -8
- package/bin/src/commands/undo.js.map +1 -1
- package/bin/src/commands/web.js +8 -8
- package/bin/src/commands/web.js.map +1 -1
- package/bin/src/discovery/config.d.ts.map +1 -1
- package/bin/src/discovery/config.js.map +1 -1
- package/bin/src/discovery/display.js +1 -1
- package/bin/src/discovery/display.js.map +1 -1
- package/bin/src/discovery/messages.js.map +1 -1
- package/bin/src/discovery/signals.d.ts.map +1 -1
- package/bin/src/discovery/signals.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -1
- package/bin/src/engine/agentEngine.js +414 -77
- package/bin/src/engine/agentEngine.js.map +1 -1
- package/bin/src/engine/git.d.ts.map +1 -1
- package/bin/src/engine/git.js.map +1 -1
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +174 -78
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/verifier.d.ts.map +1 -1
- package/bin/src/engine/verifier.js +6 -6
- package/bin/src/engine/verifier.js.map +1 -1
- package/bin/src/engine/web.d.ts.map +1 -1
- package/bin/src/engine/web.js +1 -1
- package/bin/src/engine/web.js.map +1 -1
- package/bin/src/index.js +78 -68
- package/bin/src/index.js.map +1 -1
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +7 -7
- package/bin/src/utils/branding.js.map +1 -1
- package/bin/src/utils/config.d.ts.map +1 -1
- package/bin/src/utils/config.js.map +1 -1
- package/bin/src/utils/email.d.ts.map +1 -1
- package/bin/src/utils/email.js +42 -42
- package/bin/src/utils/email.js.map +1 -1
- package/bin/src/utils/firebaseAuth.d.ts.map +1 -1
- package/bin/src/utils/firebaseAuth.js.map +1 -1
- package/bin/src/utils/holoseneAuth.d.ts.map +1 -1
- package/bin/src/utils/holoseneAuth.js +22 -22
- package/bin/src/utils/holoseneAuth.js.map +1 -1
- package/bin/src/utils/language.d.ts.map +1 -1
- package/bin/src/utils/language.js.map +1 -1
- package/bin/src/utils/markdown.js +4 -4
- package/bin/src/utils/markdown.js.map +1 -1
- package/bin/src/utils/updateCheck.js +5 -5
- package/bin/src/utils/updateCheck.js.map +1 -1
- package/package.json +2 -2
- 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.
|
|
252
|
-
5.
|
|
253
|
-
6.
|
|
254
|
-
7.
|
|
255
|
-
8.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
11.
|
|
264
|
-
12.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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 -
|
|
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(
|
|
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(
|
|
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(
|
|
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),
|
|
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(
|
|
487
|
-
safeNote(`${chalk.bold.red(
|
|
488
|
-
`${chalk.yellow('
|
|
489
|
-
`${chalk.
|
|
490
|
-
`${chalk.
|
|
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: '
|
|
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 = [
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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}`,
|
|
827
|
+
` • Why: ${reason}`, ` Changed ${numberOfLinesChanged} line(s) in ${targetPath}`);
|
|
617
828
|
const checkSpin = spinner();
|
|
618
|
-
checkSpin.start(chalk.cyan(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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}`,
|
|
884
|
+
` • Why: ${reason}`, ` Changed ${numberOfLinesChanged} line(s) in ${targetPath}`);
|
|
674
885
|
const checkSpin = spinner();
|
|
675
|
-
checkSpin.start(chalk.cyan(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, '
|
|
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...',
|