ft-scout 6.0.5 → 6.0.9
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 +61 -38
- package/bin/ft.js +1 -1
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +439 -39
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/audit.js.map +1 -1
- package/bin/src/commands/authCommand.js.map +1 -1
- 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.map +1 -1
- package/bin/src/commands/dashboard.js.map +1 -1
- package/bin/src/commands/history.js.map +1 -1
- 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.map +1 -1
- package/bin/src/commands/logout.js.map +1 -1
- package/bin/src/commands/owners.js.map +1 -1
- package/bin/src/commands/quota.js.map +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.map +1 -1
- 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.map +1 -1
- package/bin/src/commands/undo.js.map +1 -1
- 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.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 +369 -33
- 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 +57 -6
- 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.map +1 -1
- package/bin/src/engine/web.d.ts.map +1 -1
- package/bin/src/engine/web.js.map +1 -1
- package/bin/src/index.js +4 -4
- 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.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.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.map +1 -1
- package/bin/src/utils/updateCheck.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import readline from 'readline';
|
|
3
4
|
import { spinner, text, isCancel, select, confirm } from '@clack/prompts';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { simpleGit } from 'simple-git';
|
|
6
|
-
import { generateMultiCodeFix, identifyTargetFiles, analyzeCommandError, generateCodeScanReport, generateFeaturePlan, generateCodeFile, askRepoQuestion, } from '../engine/llm.js';
|
|
7
|
+
import { generateMultiCodeFix, identifyTargetFiles, analyzeCommandError, generateCodeScanReport, generateFeaturePlan, generateCodeFile, askRepoQuestion, getActiveModelInfo, } from '../engine/llm.js';
|
|
7
8
|
import { loadContext, recordHistoryAction, getDirectoryFiles } from '../utils/config.js';
|
|
8
9
|
import { renderMarkdown, safeNote } from '../utils/markdown.js';
|
|
9
10
|
import { normalizeLanguage } from '../utils/language.js';
|
|
10
11
|
import { createPullRequest } from '../engine/git.js';
|
|
12
|
+
import { consumeCredit, checkTierAccess } from '../utils/auth.js';
|
|
11
13
|
import { handleUndo } from './undo.js';
|
|
12
|
-
import { AgentExecutionLoop } from '../engine/agentEngine.js';
|
|
13
|
-
import { verifyAndSelfHealFiles, executeSmartCommand } from '../engine/verifier.js';
|
|
14
|
+
import { AgentExecutionLoop, buildDirectoryTree } from '../engine/agentEngine.js';
|
|
15
|
+
import { verifyAndSelfHealFiles, executeSmartCommand, runProjectVerification } from '../engine/verifier.js';
|
|
14
16
|
import { exec } from 'child_process';
|
|
15
17
|
import { promisify } from 'util';
|
|
16
18
|
const execAsync = promisify(exec);
|
|
@@ -226,7 +228,78 @@ export function findTargetFilesFromPrompt(userPrompt, specifiedFile, filesList)
|
|
|
226
228
|
}
|
|
227
229
|
return [];
|
|
228
230
|
}
|
|
229
|
-
|
|
231
|
+
export async function getAgentGoalInput(placeholderStr) {
|
|
232
|
+
if (!process.stdin.isTTY) {
|
|
233
|
+
return await text({
|
|
234
|
+
message: chalk.bold('Agent Prompt:'),
|
|
235
|
+
placeholder: placeholderStr,
|
|
236
|
+
validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or /exit to quit' : undefined),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
let buffer = '';
|
|
241
|
+
let isDone = false;
|
|
242
|
+
const rl = readline.createInterface({
|
|
243
|
+
input: process.stdin,
|
|
244
|
+
output: process.stdout,
|
|
245
|
+
terminal: true,
|
|
246
|
+
});
|
|
247
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
248
|
+
if (process.stdin.setRawMode) {
|
|
249
|
+
process.stdin.setRawMode(true);
|
|
250
|
+
}
|
|
251
|
+
process.stdout.write(`\n${chalk.cyan('◇')} ${chalk.bold('Agent Prompt:')}\n${chalk.cyan('│')} `);
|
|
252
|
+
const cleanup = () => {
|
|
253
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
254
|
+
if (process.stdin.setRawMode) {
|
|
255
|
+
process.stdin.setRawMode(false);
|
|
256
|
+
}
|
|
257
|
+
rl.close();
|
|
258
|
+
};
|
|
259
|
+
const onKeypress = (str, key) => {
|
|
260
|
+
if (isDone)
|
|
261
|
+
return;
|
|
262
|
+
if (str === '\u0003' || (key && key.ctrl && key.name === 'c')) {
|
|
263
|
+
isDone = true;
|
|
264
|
+
cleanup();
|
|
265
|
+
process.stdout.write('\n');
|
|
266
|
+
resolve(Symbol.for('clack:cancel'));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Immediately catch '/' keypress as the very first typed character without requiring Enter!
|
|
270
|
+
if (buffer.length === 0 && str === '/') {
|
|
271
|
+
isDone = true;
|
|
272
|
+
cleanup();
|
|
273
|
+
readline.cursorTo(process.stdout, 0);
|
|
274
|
+
readline.clearLine(process.stdout, 0);
|
|
275
|
+
process.stdout.write(`${chalk.cyan('│')} /\n`);
|
|
276
|
+
resolve('/');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (key && (key.name === 'return' || key.name === 'enter')) {
|
|
280
|
+
isDone = true;
|
|
281
|
+
cleanup();
|
|
282
|
+
process.stdout.write('\n');
|
|
283
|
+
resolve(buffer.trim());
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (key && key.name === 'backspace') {
|
|
287
|
+
if (buffer.length > 0) {
|
|
288
|
+
buffer = buffer.slice(0, -1);
|
|
289
|
+
readline.cursorTo(process.stdout, 0);
|
|
290
|
+
readline.clearLine(process.stdout, 0);
|
|
291
|
+
process.stdout.write(`${chalk.cyan('│')} ${buffer}`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (str && !key.ctrl && !key.meta && key.name !== 'escape') {
|
|
296
|
+
buffer += str;
|
|
297
|
+
process.stdout.write(str);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
process.stdin.on('keypress', onKeypress);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
230
303
|
export async function handleAgent(goalInput, options) {
|
|
231
304
|
const tierCheck = checkTierAccess('agent');
|
|
232
305
|
if (!tierCheck.allowed) {
|
|
@@ -267,17 +340,26 @@ export async function handleAgent(goalInput, options) {
|
|
|
267
340
|
language: targetLang,
|
|
268
341
|
projectName,
|
|
269
342
|
});
|
|
343
|
+
const modelInfo = getActiveModelInfo();
|
|
344
|
+
const fallbackPipeline = modelInfo.fallbackChain.slice(1).join(' ➔ ');
|
|
270
345
|
const welcomeBanner = `
|
|
271
346
|
${chalk.bold.cyan('Scout Autonomous AI Agent')} ${targetLang ? `(${targetLang})` : ''}
|
|
347
|
+
${chalk.bold('Active Model:')} ${chalk.bold.green(modelInfo.currentActiveModel)}
|
|
348
|
+
${chalk.bold('Fallback Pipeline:')} ${chalk.dim(fallbackPipeline)}
|
|
349
|
+
|
|
272
350
|
${chalk.dim('Type your goal, or use slash commands:')}
|
|
273
|
-
${chalk.cyan('/help')}
|
|
274
|
-
${chalk.cyan('/
|
|
275
|
-
${chalk.cyan('/
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
351
|
+
${chalk.cyan('/help')} - Show agent tools & commands (14 tools)
|
|
352
|
+
${chalk.cyan('/tree')} - View workspace directory tree
|
|
353
|
+
${chalk.cyan('/stats')} - View real-time session & model telemetry
|
|
354
|
+
${chalk.cyan('/test')} - Run automated project test suite
|
|
355
|
+
${chalk.cyan('/diff')} - Show git diff of uncommitted changes
|
|
356
|
+
${chalk.cyan('/export')} - Save session report to .ft/
|
|
357
|
+
${chalk.cyan('/undo')} - Revert last file modifications
|
|
358
|
+
${chalk.cyan('/clear')} - Reset conversation context
|
|
359
|
+
${chalk.cyan('/scan')} - Run vulnerability scan
|
|
360
|
+
${chalk.cyan('/plan')} - Generate implementation roadmap
|
|
361
|
+
${chalk.cyan('/exit')} - Exit session
|
|
362
|
+
`.trim();
|
|
281
363
|
safeNote(welcomeBanner, 'Scout Agent Active Session');
|
|
282
364
|
while (true) {
|
|
283
365
|
let rawGoal = undefined;
|
|
@@ -288,43 +370,258 @@ ${chalk.cyan('/clear')} - Reset conversation context.
|
|
|
288
370
|
else {
|
|
289
371
|
isFirstRun = false;
|
|
290
372
|
options = {};
|
|
291
|
-
const userGoalInput = await
|
|
292
|
-
|
|
293
|
-
placeholder: 'Ask question, fix bug, run command, scan issues, create file... (/help /exit)',
|
|
294
|
-
validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or /exit to quit' : undefined),
|
|
295
|
-
});
|
|
296
|
-
if (isCancel(userGoalInput)) {
|
|
373
|
+
const userGoalInput = await getAgentGoalInput('Ask question, fix bug, run command, scan issues, create file... (/help /exit)');
|
|
374
|
+
if (isCancel(userGoalInput) || typeof userGoalInput !== 'string') {
|
|
297
375
|
safeNote(chalk.yellow('Exiting Scout Agent session. see you soon!'), 'Scout Agent Ended');
|
|
298
376
|
break;
|
|
299
377
|
}
|
|
300
|
-
|
|
378
|
+
let trimmedInput = userGoalInput.trim();
|
|
379
|
+
const KNOWN_SLASH_COMMANDS = [
|
|
380
|
+
'/help', '/tree', '/status', '/stats', '/test', '/explain',
|
|
381
|
+
'/export', '/save-session', '/diff', '/clear', '/compact',
|
|
382
|
+
'/undo', '/history', '/scan', '/plan', '/exit', '/quit'
|
|
383
|
+
];
|
|
384
|
+
if (trimmedInput.startsWith('/')) {
|
|
385
|
+
const cmdName = trimmedInput.split(/\s+/)[0].toLowerCase();
|
|
386
|
+
if (!KNOWN_SLASH_COMMANDS.includes(cmdName)) {
|
|
387
|
+
const filterStr = trimmedInput.toLowerCase();
|
|
388
|
+
let availableOptions = [
|
|
389
|
+
{ value: '/help', label: '/help', hint: 'Show agent tools & commands guide' },
|
|
390
|
+
{ value: '/tree', label: '/tree', hint: 'Display workspace directory tree' },
|
|
391
|
+
{ value: '/stats', label: '/stats', hint: 'Show session telemetry & model statistics' },
|
|
392
|
+
{ value: '/test', label: '/test', hint: 'Run project verification test suite' },
|
|
393
|
+
{ value: '/diff', label: '/diff', hint: 'Display git diff of uncommitted changes' },
|
|
394
|
+
{ value: '/scan', label: '/scan', hint: 'Run read-only vulnerability scan' },
|
|
395
|
+
{ value: '/plan', label: '/plan', hint: 'Generate feature roadmap & plan' },
|
|
396
|
+
{ value: '/explain', label: '/explain', hint: 'Architectural breakdown of a file' },
|
|
397
|
+
{ value: '/export', label: '/export', hint: 'Save session report to .ft/' },
|
|
398
|
+
{ value: '/undo', label: '/undo', hint: 'Revert last file modifications' },
|
|
399
|
+
{ value: '/history', label: '/history', hint: 'View recent action history logs' },
|
|
400
|
+
{ value: '/clear', label: '/clear', hint: 'Reset conversation context' },
|
|
401
|
+
{ value: '/exit', label: '/exit', hint: 'Exit Scout Agent session' },
|
|
402
|
+
];
|
|
403
|
+
if (filterStr !== '/') {
|
|
404
|
+
const filtered = availableOptions.filter(o => o.value.startsWith(filterStr));
|
|
405
|
+
if (filtered.length > 0) {
|
|
406
|
+
availableOptions = filtered;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const pickedCmd = await select({
|
|
410
|
+
message: chalk.bold('Select a Slash Command:'),
|
|
411
|
+
options: availableOptions,
|
|
412
|
+
});
|
|
413
|
+
if (isCancel(pickedCmd)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
let chosen = pickedCmd;
|
|
417
|
+
if (chosen === '/explain') {
|
|
418
|
+
const filePath = await text({
|
|
419
|
+
message: chalk.bold('Enter file path to explain (e.g. bin/src/index.ts):'),
|
|
420
|
+
placeholder: 'bin/src/index.ts',
|
|
421
|
+
validate: (val) => (!val || !val.trim() ? 'File path is required' : undefined),
|
|
422
|
+
});
|
|
423
|
+
if (isCancel(filePath))
|
|
424
|
+
continue;
|
|
425
|
+
chosen = `/explain ${filePath.trim()}`;
|
|
426
|
+
}
|
|
427
|
+
else if (chosen === '/plan') {
|
|
428
|
+
const idea = await text({
|
|
429
|
+
message: chalk.bold('Enter feature idea or goal for the plan:'),
|
|
430
|
+
placeholder: 'Add authentication middleware...',
|
|
431
|
+
validate: (val) => (!val || !val.trim() ? 'Goal description is required' : undefined),
|
|
432
|
+
});
|
|
433
|
+
if (isCancel(idea))
|
|
434
|
+
continue;
|
|
435
|
+
chosen = `/plan ${idea.trim()}`;
|
|
436
|
+
}
|
|
437
|
+
else if (chosen === '/tree') {
|
|
438
|
+
const dirInput = await text({
|
|
439
|
+
message: chalk.bold('Enter directory path & optional depth (default "." 4):'),
|
|
440
|
+
placeholder: '. 4',
|
|
441
|
+
});
|
|
442
|
+
if (!isCancel(dirInput) && typeof dirInput === 'string' && dirInput.trim()) {
|
|
443
|
+
chosen = `/tree ${dirInput.trim()}`;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else if (chosen === '/test') {
|
|
447
|
+
const testCmd = await text({
|
|
448
|
+
message: chalk.bold('Enter custom test command (press Enter for default):'),
|
|
449
|
+
placeholder: 'npm test',
|
|
450
|
+
});
|
|
451
|
+
if (!isCancel(testCmd) && typeof testCmd === 'string' && testCmd.trim()) {
|
|
452
|
+
chosen = `/test ${testCmd.trim()}`;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
trimmedInput = chosen;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
301
458
|
if (['exit', 'quit', 'q', ':q', '/exit', '/quit'].includes(trimmedInput.toLowerCase())) {
|
|
302
459
|
safeNote(chalk.yellow('Exiting Scout Agent session. see you soon!'), 'Scout Agent Ended');
|
|
303
460
|
break;
|
|
304
461
|
}
|
|
305
462
|
if (trimmedInput.toLowerCase() === '/help') {
|
|
306
463
|
const helpText = `
|
|
307
|
-
###
|
|
308
|
-
- **read_file**: Read content & line ranges of
|
|
309
|
-
- **write_file**: Create new files or overwrite
|
|
310
|
-
- **edit_file**: Search
|
|
311
|
-
- **
|
|
312
|
-
- **
|
|
313
|
-
- **
|
|
464
|
+
### 🛠️ Scout Autonomous Agent Tools (14 Available)
|
|
465
|
+
- **read_file**: Read content & line ranges of workspace files.
|
|
466
|
+
- **write_file**: Create new files or overwrite code directly.
|
|
467
|
+
- **edit_file**: Search & replace exact target code snippets.
|
|
468
|
+
- **multi_edit_file**: Apply batch snippet replacements in a single step.
|
|
469
|
+
- **run_command**: Execute live terminal commands (\`npm test\`, \`build\`).
|
|
470
|
+
- **tree_view**: Generate visual ASCII directory tree structures.
|
|
471
|
+
- **file_info**: Get file statistics, size, lines, & modification time.
|
|
472
|
+
- **grep_search**: String/regex pattern search across codebase.
|
|
473
|
+
- **glob_search**: Filter workspace files by file pattern.
|
|
474
|
+
- **fetch_url**: Fetch web API documentation or URL content.
|
|
475
|
+
- **git_diff**: Retrieve uncommitted workspace git diff.
|
|
476
|
+
- **ask_user**: Request missing secret credentials or API keys.
|
|
477
|
+
- **task_completed**: Conclude autonomous goal execution with teardown.
|
|
314
478
|
|
|
315
|
-
### Slash Commands
|
|
316
|
-
- \`/help\`:
|
|
317
|
-
- \`/
|
|
318
|
-
- \`/
|
|
319
|
-
- \`/
|
|
320
|
-
- \`/
|
|
321
|
-
- \`/
|
|
322
|
-
- \`/
|
|
323
|
-
- \`/
|
|
479
|
+
### 🚀 Interactive Slash Commands
|
|
480
|
+
- \`/help\`: Show this tools & commands guide.
|
|
481
|
+
- \`/tree [dir]\`: Display workspace directory tree structure.
|
|
482
|
+
- \`/status\` or \`/stats\`: Show session telemetry & statistics.
|
|
483
|
+
- \`/test [cmd]\`: Run project verification test suite.
|
|
484
|
+
- \`/explain <file>\`: Get deep architectural breakdown of a file.
|
|
485
|
+
- \`/export\` or \`/save-session\`: Save session report to \`.ft/session-<timestamp>.md\`.
|
|
486
|
+
- \`/diff\`: Display colorized git diff of uncommitted changes.
|
|
487
|
+
- \`/clear\` or \`/compact\`: Reset agent conversation context.
|
|
488
|
+
- \`/undo\`: Revert last file modifications.
|
|
489
|
+
- \`/history\`: View recent Scout action logs.
|
|
490
|
+
- \`/exit\`: End Scout Agent session.
|
|
324
491
|
`.trim();
|
|
325
492
|
safeNote(renderMarkdown(helpText), ' Scout Slash Commands & Tools');
|
|
326
493
|
continue;
|
|
327
494
|
}
|
|
495
|
+
if (trimmedInput.toLowerCase().startsWith('/tree')) {
|
|
496
|
+
const parts = trimmedInput.split(/\s+/).slice(1);
|
|
497
|
+
let targetDir = '.';
|
|
498
|
+
let maxDepth = 4;
|
|
499
|
+
if (parts.length === 1) {
|
|
500
|
+
if (/^\d+$/.test(parts[0])) {
|
|
501
|
+
maxDepth = parseInt(parts[0], 10);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
targetDir = parts[0];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else if (parts.length >= 2) {
|
|
508
|
+
if (/^\d+$/.test(parts[0])) {
|
|
509
|
+
maxDepth = parseInt(parts[0], 10);
|
|
510
|
+
targetDir = parts[1];
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
targetDir = parts[0];
|
|
514
|
+
if (/^\d+$/.test(parts[1])) {
|
|
515
|
+
maxDepth = parseInt(parts[1], 10);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const fullDir = path.resolve(process.cwd(), targetDir);
|
|
520
|
+
if (!fs.existsSync(fullDir) || !fs.statSync(fullDir).isDirectory()) {
|
|
521
|
+
safeNote(chalk.yellow(`Directory not found: "${targetDir}"`), '️ Directory Tree Error');
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const treeStr = buildDirectoryTree(fullDir, '', 0, maxDepth);
|
|
525
|
+
safeNote(`Directory Tree for "${targetDir}" (depth ${maxDepth}):\n${targetDir}/\n${treeStr}`, ' Workspace Directory Tree');
|
|
526
|
+
}
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (['/status', '/stats'].includes(trimmedInput.toLowerCase())) {
|
|
530
|
+
const stats = agentLoop.getStats();
|
|
531
|
+
const modifiedFiles = agentLoop.getModifiedFiles();
|
|
532
|
+
const backups = agentLoop.getBackups();
|
|
533
|
+
const git = simpleGit();
|
|
534
|
+
let currentBranch = 'main';
|
|
535
|
+
try {
|
|
536
|
+
const branchSummary = await git.branch();
|
|
537
|
+
currentBranch = branchSummary.current || 'main';
|
|
538
|
+
}
|
|
539
|
+
catch { }
|
|
540
|
+
const elapsedSec = Math.round(stats.elapsedMs / 1000);
|
|
541
|
+
const activeModel = getActiveModelInfo();
|
|
542
|
+
const fallbackStatus = activeModel.lastFallbackInfo
|
|
543
|
+
? `Switched from \`${activeModel.lastFallbackInfo.previousModel}\` ➔ \`${activeModel.lastFallbackInfo.fallbackModel}\` (${activeModel.lastFallbackInfo.reason})`
|
|
544
|
+
: 'None (Primary model actively responding)';
|
|
545
|
+
const statsMd = `
|
|
546
|
+
### 📊 Scout Session & AI Model Telemetry
|
|
547
|
+
- **Active AI Model**: \`${activeModel.currentActiveModel}\`
|
|
548
|
+
- **Primary AI Model**: \`${activeModel.primaryModel}\`
|
|
549
|
+
- **Fallback Chain**: \`${activeModel.fallbackChain.join(' ➔ ')}\`
|
|
550
|
+
- **Last Fallback Event**: ${fallbackStatus}
|
|
551
|
+
- **Active Branch**: \`${currentBranch}\`
|
|
552
|
+
- **Elapsed Session Time**: \`${elapsedSec}s\`
|
|
553
|
+
- **Steps Executed**: \`${stats.totalSteps}\`
|
|
554
|
+
- **Total Tool Calls**: \`${stats.totalToolCalls}\`
|
|
555
|
+
- **Modified Files**: \`${modifiedFiles.length}\` (${modifiedFiles.map(f => `\`${f}\``).join(', ') || 'None'})
|
|
556
|
+
- **Stored File Backups**: \`${backups.length}\`
|
|
557
|
+
`.trim();
|
|
558
|
+
safeNote(renderMarkdown(statsMd), ' Scout Session & Model Telemetry');
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (trimmedInput.toLowerCase().startsWith('/test')) {
|
|
562
|
+
const parts = trimmedInput.split(/\s+/);
|
|
563
|
+
const customCmd = parts.slice(1).join(' ') || options?.verifyCmd;
|
|
564
|
+
const testSpin = spinner();
|
|
565
|
+
testSpin.start(`Running project verification test suite ${customCmd ? `(\`${customCmd}\`)` : ''}...`);
|
|
566
|
+
const testRes = await runProjectVerification(process.cwd(), customCmd);
|
|
567
|
+
if (testRes.valid) {
|
|
568
|
+
testSpin.stop(chalk.green('Project verification tests passed cleanly!'));
|
|
569
|
+
safeNote(`${chalk.green(' Verification Passed:')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Suite Clean');
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
testSpin.stop(chalk.red('Project verification test suite failed.'));
|
|
573
|
+
safeNote(`${chalk.red('️ Verification Failed:')}\n${testRes.errors.join('\n')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Failure Warning');
|
|
574
|
+
}
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (trimmedInput.toLowerCase().startsWith('/explain')) {
|
|
578
|
+
const parts = trimmedInput.split(/\s+/);
|
|
579
|
+
const targetPath = parts[1];
|
|
580
|
+
if (!targetPath) {
|
|
581
|
+
safeNote(chalk.yellow('Usage: /explain <filePath> (e.g. /explain bin/src/index.ts)'), ' Explain Command');
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const fullP = path.resolve(process.cwd(), targetPath);
|
|
585
|
+
if (!fs.existsSync(fullP)) {
|
|
586
|
+
safeNote(chalk.yellow(`File not found: ${targetPath}`), ' Explain Error');
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const explainSpin = spinner();
|
|
590
|
+
explainSpin.start(`Analyzing architecture and structure of ${targetPath}...`);
|
|
591
|
+
const content = fs.readFileSync(fullP, 'utf-8');
|
|
592
|
+
const explanation = await askRepoQuestion(`Provide a deep architectural breakdown, primary purpose, component breakdown, and code explanation for file "${targetPath}":\n\`\`\`\n${content.slice(0, 6000)}\n\`\`\``, projectName, filesList, '', [], { language: targetLang });
|
|
593
|
+
explainSpin.stop('Analysis complete.');
|
|
594
|
+
safeNote(renderMarkdown(explanation), ` Architectural Breakdown: ${targetPath}`);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (['/export', '/save-session'].includes(trimmedInput.toLowerCase())) {
|
|
598
|
+
const ftDir = path.join(process.cwd(), '.ft');
|
|
599
|
+
if (!fs.existsSync(ftDir))
|
|
600
|
+
fs.mkdirSync(ftDir, { recursive: true });
|
|
601
|
+
const timeStamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
602
|
+
const sessionPath = path.join(ftDir, `session-${timeStamp}.md`);
|
|
603
|
+
const stats = agentLoop.getStats();
|
|
604
|
+
const modifiedFiles = agentLoop.getModifiedFiles();
|
|
605
|
+
const history = (await import('../utils/config.js')).loadHistory();
|
|
606
|
+
const exportContent = `# Scout Agent Session Report
|
|
607
|
+
Date: ${new Date().toLocaleString()}
|
|
608
|
+
Project: ${projectName}
|
|
609
|
+
|
|
610
|
+
## Session Statistics
|
|
611
|
+
- Total Steps: ${stats.totalSteps}
|
|
612
|
+
- Tool Calls: ${stats.totalToolCalls}
|
|
613
|
+
- Modified Files Count: ${modifiedFiles.length}
|
|
614
|
+
|
|
615
|
+
## Modified Files
|
|
616
|
+
${modifiedFiles.map(f => `- ${f}`).join('\n') || 'None'}
|
|
617
|
+
|
|
618
|
+
## Action History
|
|
619
|
+
${history.slice(0, 15).map(h => `- [${h.timestamp}] ${h.command}: ${h.description}`).join('\n')}
|
|
620
|
+
`;
|
|
621
|
+
fs.writeFileSync(sessionPath, exportContent, 'utf-8');
|
|
622
|
+
safeNote(chalk.green(`Session report saved to: ${chalk.bold.cyan(path.relative(process.cwd(), sessionPath))}`), ' Session Exported');
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
328
625
|
if (trimmedInput.toLowerCase() === '/diff') {
|
|
329
626
|
try {
|
|
330
627
|
const { execSync } = await import('child_process');
|
|
@@ -619,14 +916,74 @@ ${filePreviews}
|
|
|
619
916
|
safeNote(chalk.yellow('No active goal specified.'), ' Scout Agent');
|
|
620
917
|
continue;
|
|
621
918
|
}
|
|
919
|
+
const git = simpleGit();
|
|
920
|
+
const isRepo = await git.checkIsRepo();
|
|
921
|
+
let currentBranch = 'main';
|
|
922
|
+
if (isRepo) {
|
|
923
|
+
try {
|
|
924
|
+
const branchSummary = await git.branch();
|
|
925
|
+
currentBranch = branchSummary.current || 'main';
|
|
926
|
+
}
|
|
927
|
+
catch { }
|
|
928
|
+
}
|
|
929
|
+
// Fast Multi-File Fix path via generateMultiCodeFix if --issue is set
|
|
930
|
+
if (options?.issue) {
|
|
931
|
+
const idSpinner = spinner();
|
|
932
|
+
idSpinner.start('Scout AI: Identifying affected target files...');
|
|
933
|
+
const targetFiles = await identifyTargetFiles(finalGoal, projectName, filesList, targetLang, options?.native);
|
|
934
|
+
idSpinner.stop(targetFiles.length > 0 ? `Identified ${targetFiles.length} target file(s): ${targetFiles.join(', ')}` : 'Target identification complete.');
|
|
935
|
+
if (targetFiles.length > 0) {
|
|
936
|
+
const targetMap = {};
|
|
937
|
+
for (const tf of targetFiles) {
|
|
938
|
+
const absP = path.resolve(cwd, tf);
|
|
939
|
+
if (fs.existsSync(absP)) {
|
|
940
|
+
targetMap[tf] = fs.readFileSync(absP, 'utf-8');
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (Object.keys(targetMap).length > 0) {
|
|
944
|
+
const fixSpinner = spinner();
|
|
945
|
+
fixSpinner.start(`Scout AI: Generating multi-file fixes for ${Object.keys(targetMap).join(', ')}...`);
|
|
946
|
+
const multiFixRes = await generateMultiCodeFix(finalGoal, projectName, filesList, targetMap, targetLang, options?.native, (done, total, curFile) => {
|
|
947
|
+
fixSpinner.message(`Generating fix for ${curFile} (${done}/${total})...`);
|
|
948
|
+
});
|
|
949
|
+
fixSpinner.stop('Multi-file fixes generated.');
|
|
950
|
+
const backups = [];
|
|
951
|
+
const affectedFiles = [];
|
|
952
|
+
for (const item of multiFixRes.files) {
|
|
953
|
+
const absPath = path.resolve(cwd, item.targetFile);
|
|
954
|
+
if (fs.existsSync(absPath)) {
|
|
955
|
+
backups.push({
|
|
956
|
+
filePath: item.targetFile,
|
|
957
|
+
originalContent: fs.readFileSync(absPath, 'utf-8'),
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
961
|
+
fs.writeFileSync(absPath, item.fixedCode, 'utf-8');
|
|
962
|
+
affectedFiles.push(item.targetFile);
|
|
963
|
+
}
|
|
964
|
+
recordHistoryAction({
|
|
965
|
+
command: `scout agent --issue ${options.issue}`,
|
|
966
|
+
description: `Auto-fixed issue on ${affectedFiles.length} file(s)`,
|
|
967
|
+
previousBranch: currentBranch,
|
|
968
|
+
affectedFiles,
|
|
969
|
+
backups,
|
|
970
|
+
});
|
|
971
|
+
safeNote(`### Fixed Issue\n${multiFixRes.problem}\n\n### Fix Applied\n${multiFixRes.howFixed}\n\n### Modified Files\n${affectedFiles.map(f => ` • \`${f}\``).join('\n')}`, ' Scout Multi-File Auto-Fix Complete');
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// Standard Agent Goal Execution: Pre-scan & identify target files
|
|
977
|
+
const idSpinner = spinner();
|
|
978
|
+
idSpinner.start('Scout AI: Analyzing codebase file tree & resolving target files...');
|
|
979
|
+
const targetFiles = await identifyTargetFiles(finalGoal, projectName, filesList, targetLang, options?.native);
|
|
980
|
+
idSpinner.stop(targetFiles.length > 0 ? `Target files resolved: ${targetFiles.map(f => `\`${f}\``).join(', ')}` : 'Analysis complete.');
|
|
622
981
|
safeNote(`${chalk.bold.cyan(' Scout Autonomous Execution Initialized')}\n` +
|
|
623
|
-
`${chalk.dim('Goal:')} ${finalGoal}
|
|
982
|
+
`${chalk.dim('Goal:')} ${finalGoal}` +
|
|
983
|
+
(targetFiles.length > 0 ? `\n${chalk.dim('Target Files:')} ${targetFiles.map(f => `\`${f}\``).join(', ')}` : ''), ' Scout Agent Execution');
|
|
624
984
|
const execResult = await agentLoop.executeGoal(finalGoal);
|
|
625
985
|
const modifiedFiles = agentLoop.getModifiedFiles();
|
|
626
986
|
const backups = agentLoop.getBackups();
|
|
627
|
-
const git = simpleGit();
|
|
628
|
-
const isRepo = await git.checkIsRepo();
|
|
629
|
-
let currentBranch = 'main';
|
|
630
987
|
if (isRepo) {
|
|
631
988
|
try {
|
|
632
989
|
const branchSummary = await git.branch();
|
|
@@ -665,7 +1022,9 @@ ${execResult.summary}
|
|
|
665
1022
|
message: chalk.bold('Review Scout Agent result. What would you like to do?'),
|
|
666
1023
|
options: [
|
|
667
1024
|
{ value: 'keep', label: ' Keep Changes', hint: `Retain agent changes on current branch (${currentBranch})` },
|
|
1025
|
+
{ value: 'test', label: ' Run Verification Tests', hint: 'Execute test suite to confirm zero regressions' },
|
|
668
1026
|
{ value: 'pr', label: ' Create Pull Request', hint: 'Push fix to a new branch & open a Pull Request' },
|
|
1027
|
+
{ value: 'export', label: ' Export Session Report', hint: 'Save detailed session teardown report to .ft/' },
|
|
669
1028
|
{ value: 'revert', label: '️ Revert Changes', hint: 'Restore original code immediately' },
|
|
670
1029
|
],
|
|
671
1030
|
});
|
|
@@ -676,6 +1035,47 @@ ${execResult.summary}
|
|
|
676
1035
|
revSpinner.stop('Changes reverted successfully.');
|
|
677
1036
|
continue;
|
|
678
1037
|
}
|
|
1038
|
+
if (actionChoice === 'test') {
|
|
1039
|
+
const testSpin = spinner();
|
|
1040
|
+
testSpin.start('Running project verification test suite...');
|
|
1041
|
+
const testRes = await runProjectVerification(process.cwd(), options?.verifyCmd);
|
|
1042
|
+
if (testRes.valid) {
|
|
1043
|
+
testSpin.stop(chalk.green('Project verification tests passed cleanly!'));
|
|
1044
|
+
safeNote(`${chalk.green(' Verification Passed:')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Suite Clean');
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
testSpin.stop(chalk.red('Project verification test suite failed.'));
|
|
1048
|
+
safeNote(`${chalk.red('️ Verification Failed:')}\n${testRes.errors.join('\n')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Failure Warning');
|
|
1049
|
+
}
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (actionChoice === 'export') {
|
|
1053
|
+
const ftDir = path.join(process.cwd(), '.ft');
|
|
1054
|
+
if (!fs.existsSync(ftDir))
|
|
1055
|
+
fs.mkdirSync(ftDir, { recursive: true });
|
|
1056
|
+
const timeStamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1057
|
+
const sessionPath = path.join(ftDir, `session-${timeStamp}.md`);
|
|
1058
|
+
const stats = agentLoop.getStats();
|
|
1059
|
+
const exportContent = `# Scout Agent Session Report
|
|
1060
|
+
Date: ${new Date().toLocaleString()}
|
|
1061
|
+
Project: ${projectName}
|
|
1062
|
+
Goal: ${finalGoal}
|
|
1063
|
+
|
|
1064
|
+
## Session Statistics
|
|
1065
|
+
- Total Steps: ${stats.totalSteps}
|
|
1066
|
+
- Tool Calls: ${stats.totalToolCalls}
|
|
1067
|
+
- Modified Files Count: ${modifiedFiles.length}
|
|
1068
|
+
|
|
1069
|
+
## Modified Files
|
|
1070
|
+
${modifiedFiles.map(f => `- ${f}`).join('\n') || 'None'}
|
|
1071
|
+
|
|
1072
|
+
## Execution Summary
|
|
1073
|
+
${execResult.summary}
|
|
1074
|
+
`;
|
|
1075
|
+
fs.writeFileSync(sessionPath, exportContent, 'utf-8');
|
|
1076
|
+
safeNote(chalk.green(`Session report saved to: ${chalk.bold.cyan(path.relative(process.cwd(), sessionPath))}`), ' Session Exported');
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
679
1079
|
if (actionChoice === 'pr') {
|
|
680
1080
|
const prSpinner = spinner();
|
|
681
1081
|
prSpinner.start('Creating Pull Request...');
|