ft-scout 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/.firebase/hosting.d2Vi.cache +9 -0
  2. package/.firebaserc +14 -0
  3. package/AUTONOMOUS_FEATURES.md +192 -0
  4. package/README.md +482 -32
  5. package/bin/ft.js +2 -0
  6. package/bin/src/commands/agent.d.ts.map +1 -0
  7. package/bin/src/commands/agent.js +1230 -0
  8. package/bin/src/commands/agent.js.map +1 -0
  9. package/bin/src/commands/audit.d.ts.map +1 -0
  10. package/bin/src/commands/audit.js +104 -0
  11. package/bin/src/commands/audit.js.map +1 -0
  12. package/bin/src/commands/authCommand.d.ts.map +1 -0
  13. package/bin/src/commands/authCommand.js +38 -0
  14. package/bin/src/commands/authCommand.js.map +1 -0
  15. package/bin/src/commands/benchmark.d.ts.map +1 -0
  16. package/bin/src/commands/benchmark.js +135 -0
  17. package/bin/src/commands/benchmark.js.map +1 -0
  18. package/bin/src/commands/brief.d.ts.map +1 -1
  19. package/bin/src/commands/brief.js +28 -19
  20. package/bin/src/commands/brief.js.map +1 -1
  21. package/bin/src/commands/configCommand.d.ts.map +1 -0
  22. package/bin/src/commands/configCommand.js +53 -0
  23. package/bin/src/commands/configCommand.js.map +1 -0
  24. package/bin/src/commands/dashboard.d.ts.map +1 -0
  25. package/bin/src/commands/dashboard.js +41 -0
  26. package/bin/src/commands/dashboard.js.map +1 -0
  27. package/bin/src/commands/history.d.ts.map +1 -0
  28. package/bin/src/commands/history.js +20 -0
  29. package/bin/src/commands/history.js.map +1 -0
  30. package/bin/src/commands/init.d.ts.map +1 -1
  31. package/bin/src/commands/init.js +411 -78
  32. package/bin/src/commands/init.js.map +1 -1
  33. package/bin/src/commands/login.d.ts.map +1 -0
  34. package/bin/src/commands/login.js +190 -0
  35. package/bin/src/commands/login.js.map +1 -0
  36. package/bin/src/commands/logout.d.ts.map +1 -0
  37. package/bin/src/commands/logout.js +15 -0
  38. package/bin/src/commands/logout.js.map +1 -0
  39. package/bin/src/commands/mcp.d.ts.map +1 -0
  40. package/bin/src/commands/mcp.js +393 -0
  41. package/bin/src/commands/mcp.js.map +1 -0
  42. package/bin/src/commands/owners.js +6 -6
  43. package/bin/src/commands/owners.js.map +1 -1
  44. package/bin/src/commands/plan.d.ts.map +1 -0
  45. package/bin/src/commands/plan.js +152 -0
  46. package/bin/src/commands/plan.js.map +1 -0
  47. package/bin/src/commands/quota.d.ts.map +1 -0
  48. package/bin/src/commands/quota.js +32 -0
  49. package/bin/src/commands/quota.js.map +1 -0
  50. package/bin/src/commands/recommend.d.ts.map +1 -0
  51. package/bin/src/commands/recommend.js +46 -0
  52. package/bin/src/commands/recommend.js.map +1 -0
  53. package/bin/src/commands/risky.d.ts.map +1 -1
  54. package/bin/src/commands/risky.js +235 -39
  55. package/bin/src/commands/risky.js.map +1 -1
  56. package/bin/src/commands/setup.d.ts.map +1 -1
  57. package/bin/src/commands/setup.js +411 -16
  58. package/bin/src/commands/setup.js.map +1 -1
  59. package/bin/src/commands/signup.d.ts.map +1 -0
  60. package/bin/src/commands/signup.js +118 -0
  61. package/bin/src/commands/signup.js.map +1 -0
  62. package/bin/src/commands/undo.d.ts.map +1 -0
  63. package/bin/src/commands/undo.js +59 -0
  64. package/bin/src/commands/undo.js.map +1 -0
  65. package/bin/src/commands/web.d.ts.map +1 -0
  66. package/bin/src/commands/web.js +90 -0
  67. package/bin/src/commands/web.js.map +1 -0
  68. package/bin/src/discovery/config.d.ts.map +1 -0
  69. package/bin/src/discovery/config.js +41 -0
  70. package/bin/src/discovery/config.js.map +1 -0
  71. package/bin/src/discovery/display.d.ts.map +1 -0
  72. package/bin/src/discovery/display.js +57 -0
  73. package/bin/src/discovery/display.js.map +1 -0
  74. package/bin/src/discovery/messages.d.ts.map +1 -0
  75. package/bin/src/discovery/messages.js +7 -0
  76. package/bin/src/discovery/messages.js.map +1 -0
  77. package/bin/src/discovery/signals.d.ts.map +1 -0
  78. package/bin/src/discovery/signals.js +33 -0
  79. package/bin/src/discovery/signals.js.map +1 -0
  80. package/bin/src/engine/actionVerifier.d.ts.map +1 -0
  81. package/bin/src/engine/actionVerifier.js +283 -0
  82. package/bin/src/engine/actionVerifier.js.map +1 -0
  83. package/bin/src/engine/agentEngine.d.ts.map +1 -0
  84. package/bin/src/engine/agentEngine.js +2188 -0
  85. package/bin/src/engine/agentEngine.js.map +1 -0
  86. package/bin/src/engine/agentState.d.ts.map +1 -0
  87. package/bin/src/engine/agentState.js +381 -0
  88. package/bin/src/engine/agentState.js.map +1 -0
  89. package/bin/src/engine/browser/browserController.d.ts.map +1 -0
  90. package/bin/src/engine/browser/browserController.js +737 -0
  91. package/bin/src/engine/browser/browserController.js.map +1 -0
  92. package/bin/src/engine/browser/index.d.ts.map +1 -0
  93. package/bin/src/engine/browser/index.js +2 -0
  94. package/bin/src/engine/browser/index.js.map +1 -0
  95. package/bin/src/engine/computer/computerController.d.ts.map +1 -0
  96. package/bin/src/engine/computer/computerController.js +773 -0
  97. package/bin/src/engine/computer/computerController.js.map +1 -0
  98. package/bin/src/engine/computer/computerDiagnostic.d.ts.map +1 -0
  99. package/bin/src/engine/computer/computerDiagnostic.js +71 -0
  100. package/bin/src/engine/computer/computerDiagnostic.js.map +1 -0
  101. package/bin/src/engine/computer/index.d.ts.map +1 -0
  102. package/bin/src/engine/computer/index.js +4 -0
  103. package/bin/src/engine/computer/index.js.map +1 -0
  104. package/bin/src/engine/computer/mockComputer.d.ts.map +1 -0
  105. package/bin/src/engine/computer/mockComputer.js +260 -0
  106. package/bin/src/engine/computer/mockComputer.js.map +1 -0
  107. package/bin/src/engine/computer/scoutDriver.cs +1161 -0
  108. package/bin/src/engine/computer/scout_driver.exe +0 -0
  109. package/bin/src/engine/git.d.ts.map +1 -1
  110. package/bin/src/engine/git.js +71 -0
  111. package/bin/src/engine/git.js.map +1 -1
  112. package/bin/src/engine/llm.d.ts.map +1 -1
  113. package/bin/src/engine/llm.js +1272 -23
  114. package/bin/src/engine/llm.js.map +1 -1
  115. package/bin/src/engine/planner.d.ts.map +1 -0
  116. package/bin/src/engine/planner.js +321 -0
  117. package/bin/src/engine/planner.js.map +1 -0
  118. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -0
  119. package/bin/src/engine/runtime/agentRuntime.js +1258 -0
  120. package/bin/src/engine/runtime/agentRuntime.js.map +1 -0
  121. package/bin/src/engine/runtime/index.d.ts.map +1 -0
  122. package/bin/src/engine/runtime/index.js +2 -0
  123. package/bin/src/engine/runtime/index.js.map +1 -0
  124. package/bin/src/engine/safety.d.ts.map +1 -0
  125. package/bin/src/engine/safety.js +316 -0
  126. package/bin/src/engine/safety.js.map +1 -0
  127. package/bin/src/engine/scoutAudioRecorder.cs +215 -0
  128. package/bin/src/engine/scout_audio_recorder.exe +0 -0
  129. package/bin/src/engine/toolRegistry.d.ts.map +1 -0
  130. package/bin/src/engine/toolRegistry.js +521 -0
  131. package/bin/src/engine/toolRegistry.js.map +1 -0
  132. package/bin/src/engine/verifier.d.ts.map +1 -0
  133. package/bin/src/engine/verifier.js +414 -0
  134. package/bin/src/engine/verifier.js.map +1 -0
  135. package/bin/src/engine/voiceEngine.d.ts.map +1 -0
  136. package/bin/src/engine/voiceEngine.js +474 -0
  137. package/bin/src/engine/voiceEngine.js.map +1 -0
  138. package/bin/src/engine/web.d.ts.map +1 -0
  139. package/bin/src/engine/web.js +161 -0
  140. package/bin/src/engine/web.js.map +1 -0
  141. package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -0
  142. package/bin/src/engine/workspace/agentWorkspace.js +505 -0
  143. package/bin/src/engine/workspace/agentWorkspace.js.map +1 -0
  144. package/bin/src/engine/workspace/index.d.ts.map +1 -0
  145. package/bin/src/engine/workspace/index.js +2 -0
  146. package/bin/src/engine/workspace/index.js.map +1 -0
  147. package/bin/src/index.js +365 -23
  148. package/bin/src/index.js.map +1 -1
  149. package/bin/src/mcp/client.d.ts.map +1 -0
  150. package/bin/src/mcp/client.js +127 -0
  151. package/bin/src/mcp/client.js.map +1 -0
  152. package/bin/src/mcp/discovery.d.ts.map +1 -0
  153. package/bin/src/mcp/discovery.js +91 -0
  154. package/bin/src/mcp/discovery.js.map +1 -0
  155. package/bin/src/mcp/health.d.ts.map +1 -0
  156. package/bin/src/mcp/health.js +44 -0
  157. package/bin/src/mcp/health.js.map +1 -0
  158. package/bin/src/mcp/index.d.ts.map +1 -0
  159. package/bin/src/mcp/index.js +15 -0
  160. package/bin/src/mcp/index.js.map +1 -0
  161. package/bin/src/mcp/manager.d.ts.map +1 -0
  162. package/bin/src/mcp/manager.js +648 -0
  163. package/bin/src/mcp/manager.js.map +1 -0
  164. package/bin/src/mcp/permissions.d.ts.map +1 -0
  165. package/bin/src/mcp/permissions.js +50 -0
  166. package/bin/src/mcp/permissions.js.map +1 -0
  167. package/bin/src/mcp/persistence.d.ts.map +1 -0
  168. package/bin/src/mcp/persistence.js +171 -0
  169. package/bin/src/mcp/persistence.js.map +1 -0
  170. package/bin/src/mcp/recovery.d.ts.map +1 -0
  171. package/bin/src/mcp/recovery.js +75 -0
  172. package/bin/src/mcp/recovery.js.map +1 -0
  173. package/bin/src/mcp/telemetry.d.ts.map +1 -0
  174. package/bin/src/mcp/telemetry.js +82 -0
  175. package/bin/src/mcp/telemetry.js.map +1 -0
  176. package/bin/src/mcp/toolRouter.d.ts.map +1 -0
  177. package/bin/src/mcp/toolRouter.js +127 -0
  178. package/bin/src/mcp/toolRouter.js.map +1 -0
  179. package/bin/src/mcp/transport.d.ts.map +1 -0
  180. package/bin/src/mcp/transport.js +328 -0
  181. package/bin/src/mcp/transport.js.map +1 -0
  182. package/bin/src/mcp/types.d.ts.map +1 -0
  183. package/bin/src/mcp/types.js +6 -0
  184. package/bin/src/mcp/types.js.map +1 -0
  185. package/bin/src/server/server.d.ts.map +1 -0
  186. package/bin/src/server/server.js +989 -0
  187. package/bin/src/server/server.js.map +1 -0
  188. package/bin/src/utils/admin.d.ts.map +1 -0
  189. package/bin/src/utils/admin.js +231 -0
  190. package/bin/src/utils/admin.js.map +1 -0
  191. package/bin/src/utils/auditLogger.d.ts.map +1 -0
  192. package/bin/src/utils/auditLogger.js +70 -0
  193. package/bin/src/utils/auditLogger.js.map +1 -0
  194. package/bin/src/utils/auth.d.ts.map +1 -0
  195. package/bin/src/utils/auth.js +679 -0
  196. package/bin/src/utils/auth.js.map +1 -0
  197. package/bin/src/utils/branding.d.ts.map +1 -0
  198. package/bin/src/utils/branding.js +14 -0
  199. package/bin/src/utils/branding.js.map +1 -0
  200. package/bin/src/utils/config.d.ts.map +1 -1
  201. package/bin/src/utils/config.js +124 -3
  202. package/bin/src/utils/config.js.map +1 -1
  203. package/bin/src/utils/email.d.ts.map +1 -0
  204. package/bin/src/utils/email.js +267 -0
  205. package/bin/src/utils/email.js.map +1 -0
  206. package/bin/src/utils/firebaseAuth.d.ts.map +1 -0
  207. package/bin/src/utils/firebaseAuth.js +174 -0
  208. package/bin/src/utils/firebaseAuth.js.map +1 -0
  209. package/bin/src/utils/holoseneAuth.d.ts.map +1 -0
  210. package/bin/src/utils/holoseneAuth.js +307 -0
  211. package/bin/src/utils/holoseneAuth.js.map +1 -0
  212. package/bin/src/utils/language.d.ts.map +1 -0
  213. package/bin/src/utils/language.js +181 -0
  214. package/bin/src/utils/language.js.map +1 -0
  215. package/bin/src/utils/markdown.d.ts.map +1 -0
  216. package/bin/src/utils/markdown.js +215 -0
  217. package/bin/src/utils/markdown.js.map +1 -0
  218. package/bin/src/utils/payment.d.ts.map +1 -0
  219. package/bin/src/utils/payment.js +290 -0
  220. package/bin/src/utils/payment.js.map +1 -0
  221. package/bin/src/utils/pricing.d.ts.map +1 -0
  222. package/bin/src/utils/pricing.js +165 -0
  223. package/bin/src/utils/pricing.js.map +1 -0
  224. package/bin/src/utils/razorpay.d.ts.map +1 -0
  225. package/bin/src/utils/razorpay.js +144 -0
  226. package/bin/src/utils/razorpay.js.map +1 -0
  227. package/bin/src/utils/session.d.ts.map +1 -0
  228. package/bin/src/utils/session.js +82 -0
  229. package/bin/src/utils/session.js.map +1 -0
  230. package/bin/src/utils/updateCheck.d.ts.map +1 -0
  231. package/bin/src/utils/updateCheck.js +108 -0
  232. package/bin/src/utils/updateCheck.js.map +1 -0
  233. package/downloads/invoice-2026-03.pdf +1 -0
  234. package/firebase.json +74 -0
  235. package/package.json +49 -46
  236. package/web/ScoutFavicon.png +0 -0
  237. package/web/app.js +3944 -0
  238. package/web/styles.css +3081 -0
  239. package/.ft/context.json +0 -18
  240. package/email-template.html +0 -223
  241. package/email-template.txt +0 -45
  242. package/scripts/send-brevo-campaign.cjs +0 -167
  243. package/scripts/send-email.js +0 -77
@@ -0,0 +1,1230 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spinner, text, isCancel, select, confirm } from '@clack/prompts';
4
+ import chalk from 'chalk';
5
+ import { simpleGit } from 'simple-git';
6
+ import { generateMultiCodeFix, identifyTargetFiles, analyzeCommandError, generateCodeScanReport, generateFeaturePlan, generateCodeFile, askRepoQuestion, getActiveModelInfo, } from '../engine/llm.js';
7
+ import { loadContext, recordHistoryAction, saveAgentSession, getDirectoryFiles } from '../utils/config.js';
8
+ import { renderMarkdown, safeNote } from '../utils/markdown.js';
9
+ import { normalizeLanguage } from '../utils/language.js';
10
+ import { createPullRequest } from '../engine/git.js';
11
+ import { consumeCredit, checkTierAccess, loadAuthConfig, isProOrHigher } from '../utils/auth.js';
12
+ import { handleUndo } from './undo.js';
13
+ import { AgentExecutionLoop, buildDirectoryTree, AGENT_TOOLS } from '../engine/agentEngine.js';
14
+ import { verifyAndSelfHealFiles, executeSmartCommand, runProjectVerification } from '../engine/verifier.js';
15
+ import { speakText, listenSpeechToText, promptInteractionMode, cleanTextForSpeech } from '../engine/voiceEngine.js';
16
+ import { exec } from 'child_process';
17
+ import { promisify } from 'util';
18
+ const execAsync = promisify(exec);
19
+ const FT_DIR = path.join(process.cwd(), '.ft', 'scout');
20
+ const SCAN_CACHE = path.join(FT_DIR, 'scan-report.json');
21
+ function getConversationalAcknowledgment(goal) {
22
+ const lower = goal.toLowerCase().trim();
23
+ if (/^(hi|hello|hey|sup|namaste)/i.test(lower)) {
24
+ return "Hey there! How can I help you today?";
25
+ }
26
+ if (lower.startsWith('open ') || lower.startsWith('launch ')) {
27
+ const app = goal.slice(5).trim();
28
+ return `On it! Opening ${app} for you.`;
29
+ }
30
+ if (lower.startsWith('close ') || lower.startsWith('kill ')) {
31
+ const app = goal.slice(6).trim();
32
+ return `Sure, closing ${app}.`;
33
+ }
34
+ if (lower.startsWith('create ') || lower.startsWith('make ')) {
35
+ return "Understood! Getting started on creating that.";
36
+ }
37
+ if (lower.startsWith('fix ') || lower.startsWith('debug ')) {
38
+ return "Let's track that down. Inspecting the issue now.";
39
+ }
40
+ if (lower.startsWith('run ') || lower.startsWith('test ')) {
41
+ return "Running that right away.";
42
+ }
43
+ const cleanGoal = goal.replace(/["'`]/g, '');
44
+ const shortGoal = cleanGoal.length > 30 ? cleanGoal.slice(0, 30) + '...' : cleanGoal;
45
+ return `Got it! Working on ${shortGoal}.`;
46
+ }
47
+ function formatConversationalClosing(summary) {
48
+ let text = summary.trim();
49
+ text = text.replace(/^(?:execution|task|action)\s*(?:completed|finished|summary)?\s*:\s*/i, '');
50
+ text = text.replace(/^mock execution completed successfully for goal:\s*"[^"]*"\.?\s*/i, '');
51
+ text = cleanTextForSpeech(text);
52
+ if (!text.endsWith('.') && !text.endsWith('!') && !text.endsWith('?')) {
53
+ text += '.';
54
+ }
55
+ return text;
56
+ }
57
+ export function saveScanReport(issues) {
58
+ if (!fs.existsSync(FT_DIR)) {
59
+ fs.mkdirSync(FT_DIR, { recursive: true });
60
+ }
61
+ fs.writeFileSync(SCAN_CACHE, JSON.stringify(issues, null, 2));
62
+ }
63
+ export function loadScanReport() {
64
+ if (!fs.existsSync(SCAN_CACHE))
65
+ return [];
66
+ try {
67
+ return JSON.parse(fs.readFileSync(SCAN_CACHE, 'utf-8'));
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ export function extractCreateTargetFiles(rawGoal) {
74
+ if (!rawGoal || !rawGoal.trim())
75
+ return [];
76
+ const goalLower = rawGoal.toLowerCase().trim();
77
+ const isCreateIntent = /^(?:create|generate|make|build|add|write|edit|modify|update|implement|populate|fill)\b/i.test(goalLower) ||
78
+ /\b(?:create|generate|make|build|add|new|edit|modify|update|implement)\s+(?:a\s+)?(?:new\s+)?(?:file|files|code|script)\b/i.test(goalLower) ||
79
+ /\b(?:in|into|to|called|named|file|files|edit|modify|update)\s+([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)\b/i.test(rawGoal);
80
+ if (!isCreateIntent)
81
+ return [];
82
+ const validExts = [
83
+ 'ts', 'js', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'go', 'rs', 'php',
84
+ 'rb', 'html', 'css', 'json', 'md', 'sql', 'txt', 'sh', 'yaml', 'yml',
85
+ 'xml', 'jsx', 'tsx', 'vue', 'svelte'
86
+ ];
87
+ const foundFiles = [];
88
+ const directMatches = rawGoal.matchAll(/(?:create|generate|make|build|add|write|edit|modify|update|implement|populate|fill|file|called|named)\s+(?:a\s+)?(?:new\s+)?(?:file\s+)?(?:in\s+)?(?:the\s+)?(?:root\s+)?(?:repo\s+|repository\s+|dir\s+|directory\s+)?(?:called\s+|named\s+)?([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)\b/gi);
89
+ for (const match of directMatches) {
90
+ if (match && match[1]) {
91
+ const file = match[1].replace(/^\.\//, '');
92
+ if (file.toLowerCase() !== 'package.json' && !foundFiles.includes(file)) {
93
+ const ext = path.extname(file).replace('.', '').toLowerCase();
94
+ if (validExts.includes(ext)) {
95
+ foundFiles.push(file);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ const tokens = rawGoal.split(/[\s,;]+/).map((w) => w.replace(/["'()]/g, '').trim());
101
+ for (const token of tokens) {
102
+ if (token.includes('.') &&
103
+ !token.startsWith('.') &&
104
+ !token.startsWith('*') &&
105
+ /^[a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+$/.test(token)) {
106
+ const ext = path.extname(token).replace('.', '').toLowerCase();
107
+ if (validExts.includes(ext) && token.toLowerCase() !== 'package.json') {
108
+ const cleanPath = token.replace(/^\.\//, '');
109
+ if (!foundFiles.includes(cleanPath)) {
110
+ foundFiles.push(cleanPath);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return foundFiles;
116
+ }
117
+ export function extractCreateTargetFile(rawGoal) {
118
+ const files = extractCreateTargetFiles(rawGoal);
119
+ return files.length > 0 ? files[0] : undefined;
120
+ }
121
+ export function parseUserPromptIntent(rawGoal, filesList) {
122
+ if (!rawGoal || !rawGoal.trim()) {
123
+ return { cleanGoal: '', intentType: 'question' };
124
+ }
125
+ let cleaned = rawGoal.trim();
126
+ const prefixRegex = /^(?:bro|hey|hi|hello|please|scout|ft|agent|yo|can\s+you|could\s+you|would\s+you|i\s+want\s+to|i\s+wanna|let's|lets|in\s+scout\s+agent\s+mode|so)\b\s*/gi;
127
+ let prev = '';
128
+ while (cleaned !== prev) {
129
+ prev = cleaned;
130
+ cleaned = cleaned.replace(prefixRegex, '').trim();
131
+ }
132
+ if (!cleaned)
133
+ cleaned = rawGoal.trim();
134
+ const lowerCleaned = cleaned.toLowerCase();
135
+ let targetFiles = extractCreateTargetFiles(cleaned);
136
+ if (targetFiles.length === 0) {
137
+ targetFiles = extractCreateTargetFiles(rawGoal);
138
+ }
139
+ let targetFile = targetFiles.length > 0 ? targetFiles[0] : undefined;
140
+ if (!targetFile) {
141
+ const tokens = cleaned.split(/\s+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
142
+ const validExts = [
143
+ 'ts', 'js', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'go', 'rs', 'php',
144
+ 'rb', 'html', 'css', 'json', 'md', 'sql', 'txt', 'sh', 'yaml', 'yml',
145
+ 'xml', 'jsx', 'tsx', 'vue', 'svelte'
146
+ ];
147
+ for (const token of tokens) {
148
+ if (token.includes('.') && !token.startsWith('.') && !token.startsWith('*') && /^[a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+$/.test(token)) {
149
+ const ext = path.extname(token).replace('.', '').toLowerCase();
150
+ if (validExts.includes(ext) && token.toLowerCase() !== 'package.json') {
151
+ targetFile = token.replace(/^\.\//, '');
152
+ targetFiles = [targetFile];
153
+ break;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ if (!targetFile) {
159
+ const matchedFiles = findTargetFilesFromPrompt(cleaned, undefined, filesList);
160
+ if (matchedFiles.length > 0) {
161
+ targetFile = matchedFiles[0];
162
+ targetFiles = matchedFiles;
163
+ }
164
+ }
165
+ const isScan = /\b(?:scan|bug scan|scan codebase|check for bugs|find bugs|security scan)\b/i.test(lowerCleaned);
166
+ if (isScan)
167
+ return { cleanGoal: cleaned, intentType: 'scan', targetFile, targetFiles };
168
+ const isRun = /^(?:run|exec|execute|terminal)\b/i.test(lowerCleaned);
169
+ if (isRun)
170
+ return { cleanGoal: cleaned, intentType: 'run', targetFile, targetFiles };
171
+ const isPlan = /^(?:plan|roadmap|feasibility|design|architect)\b/i.test(lowerCleaned) || /^add\s+feature\b/i.test(lowerCleaned);
172
+ if (isPlan)
173
+ return { cleanGoal: cleaned, intentType: 'plan', targetFile, targetFiles };
174
+ const creationKeywordsRegex = /\b(?:create|generate|make|build|add\s+file|new\s+file|write\s+file|edit\s+file|populate|write\s+code|implement\s+code|write\s+snippet|add\s+snippet|all\s+necessary\s+files)\b/i;
175
+ const actionVerbsRegex = /\b(?:create|generate|make|build|write|add|edit|modify|update|fix|refactor|change|delete|remove|patch|implement|populate|fill|code|put|setup)\b/i;
176
+ const hasActionVerb = actionVerbsRegex.test(lowerCleaned);
177
+ const isCreationIntent = creationKeywordsRegex.test(lowerCleaned) || Boolean(targetFiles.length > 0 && /\b(?:create|generate|make|build|write|add|edit|modify|update|populate|fill|implement|snippet|game|code)\b/i.test(lowerCleaned));
178
+ const pureQuestionRegex = /^(?:what|why|where|who|explain|read|display|tell|describe|list|purpose\s+of|how\s+does|how\s+is|show\s+me\s+code)\b/i;
179
+ const isPureQuestion = pureQuestionRegex.test(lowerCleaned) && !hasActionVerb;
180
+ if (isPureQuestion) {
181
+ return { cleanGoal: cleaned, intentType: 'question', targetFile, targetFiles };
182
+ }
183
+ if (isCreationIntent) {
184
+ return { cleanGoal: cleaned, intentType: 'create', targetFile, targetFiles };
185
+ }
186
+ if (hasActionVerb || targetFile) {
187
+ return { cleanGoal: cleaned, intentType: 'modify', targetFile, targetFiles };
188
+ }
189
+ return { cleanGoal: cleaned, intentType: 'question', targetFile, targetFiles };
190
+ }
191
+ export function findTargetFilesFromPrompt(userPrompt, specifiedFile, filesList) {
192
+ const norm = (p) => p.replace(/\\/g, '/').toLowerCase().trim();
193
+ if (specifiedFile && specifiedFile.trim()) {
194
+ const raw = specifiedFile.trim();
195
+ const splitItems = raw.split(',').map((s) => s.trim()).filter(Boolean);
196
+ const matchedFromFlag = [];
197
+ for (const item of splitItems) {
198
+ const normItem = norm(item);
199
+ const isExplicitExtPattern = item.startsWith('*') ||
200
+ item.startsWith('.') ||
201
+ (item.length <= 10 && !item.includes('/') && !item.includes('\\') && !filesList.some((f) => norm(f) === normItem || path.basename(norm(f)) === normItem));
202
+ if (isExplicitExtPattern) {
203
+ const cleanExt = normItem.replace(/^(\*|\.)+/, '');
204
+ if (cleanExt) {
205
+ const extFiles = filesList.filter((f) => norm(f).endsWith(`.${cleanExt}`));
206
+ matchedFromFlag.push(...extFiles);
207
+ }
208
+ continue;
209
+ }
210
+ const exact = filesList.find((f) => norm(f) === normItem);
211
+ if (exact) {
212
+ matchedFromFlag.push(exact);
213
+ continue;
214
+ }
215
+ const suffixMatches = filesList.filter((f) => norm(f).endsWith('/' + normItem));
216
+ if (suffixMatches.length > 0) {
217
+ matchedFromFlag.push(...suffixMatches);
218
+ continue;
219
+ }
220
+ const baseMatches = filesList.filter((f) => path.basename(norm(f)) === path.basename(normItem));
221
+ if (baseMatches.length > 0) {
222
+ matchedFromFlag.push(...baseMatches);
223
+ continue;
224
+ }
225
+ matchedFromFlag.push(item);
226
+ }
227
+ if (matchedFromFlag.length > 0) {
228
+ return Array.from(new Set(matchedFromFlag));
229
+ }
230
+ }
231
+ const rawWords = userPrompt.split(/\s+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
232
+ const cleanPromptWords = rawWords.map((w) => w.toLowerCase());
233
+ const lowerPrompt = userPrompt.toLowerCase();
234
+ const matchedByName = [];
235
+ for (const word of rawWords) {
236
+ if (word && word.includes('.')) {
237
+ const fullP = path.resolve(process.cwd(), word);
238
+ if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
239
+ const relP = path.relative(process.cwd(), fullP).replace(/\\/g, '/');
240
+ matchedByName.push(relP);
241
+ }
242
+ }
243
+ }
244
+ for (const f of filesList) {
245
+ const normF = norm(f);
246
+ const base = path.basename(normF);
247
+ if (lowerPrompt.includes(normF) ||
248
+ cleanPromptWords.includes(normF) ||
249
+ (base.includes('.') && (lowerPrompt.includes(base) || cleanPromptWords.includes(base)))) {
250
+ matchedByName.push(f);
251
+ }
252
+ }
253
+ if (matchedByName.length > 0) {
254
+ return Array.from(new Set(matchedByName));
255
+ }
256
+ const bulkExtMatch = lowerPrompt.match(/(?:all|\*|in|from|for|across|every)?\s*(?:\*|\.)?([a-zA-Z0-9]{1,10})\s+files?\b|\*\.([a-zA-Z0-9]{1,10})/i);
257
+ if (bulkExtMatch) {
258
+ const ext = (bulkExtMatch[1] || bulkExtMatch[2])?.toLowerCase();
259
+ if (ext) {
260
+ const extFiles = filesList.filter((f) => norm(f).endsWith(`.${ext}`));
261
+ if (extFiles.length > 0) {
262
+ return Array.from(new Set(extFiles));
263
+ }
264
+ }
265
+ }
266
+ return [];
267
+ }
268
+ export async function getAgentGoalInput(placeholderStr) {
269
+ return await text({
270
+ message: chalk.bold('Agent Prompt:'),
271
+ placeholder: placeholderStr,
272
+ validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or /exit to quit' : undefined),
273
+ });
274
+ }
275
+ export async function handleAgent(goalInput, options) {
276
+ const tierCheck = checkTierAccess('agent');
277
+ if (!tierCheck.allowed) {
278
+ console.log(`\n${chalk.bold.red('Credit Quota Exhausted!')}`);
279
+ console.log(chalk.yellow(tierCheck.reason || 'Credit limit reached for Free Tier.'));
280
+ if (tierCheck.renewalFormatted) {
281
+ console.log(`Renewal: ${chalk.bold.cyan(tierCheck.renewalFormatted)}`);
282
+ }
283
+ console.log(chalk.cyan('\nTo continue using Scout CLI AI Agent:'));
284
+ console.log(` 1. Upgrade to ${chalk.bold.green('Pro Tier ($19/mo)')} in dashboard (${chalk.underline('scout dashboard')})`);
285
+ console.log(` 2. Or enable ${chalk.bold.magenta('BYOK Mode')} in dashboard to use your custom API key / proxy endpoint.\n`);
286
+ return;
287
+ }
288
+ const creditRes = consumeCredit(1);
289
+ if (!creditRes.success) {
290
+ console.log(`\n${chalk.bold.red('Credit Quota Exhausted!')}`);
291
+ console.log(chalk.yellow(creditRes.reason));
292
+ if (creditRes.renewalFormatted) {
293
+ console.log(`Renewal: ${chalk.bold.cyan(creditRes.renewalFormatted)}`);
294
+ }
295
+ console.log(chalk.cyan('\nRun `scout dashboard` to upgrade your plan or activate BYOK mode.\n'));
296
+ return;
297
+ }
298
+ if (creditRes.isByok) {
299
+ console.log(`${chalk.bold.magenta('BYOK Mode Active:')} Custom API Key / Proxy Endpoint used (${chalk.dim('Scout credits bypassed')})`);
300
+ }
301
+ else {
302
+ console.log(`${chalk.dim(`Scout Credit Consumed | ${creditRes.remaining} credits remaining`)}`);
303
+ if (creditRes.isLowCredit) {
304
+ console.log(`\n${chalk.bold.yellow('Low Credit Warning:')} Only ${chalk.bold(creditRes.remaining)} credits remaining on your current plan.`);
305
+ console.log(` ${chalk.cyan('Run `scout dashboard` to upgrade your tier or activate BYOK mode.')}\n`);
306
+ }
307
+ }
308
+ const existingContext = loadContext();
309
+ const targetLang = normalizeLanguage(options?.lang) || normalizeLanguage(existingContext?.language);
310
+ const cwd = process.cwd();
311
+ const filesList = getDirectoryFiles(cwd);
312
+ const projectName = existingContext?.projectName || path.basename(cwd) || 'Codebase';
313
+ let initialGoal = goalInput || options?.goal;
314
+ const isOneShot = Boolean(initialGoal || options?.scan || options?.add || options?.plan || options?.run || options?.create || options?.issue);
315
+ let isFirstRun = true;
316
+ let isAudioMode = options?.voice;
317
+ if (isAudioMode === undefined && !initialGoal && process.stdout.isTTY) {
318
+ const selectedMode = await promptInteractionMode();
319
+ isAudioMode = selectedMode === 'audio';
320
+ }
321
+ const authCfg = loadAuthConfig();
322
+ const userTier = (authCfg.currentTier || 'free').toLowerCase();
323
+ const isPro = isProOrHigher(userTier);
324
+ const agentLoop = new AgentExecutionLoop({
325
+ autoApprove: options?.yes,
326
+ maxSteps: options?.steps || options?.maxSteps || 30,
327
+ language: targetLang,
328
+ tier: userTier,
329
+ isAudioMode: Boolean(isAudioMode),
330
+ });
331
+ if (options?.resume) {
332
+ const loaded = agentLoop.getStateManager().loadTaskState(options.resume);
333
+ if (loaded) {
334
+ initialGoal = loaded.objective;
335
+ safeNote(`${chalk.bold.green('Resuming Autonomous Task')} ${chalk.cyan(options.resume)}\n` +
336
+ `Goal: ${loaded.objective}\n` +
337
+ `Status: ${loaded.status} (Step ${loaded.current_step})\n` +
338
+ `Completed Steps: ${loaded.plan.filter((s) => s.status === 'completed').length}/${loaded.plan.length}`, 'Scout Autonomous Task Resumed');
339
+ }
340
+ else {
341
+ safeNote(chalk.yellow(`Could not find saved task state for ID: "${options.resume}" in .ft/tasks/`), 'Scout Agent Resume');
342
+ }
343
+ }
344
+ const modelInfo = getActiveModelInfo();
345
+ const fallbackPipeline = modelInfo.fallbackChain.slice(1).join(' ➔ ');
346
+ const tierBadge = isPro
347
+ ? chalk.bold.magenta(`[Plan: ${userTier.toUpperCase()} - Autonomous & Scratchpad Enabled]`)
348
+ : chalk.bold.yellow(`[Plan: FREE - Basic Dev Ops (Edit/Create/Run)]`);
349
+ const welcomeBanner = `
350
+ ${chalk.bold.cyan('Scout Autonomous AI Agent')} ${targetLang ? `(${targetLang})` : ''} [${isAudioMode ? '🎤 Audio Mode' : '📝 Text Mode'}] ${tierBadge}
351
+ ${chalk.bold('Active Model:')} ${chalk.bold.green(modelInfo.currentActiveModel)}
352
+ ${chalk.bold('Capabilities:')} ${isPro ? chalk.green('Autonomous Computer/Browser Control + AI Scratchpad Working Memory') : chalk.dim('Basic Dev Ops (Code editing, creating files/folders, running commands - Max 5 steps)')}
353
+ ${chalk.bold('Fallback Pipeline:')} ${chalk.dim(fallbackPipeline)}
354
+
355
+ ${chalk.dim('Type your goal, or use slash commands:')}
356
+ ${chalk.cyan('/help')} - Show agent tools & commands (${AGENT_TOOLS.length} tools)
357
+ ${chalk.cyan('/tree')} - View workspace directory tree
358
+ ${chalk.cyan('/stats')} - View real-time session & model telemetry
359
+ ${chalk.cyan('/test')} - Run automated project test suite
360
+ ${chalk.cyan('/diff')} - Show git diff of uncommitted changes
361
+ ${chalk.cyan('/export')} - Save session report to .ft/
362
+ ${chalk.cyan('/undo')} - Revert last file modifications
363
+ ${chalk.cyan('/clear')} - Reset conversation context
364
+ ${chalk.cyan('/scan')} - Run vulnerability scan
365
+ ${chalk.cyan('/plan')} - Generate implementation roadmap
366
+ ${chalk.cyan('/exit')} - Exit session
367
+ `.trim();
368
+ safeNote(welcomeBanner, 'Scout Agent Active Session');
369
+ while (true) {
370
+ let rawGoal = undefined;
371
+ if (isFirstRun && (initialGoal || options?.scan || options?.add || options?.plan || options?.run || options?.create || options?.issue)) {
372
+ rawGoal = initialGoal;
373
+ isFirstRun = false;
374
+ }
375
+ else if (isAudioMode) {
376
+ isFirstRun = false;
377
+ options = {};
378
+ safeNote(chalk.cyan(' Listening to your microphone for audio interaction... Speak your goal clearly!'), ' Scout Audio Interaction');
379
+ const transcript = await listenSpeechToText();
380
+ if (transcript && transcript.trim()) {
381
+ rawGoal = transcript.trim();
382
+ safeNote(` Voice Command Received: "${chalk.bold.green(rawGoal)}"`, ' Scout STT Input');
383
+ const ack = getConversationalAcknowledgment(rawGoal);
384
+ speakText(ack, { async: true });
385
+ }
386
+ else {
387
+ safeNote(chalk.yellow('No speech transcript detected or listener timed out.'), ' Scout Audio Interaction');
388
+ continue;
389
+ }
390
+ }
391
+ else {
392
+ isFirstRun = false;
393
+ options = {};
394
+ const userGoalInput = await getAgentGoalInput('Ask question, fix bug, run command, scan issues, create file... (/help /exit)');
395
+ if (isCancel(userGoalInput) || typeof userGoalInput !== 'string') {
396
+ safeNote(chalk.yellow('Exiting Scout Agent session. see you soon!'), 'Scout Agent Ended');
397
+ break;
398
+ }
399
+ let trimmedInput = userGoalInput.trim();
400
+ const KNOWN_SLASH_COMMANDS = [
401
+ '/help', '/tree', '/status', '/stats', '/test', '/explain',
402
+ '/export', '/save-session', '/diff', '/clear', '/compact',
403
+ '/undo', '/history', '/scan', '/plan', '/tasks', '/resume', '/voice', '/speak',
404
+ '/exit', '/quit'
405
+ ];
406
+ if (trimmedInput.startsWith('/')) {
407
+ const cmdName = trimmedInput.split(/\s+/)[0].toLowerCase();
408
+ if (!KNOWN_SLASH_COMMANDS.includes(cmdName)) {
409
+ const filterStr = trimmedInput.toLowerCase();
410
+ let availableOptions = [
411
+ { value: '/help', label: '/help', hint: 'Show agent tools & commands guide' },
412
+ { value: '/tasks', label: '/tasks', hint: 'List saved autonomous tasks in .ft/tasks/' },
413
+ { value: '/resume', label: '/resume', hint: 'Resume saved autonomous task by task ID' },
414
+ { value: '/voice', label: '/voice', hint: 'Activate microphone STT speech input' },
415
+ { value: '/speak', label: '/speak', hint: 'Speak text out loud via TTS' },
416
+ { value: '/tree', label: '/tree', hint: 'Display workspace directory tree' },
417
+ { value: '/stats', label: '/stats', hint: 'Show session telemetry & model statistics' },
418
+ { value: '/test', label: '/test', hint: 'Run project verification test suite' },
419
+ { value: '/diff', label: '/diff', hint: 'Display git diff of uncommitted changes' },
420
+ { value: '/scan', label: '/scan', hint: 'Run read-only vulnerability scan' },
421
+ { value: '/plan', label: '/plan', hint: 'Generate feature roadmap & plan' },
422
+ { value: '/explain', label: '/explain', hint: 'Architectural breakdown of a file' },
423
+ { value: '/export', label: '/export', hint: 'Save session report to .ft/' },
424
+ { value: '/undo', label: '/undo', hint: 'Revert last file modifications' },
425
+ { value: '/history', label: '/history', hint: 'View recent action history logs' },
426
+ { value: '/clear', label: '/clear', hint: 'Reset conversation context' },
427
+ { value: '/exit', label: '/exit', hint: 'Exit Scout Agent session' },
428
+ ];
429
+ if (filterStr !== '/') {
430
+ const filtered = availableOptions.filter(o => o.value.startsWith(filterStr));
431
+ if (filtered.length > 0) {
432
+ availableOptions = filtered;
433
+ }
434
+ }
435
+ const pickedCmd = await select({
436
+ message: chalk.bold('Select a Slash Command:'),
437
+ options: availableOptions,
438
+ });
439
+ if (isCancel(pickedCmd)) {
440
+ continue;
441
+ }
442
+ let chosen = pickedCmd;
443
+ if (chosen === '/explain') {
444
+ const filePath = await text({
445
+ message: chalk.bold('Enter file path to explain (e.g. bin/src/index.ts):'),
446
+ placeholder: 'bin/src/index.ts',
447
+ validate: (val) => (!val || !val.trim() ? 'File path is required' : undefined),
448
+ });
449
+ if (isCancel(filePath))
450
+ continue;
451
+ chosen = `/explain ${filePath.trim()}`;
452
+ }
453
+ else if (chosen === '/plan') {
454
+ const idea = await text({
455
+ message: chalk.bold('Enter feature idea or goal for the plan:'),
456
+ placeholder: 'Add authentication middleware...',
457
+ validate: (val) => (!val || !val.trim() ? 'Goal description is required' : undefined),
458
+ });
459
+ if (isCancel(idea))
460
+ continue;
461
+ chosen = `/plan ${idea.trim()}`;
462
+ }
463
+ else if (chosen === '/tree') {
464
+ const dirInput = await text({
465
+ message: chalk.bold('Enter directory path & optional depth (default "." 4):'),
466
+ placeholder: '. 4',
467
+ });
468
+ if (!isCancel(dirInput) && typeof dirInput === 'string' && dirInput.trim()) {
469
+ chosen = `/tree ${dirInput.trim()}`;
470
+ }
471
+ }
472
+ else if (chosen === '/test') {
473
+ const testCmd = await text({
474
+ message: chalk.bold('Enter custom test command (press Enter for default):'),
475
+ placeholder: 'npm test',
476
+ });
477
+ if (!isCancel(testCmd) && typeof testCmd === 'string' && testCmd.trim()) {
478
+ chosen = `/test ${testCmd.trim()}`;
479
+ }
480
+ }
481
+ trimmedInput = chosen;
482
+ }
483
+ }
484
+ if (['exit', 'quit', 'q', ':q', '/exit', '/quit'].includes(trimmedInput.toLowerCase())) {
485
+ safeNote(chalk.yellow('Exiting Scout Agent session. see you soon!'), 'Scout Agent Ended');
486
+ break;
487
+ }
488
+ if (trimmedInput.toLowerCase() === '/help') {
489
+ const helpText = `
490
+ ### 🛠️ Basic Developer Tools (Available on All Plans)
491
+ - **read_file**: Read content & line ranges of workspace files.
492
+ - **write_file**: Create new files and directories.
493
+ - **edit_file**: Search & replace exact target code snippets.
494
+ - **multi_edit_file**: Apply batch snippet replacements in a single step.
495
+ - **run_command**: Execute terminal commands (\`npm test\`, \`npm run build\`).
496
+ - **tree_view**: Generate visual ASCII directory tree structures.
497
+ - **file_info**: Get file statistics, size, lines, & modification time.
498
+ - **grep_search**: String/regex pattern search across codebase.
499
+ - **glob_search**: Filter workspace files by file pattern.
500
+ - **git_diff**: Retrieve uncommitted workspace git diff.
501
+ - **ask_user**: Request missing secret credentials or API keys.
502
+ - **task_completed**: Conclude task execution with teardown summary.
503
+
504
+ ### ⚡ Pro & Enterprise Exclusive Features
505
+ - **scratchpad**: Persistent working memory & reasoning scratchpad for multi-phase tasks.
506
+ - **computer_***: Autonomous desktop & GUI app control (screenshot, click, type, open app).
507
+ - **browser_***: Autonomous browser automation (navigate, scrape, interact).
508
+ - **Autonomous Loops**: Extended multi-step execution loops (30+ steps).
509
+
510
+ ### 🚀 Interactive Slash Commands
511
+ - \`/help\`: Show this tools & commands guide.
512
+ - \`/tree [dir]\`: Display workspace directory tree structure.
513
+ - \`/status\` or \`/stats\`: Show session telemetry & statistics.
514
+ - \`/test [cmd]\`: Run project verification test suite.
515
+ - \`/explain <file>\`: Get deep architectural breakdown of a file.
516
+ - \`/export\` or \`/save-session\`: Save session report to \`.ft/session-<timestamp>.md\`.
517
+ - \`/diff\`: Display colorized git diff of uncommitted changes.
518
+ - \`/clear\` or \`/compact\`: Reset agent conversation context.
519
+ - \`/undo\`: Revert last file modifications.
520
+ - \`/history\`: View recent Scout action logs.
521
+ - \`/plan\`: Generate implementation roadmap.
522
+ - \`/voice\`: Speak goal into microphone (STT).
523
+ - \`/speak <text>\`: Speak text out loud (TTS).
524
+ - \`/exit\`: End Scout Agent session.
525
+ `.trim();
526
+ safeNote(renderMarkdown(helpText), ' Scout Slash Commands & Tools');
527
+ continue;
528
+ }
529
+ if (trimmedInput.toLowerCase().startsWith('/speak')) {
530
+ const phrase = trimmedInput.slice(6).trim();
531
+ if (phrase) {
532
+ safeNote(` Speaking text: "${phrase}"...`, ' Scout Voice TTS');
533
+ await speakText(phrase);
534
+ }
535
+ else {
536
+ const textToSpeak = await text({
537
+ message: chalk.bold('Enter text to speak out loud:'),
538
+ placeholder: 'Hello Scout Agent',
539
+ });
540
+ if (!isCancel(textToSpeak) && typeof textToSpeak === 'string' && textToSpeak.trim()) {
541
+ safeNote(` Speaking text: "${textToSpeak}"...`, ' Scout Voice TTS');
542
+ await speakText(textToSpeak.trim());
543
+ }
544
+ }
545
+ continue;
546
+ }
547
+ if (trimmedInput.toLowerCase() === '/voice') {
548
+ safeNote(chalk.cyan(' Listening to your microphone via browser Web Speech API... Speak your goal clearly!'), ' Scout Voice STT');
549
+ const transcript = await listenSpeechToText();
550
+ if (transcript && transcript.trim()) {
551
+ safeNote(` Voice Received: "${chalk.bold.green(transcript.trim())}"`, ' Scout STT Transcript');
552
+ trimmedInput = transcript.trim();
553
+ }
554
+ else {
555
+ safeNote(chalk.yellow('No speech transcript detected or listener timed out.'), ' Scout Voice STT');
556
+ continue;
557
+ }
558
+ }
559
+ if (trimmedInput.toLowerCase() === '/tasks') {
560
+ const tasks = agentLoop.getStateManager().listSavedTasks();
561
+ if (tasks.length === 0) {
562
+ safeNote('No saved autonomous tasks found in .ft/tasks/.', 'Saved Autonomous Tasks');
563
+ }
564
+ else {
565
+ const taskList = tasks.map((t) => `• **${t.id}** [${(t.status || 'unknown').toUpperCase()}]\n Goal: _${t.objective}_\n Updated: ${new Date(t.updatedAt).toLocaleString()}`).join('\n\n');
566
+ safeNote(renderMarkdown(taskList), `Saved Autonomous Tasks (${tasks.length})`);
567
+ }
568
+ continue;
569
+ }
570
+ if (trimmedInput.toLowerCase().startsWith('/resume')) {
571
+ const parts = trimmedInput.split(/\s+/).slice(1);
572
+ const targetId = parts[0]?.trim();
573
+ if (!targetId) {
574
+ safeNote('Usage: /resume <taskId>', 'Resume Task');
575
+ continue;
576
+ }
577
+ const loaded = agentLoop.getStateManager().loadTaskState(targetId);
578
+ if (!loaded) {
579
+ safeNote(chalk.yellow(`No task found with ID "${targetId}" in .ft/tasks/`), 'Task Not Found');
580
+ continue;
581
+ }
582
+ options = { ...options, resume: targetId };
583
+ trimmedInput = loaded.objective;
584
+ }
585
+ if (trimmedInput.toLowerCase().startsWith('/tree')) {
586
+ const parts = trimmedInput.split(/\s+/).slice(1);
587
+ let targetDir = '.';
588
+ let maxDepth = 4;
589
+ if (parts.length === 1) {
590
+ if (/^\d+$/.test(parts[0])) {
591
+ maxDepth = parseInt(parts[0], 10);
592
+ }
593
+ else {
594
+ targetDir = parts[0];
595
+ }
596
+ }
597
+ else if (parts.length >= 2) {
598
+ if (/^\d+$/.test(parts[0])) {
599
+ maxDepth = parseInt(parts[0], 10);
600
+ targetDir = parts[1];
601
+ }
602
+ else {
603
+ targetDir = parts[0];
604
+ if (/^\d+$/.test(parts[1])) {
605
+ maxDepth = parseInt(parts[1], 10);
606
+ }
607
+ }
608
+ }
609
+ const fullDir = path.resolve(process.cwd(), targetDir);
610
+ if (!fs.existsSync(fullDir) || !fs.statSync(fullDir).isDirectory()) {
611
+ safeNote(chalk.yellow(`Directory not found: "${targetDir}"`), '️ Directory Tree Error');
612
+ }
613
+ else {
614
+ const treeStr = buildDirectoryTree(fullDir, '', 0, maxDepth);
615
+ safeNote(`Directory Tree for "${targetDir}" (depth ${maxDepth}):\n${targetDir}/\n${treeStr}`, ' Workspace Directory Tree');
616
+ }
617
+ continue;
618
+ }
619
+ if (['/status', '/stats'].includes(trimmedInput.toLowerCase())) {
620
+ const stats = agentLoop.getStats();
621
+ const modifiedFiles = agentLoop.getModifiedFiles();
622
+ const backups = agentLoop.getBackups();
623
+ const git = simpleGit();
624
+ let currentBranch = 'main';
625
+ try {
626
+ const branchSummary = await git.branch();
627
+ currentBranch = branchSummary.current || 'main';
628
+ }
629
+ catch { }
630
+ const elapsedSec = Math.round(stats.elapsedMs / 1000);
631
+ const activeModel = getActiveModelInfo();
632
+ const fallbackStatus = activeModel.lastFallbackInfo
633
+ ? `Switched from \`${activeModel.lastFallbackInfo.previousModel}\` ➔ \`${activeModel.lastFallbackInfo.fallbackModel}\` (${activeModel.lastFallbackInfo.reason})`
634
+ : 'None (Primary model actively responding)';
635
+ const statsMd = `
636
+ ### 📊 Scout Session & AI Model Telemetry
637
+ - **Active AI Model**: \`${activeModel.currentActiveModel}\`
638
+ - **Primary AI Model**: \`${activeModel.primaryModel}\`
639
+ - **Fallback Chain**: \`${activeModel.fallbackChain.join(' ➔ ')}\`
640
+ - **Last Fallback Event**: ${fallbackStatus}
641
+ - **Active Branch**: \`${currentBranch}\`
642
+ - **Elapsed Session Time**: \`${elapsedSec}s\`
643
+ - **Steps Executed**: \`${stats.totalSteps}\`
644
+ - **Total Tool Calls**: \`${stats.totalToolCalls}\`
645
+ - **Modified Files**: \`${modifiedFiles.length}\` (${modifiedFiles.map(f => `\`${f}\``).join(', ') || 'None'})
646
+ - **Stored File Backups**: \`${backups.length}\`
647
+ `.trim();
648
+ safeNote(renderMarkdown(statsMd), ' Scout Session & Model Telemetry');
649
+ continue;
650
+ }
651
+ if (trimmedInput.toLowerCase().startsWith('/test')) {
652
+ const parts = trimmedInput.split(/\s+/);
653
+ const customCmd = parts.slice(1).join(' ') || options?.verifyCmd;
654
+ const testSpin = spinner();
655
+ testSpin.start(`Running project verification test suite ${customCmd ? `(\`${customCmd}\`)` : ''}...`);
656
+ const testRes = await runProjectVerification(process.cwd(), customCmd);
657
+ if (testRes.valid) {
658
+ testSpin.stop(chalk.green('Project verification tests passed cleanly!'));
659
+ safeNote(`${chalk.green(' Verification Passed:')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Suite Clean');
660
+ }
661
+ else {
662
+ testSpin.stop(chalk.red('Project verification test suite failed.'));
663
+ safeNote(`${chalk.red('️ Verification Failed:')}\n${testRes.errors.join('\n')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Failure Warning');
664
+ }
665
+ continue;
666
+ }
667
+ if (trimmedInput.toLowerCase().startsWith('/explain')) {
668
+ const parts = trimmedInput.split(/\s+/);
669
+ const targetPath = parts[1];
670
+ if (!targetPath) {
671
+ safeNote(chalk.yellow('Usage: /explain <filePath> (e.g. /explain bin/src/index.ts)'), ' Explain Command');
672
+ continue;
673
+ }
674
+ const fullP = path.resolve(process.cwd(), targetPath);
675
+ if (!fs.existsSync(fullP)) {
676
+ safeNote(chalk.yellow(`File not found: ${targetPath}`), ' Explain Error');
677
+ continue;
678
+ }
679
+ const explainSpin = spinner();
680
+ explainSpin.start(`Analyzing architecture and structure of ${targetPath}...`);
681
+ const content = fs.readFileSync(fullP, 'utf-8');
682
+ 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 });
683
+ explainSpin.stop('Analysis complete.');
684
+ safeNote(renderMarkdown(explanation), ` Architectural Breakdown: ${targetPath}`);
685
+ continue;
686
+ }
687
+ if (['/export', '/save-session'].includes(trimmedInput.toLowerCase())) {
688
+ const ftDir = path.join(process.cwd(), '.ft', 'scout');
689
+ if (!fs.existsSync(ftDir))
690
+ fs.mkdirSync(ftDir, { recursive: true });
691
+ const timeStamp = new Date().toISOString().replace(/[:.]/g, '-');
692
+ const sessionPath = path.join(ftDir, `session-${timeStamp}.md`);
693
+ const stats = agentLoop.getStats();
694
+ const modifiedFiles = agentLoop.getModifiedFiles();
695
+ const history = (await import('../utils/config.js')).loadHistory();
696
+ const exportContent = `# Scout Agent Session Report
697
+ Date: ${new Date().toLocaleString()}
698
+ Project: ${projectName}
699
+
700
+ ## Session Statistics
701
+ - Total Steps: ${stats.totalSteps}
702
+ - Tool Calls: ${stats.totalToolCalls}
703
+ - Modified Files Count: ${modifiedFiles.length}
704
+
705
+ ## Modified Files
706
+ ${modifiedFiles.map(f => `- ${f}`).join('\n') || 'None'}
707
+
708
+ ## Action History
709
+ ${history.slice(0, 15).map(h => `- [${h.timestamp}] ${h.command}: ${h.description}`).join('\n')}
710
+ `;
711
+ fs.writeFileSync(sessionPath, exportContent, 'utf-8');
712
+ safeNote(chalk.green(`Session report saved to: ${chalk.bold.cyan(path.relative(process.cwd(), sessionPath))}`), ' Session Exported');
713
+ continue;
714
+ }
715
+ if (trimmedInput.toLowerCase() === '/diff') {
716
+ try {
717
+ const { execSync } = await import('child_process');
718
+ const gitDiff = execSync('git diff', { encoding: 'utf-8', cwd: process.cwd() });
719
+ if (!gitDiff || !gitDiff.trim()) {
720
+ safeNote(chalk.green('No uncommitted changes found in the workspace.'), ' Workspace Git Diff');
721
+ }
722
+ else {
723
+ const previewLines = gitDiff.split('\n');
724
+ const truncated = previewLines.length > 200 ? previewLines.slice(0, 200).join('\n') + `\n... [truncated, ${previewLines.length} total lines]` : gitDiff;
725
+ safeNote(truncated, ' Workspace Git Diff');
726
+ }
727
+ }
728
+ catch {
729
+ safeNote(chalk.yellow('Could not retrieve git diff. Ensure this directory is a git repository.'), '️ Git Diff Failed');
730
+ }
731
+ continue;
732
+ }
733
+ if (['/clear', '/compact'].includes(trimmedInput.toLowerCase())) {
734
+ agentLoop.resetContext();
735
+ safeNote(chalk.green('Agent context reset successfully.'), ' Session Context Cleared');
736
+ continue;
737
+ }
738
+ if (trimmedInput.toLowerCase() === '/undo') {
739
+ await handleUndo();
740
+ continue;
741
+ }
742
+ if (['/history', '/sessions'].includes(trimmedInput.toLowerCase())) {
743
+ const history = (await import('../utils/config.js')).loadHistory();
744
+ const sessions = (await import('../utils/session.js')).loadAgentSessions(5);
745
+ let historyMd = '### Agent Session History Memory (Previous 3+ Sessions)\n';
746
+ if (sessions.length === 0) {
747
+ historyMd += '*No previous agent sessions recorded yet.*\n';
748
+ }
749
+ else {
750
+ historyMd += sessions.map((s, i) => `- **Session #${sessions.length - i}** (${new Date(s.timestamp).toLocaleString()}):\n` +
751
+ ` • Goal: "${s.goal}"\n` +
752
+ ` • Summary: ${s.summary}\n` +
753
+ ` • Modified Files: ${s.modifiedFiles && s.modifiedFiles.length > 0 ? s.modifiedFiles.map(f => `\`${f}\``).join(', ') : 'None'}`).join('\n\n') + '\n';
754
+ }
755
+ historyMd += '\n### Recorded File Action Log\n';
756
+ historyMd += history.slice(0, 10).map((h) => `- **${h.timestamp}**: \`${h.command}\` ${h.description}`).join('\n') || 'No action history recorded yet.';
757
+ safeNote(renderMarkdown(historyMd), ' Scout History & Session Memory');
758
+ continue;
759
+ }
760
+ rawGoal = trimmedInput;
761
+ }
762
+ if (!rawGoal || !rawGoal.trim()) {
763
+ continue;
764
+ }
765
+ const parsedIntent = parseUserPromptIntent(rawGoal, filesList);
766
+ const effectiveGoal = parsedIntent.cleanGoal || rawGoal;
767
+ const goalLower = effectiveGoal.toLowerCase().trim();
768
+ // -------------------------------------------------------------
769
+ // Mode 1: Codebase Scan Mode (--scan or /scan)
770
+ // -------------------------------------------------------------
771
+ const isScanIntent = Boolean(options?.scan || (rawGoal && rawGoal.trim().startsWith('/scan')));
772
+ if (isScanIntent) {
773
+ const s = spinner();
774
+ s.start(' Scout Agent: Performing read-only bug & vulnerability scan...');
775
+ let codeSample = '';
776
+ const pkgPath = path.join(cwd, 'package.json');
777
+ if (fs.existsSync(pkgPath)) {
778
+ codeSample += fs.readFileSync(pkgPath, 'utf-8').slice(0, 1000) + '\n';
779
+ }
780
+ const issues = await generateCodeScanReport(projectName, filesList, codeSample);
781
+ saveScanReport(issues);
782
+ s.stop('Bug scan complete.');
783
+ const severityBadge = (sev) => {
784
+ switch (sev.toLowerCase()) {
785
+ case 'high':
786
+ return chalk.bgRed.white.bold(' HIGH ');
787
+ case 'medium':
788
+ return chalk.bgYellow.black.bold(' MEDIUM ');
789
+ default:
790
+ return chalk.bgBlue.white.bold(' LOW ');
791
+ }
792
+ };
793
+ const reportLines = issues.map((iss) => `${chalk.bold.cyan(`[Issue #${iss.id}]`)} ${severityBadge(iss.severity)} ${chalk.bold(iss.title)}\n` +
794
+ ` • File: ${chalk.yellow(iss.file)}\n` +
795
+ ` • Detail: ${iss.description}`);
796
+ safeNote(`Found ${issues.length} potential issues matching past hotfix patterns:\n\n` +
797
+ reportLines.join('\n\n') +
798
+ `\n\n${chalk.bold.green(' How to Auto-Fix Any Issue with Agent:')}\n` +
799
+ ` • Auto-fix Issue #${issues[0]?.id || 1}: ${chalk.bold.cyan(`scout agent --issue ${issues[0]?.id || 1}`)}\n` +
800
+ ` • Custom prompt auto-fix: ${chalk.bold.cyan('scout agent "Fix null check in src/index.ts"')}`, ' Scout Agent Bug Scan');
801
+ continue;
802
+ }
803
+ // -------------------------------------------------------------
804
+ // Mode 2: Feature Roadmap Plan Mode (--plan / --add or /plan)
805
+ // -------------------------------------------------------------
806
+ const planMatch = rawGoal ? rawGoal.match(/^\/plan\s+(.+)/i) : undefined;
807
+ const addIdea = options?.add || options?.plan || (planMatch ? planMatch[1] : undefined);
808
+ if (addIdea && !options?.create) {
809
+ const s = spinner();
810
+ s.start(` Scout Agent: Evaluating feasibility & generating roadmap for "${chalk.cyan(addIdea)}"...`);
811
+ const plan = await generateFeaturePlan(addIdea, projectName, filesList, targetLang, options?.native);
812
+ s.stop('Feasibility assessment & roadmap complete.');
813
+ safeNote(renderMarkdown(plan), ` Feature Implementation Roadmap: "${addIdea}"`);
814
+ continue;
815
+ }
816
+ // -------------------------------------------------------------
817
+ // Mode 3: Run Command Execution & Auto-Fix Mode (--run or /run)
818
+ // -------------------------------------------------------------
819
+ const runMatch = rawGoal ? rawGoal.match(/^\/run\s+["']?([^"']+)["']?$/i) : undefined;
820
+ const runCmdInput = options?.run || (runMatch ? runMatch[1] : undefined);
821
+ if (runCmdInput) {
822
+ const fullCommand = Array.isArray(runCmdInput) ? runCmdInput.join(' ') : runCmdInput;
823
+ safeNote(`Running command: ${chalk.bold.cyan(fullCommand)}\n${chalk.dim('Working directory: ' + cwd)}`, ' Scout Shell Execution');
824
+ const runSpinner = spinner();
825
+ runSpinner.start(`Executing \`${fullCommand}\`...`);
826
+ const res = await executeSmartCommand(fullCommand, cwd, 3000);
827
+ if (res.isBackgroundServer) {
828
+ runSpinner.stop(chalk.green(`Command \`${fullCommand}\` started in background (PID ${res.pid}).`));
829
+ safeNote(`${chalk.green('')} Background server process started (PID ${res.pid}). Output so far:\n\`\`\`\n${res.output}\n\`\`\``, ' Background Execution Active');
830
+ continue;
831
+ }
832
+ const exitCode = res.exitCode;
833
+ const fullOutput = res.output;
834
+ if (exitCode === 0) {
835
+ runSpinner.stop(chalk.green(`Command \`${fullCommand}\` executed successfully (Exit Code 0).`));
836
+ safeNote(`${chalk.green('')} Process completed with exit code 0.`, ' Execution Successful');
837
+ continue;
838
+ }
839
+ runSpinner.stop(chalk.red(`Command \`${fullCommand}\` failed with exit code ${exitCode}.`));
840
+ const diagSpinner = spinner();
841
+ diagSpinner.start('Scout AI is analyzing process failure output...');
842
+ const analysis = await analyzeCommandError(fullCommand, exitCode, fullOutput, projectName, filesList, targetLang, options?.native);
843
+ diagSpinner.stop(chalk.yellow('Failure diagnosis complete.'));
844
+ const diagMarkdown = `
845
+ ### Process Failure Diagnosis
846
+ Command: \`${fullCommand}\` (Exit Code ${exitCode})
847
+
848
+ ### Root Cause Analysis
849
+ ${analysis.rootCause}
850
+
851
+ ### Suggested Fix
852
+ ${analysis.suggestedFix}
853
+ ${analysis.targetFileToFix ? `\n### Suspected File\n\`${analysis.targetFileToFix}\`` : ''}
854
+ `.trim();
855
+ safeNote(renderMarkdown(diagMarkdown), '️ Scout AI Failure Diagnosis');
856
+ const shouldFix = await confirm({
857
+ message: chalk.bold('Would you like Scout Agent to auto-fix the failing code automatically?'),
858
+ initialValue: true,
859
+ });
860
+ if (!isCancel(shouldFix) && shouldFix) {
861
+ rawGoal = `Fix runtime/build error from command \`${fullCommand}\`. Cause: ${analysis.rootCause}`;
862
+ options = { ...options, file: analysis.targetFileToFix };
863
+ }
864
+ else {
865
+ continue;
866
+ }
867
+ }
868
+ // -------------------------------------------------------------
869
+ // Mode 4: Create File Generation Mode (Explicit --create flag only)
870
+ // -------------------------------------------------------------
871
+ const targetCreateFiles = options?.create
872
+ ? options.create.split(',').map((s) => s.trim()).filter(Boolean)
873
+ : undefined;
874
+ if (targetCreateFiles && targetCreateFiles.length > 0) {
875
+ let finalPrompt = effectiveGoal;
876
+ if (!finalPrompt || !finalPrompt.trim()) {
877
+ const userPromptInput = await text({
878
+ message: chalk.bold(`What should Scout generate inside ${chalk.cyan(targetCreateFiles.join(', '))}?`),
879
+ placeholder: 'e.g. Create source files with modules, styles, and logic',
880
+ validate: (val) => (!val || !val.trim() ? 'Please provide a prompt describing the file requirements' : undefined),
881
+ });
882
+ if (isCancel(userPromptInput)) {
883
+ safeNote(chalk.yellow('File creation operation cancelled.'), ' Scout File Creation');
884
+ continue;
885
+ }
886
+ finalPrompt = userPromptInput.trim();
887
+ }
888
+ const createdFilesSummary = [];
889
+ const backups = [];
890
+ const affectedFiles = [];
891
+ const git = simpleGit();
892
+ const isRepo = await git.checkIsRepo();
893
+ let currentBranch = 'main';
894
+ if (isRepo) {
895
+ try {
896
+ const branchSummary = await git.branch();
897
+ currentBranch = branchSummary.current || 'main';
898
+ }
899
+ catch { }
900
+ }
901
+ const s = spinner();
902
+ s.start(` Scout Agent: Generating ${targetCreateFiles.length} file(s) (${targetCreateFiles.join(', ')})...`);
903
+ let hasError = false;
904
+ for (const targetFilePath of targetCreateFiles) {
905
+ const resolvedPath = path.resolve(cwd, targetFilePath);
906
+ const fileExists = fs.existsSync(resolvedPath);
907
+ const originalContent = fileExists ? fs.readFileSync(resolvedPath, 'utf-8') : '__FT_WAS_NEW__';
908
+ try {
909
+ const contextPrompt = finalPrompt + (createdFilesSummary.length > 0 ? `\n\nContext of already generated files in this batch:\n` + createdFilesSummary.map(f => `--- File: ${f.path} ---\n${f.code.slice(0, 1000)}`).join('\n') : '');
910
+ const genResult = await generateCodeFile(targetFilePath, contextPrompt, projectName, filesList, targetLang, options?.native);
911
+ if (!genResult.code || !genResult.code.trim()) {
912
+ continue;
913
+ }
914
+ const parentDir = path.dirname(resolvedPath);
915
+ if (!fs.existsSync(parentDir)) {
916
+ fs.mkdirSync(parentDir, { recursive: true });
917
+ }
918
+ fs.writeFileSync(resolvedPath, genResult.code, 'utf-8');
919
+ backups.push({ filePath: targetFilePath, originalContent });
920
+ affectedFiles.push(targetFilePath);
921
+ createdFilesSummary.push({ path: targetFilePath, code: genResult.code, description: genResult.description });
922
+ }
923
+ catch (err) {
924
+ hasError = true;
925
+ s.stop(chalk.red(`Failed to generate code for ${targetFilePath}.`));
926
+ safeNote(chalk.red(`Error: ${err?.message || String(err)}`), '️ Code Generation Error');
927
+ break;
928
+ }
929
+ }
930
+ if (hasError || createdFilesSummary.length === 0) {
931
+ if (!hasError) {
932
+ s.stop(chalk.red('Failed to generate code files.'));
933
+ safeNote(chalk.yellow(`Scout AI returned empty code content. Please refine your prompt describing what code to write.`), '️ Empty Code Generated');
934
+ }
935
+ continue;
936
+ }
937
+ recordHistoryAction({
938
+ command: `scout agent --create "${affectedFiles.join(',')}" "${finalPrompt}"`,
939
+ description: `Scout created ${createdFilesSummary.length} file(s): ${affectedFiles.join(', ')}`,
940
+ previousBranch: currentBranch,
941
+ affectedFiles,
942
+ backups,
943
+ });
944
+ saveAgentSession({
945
+ goal: `Create file(s) ${affectedFiles.join(', ')}: ${finalPrompt}`,
946
+ summary: `Scout created ${createdFilesSummary.length} file(s): ${affectedFiles.join(', ')}`,
947
+ modifiedFiles: affectedFiles,
948
+ });
949
+ s.stop(chalk.green(`Successfully created ${createdFilesSummary.length} file(s)!`));
950
+ // Run automated syntax check and self-healing retries on created files
951
+ const healRes = await verifyAndSelfHealFiles(affectedFiles, cwd, projectName, filesList, {
952
+ verifyCmd: options?.verifyCmd,
953
+ maxRetries: options?.maxRetries || 3,
954
+ language: targetLang,
955
+ native: options?.native,
956
+ });
957
+ if (healRes.verifiedFiles.length > 0) {
958
+ safeNote(chalk.green(` Automated Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build verified clean!`), ' Code Verification Clean');
959
+ }
960
+ if (healRes.remainingErrors.length > 0) {
961
+ safeNote(chalk.yellow(`️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '️ Verification Warning');
962
+ }
963
+ const filePreviews = createdFilesSummary.map((f) => `#### \`${f.path}\`\n${f.description}\n\`\`\`\n${f.code.slice(0, 400)}${f.code.length > 400 ? '\n... (truncated)' : ''}\n\`\`\``).join('\n\n');
964
+ const teardownContent = `
965
+ ### Created Files (${createdFilesSummary.length})
966
+ ${createdFilesSummary.map(f => ` • \`${f.path}\``).join('\n')}
967
+
968
+ ### Overview & Previews
969
+ ${filePreviews}
970
+ `.trim();
971
+ safeNote(renderMarkdown(teardownContent), ' Scout File Creation Teardown');
972
+ if (options?.yes || !process.stdout.isTTY) {
973
+ safeNote(`${chalk.green('')} Saved ${chalk.bold.cyan(affectedFiles.length + ' file(s)')} on branch ${chalk.bold.green(currentBranch)}.\n` +
974
+ ` ${chalk.dim('You can revert this creation anytime by running:')} ${chalk.cyan('scout undo')}`, ' Creation Confirmed');
975
+ continue;
976
+ }
977
+ const actionChoice = await select({
978
+ message: chalk.bold('Review Scout Agent\'s generated file(s). What would you like to do?'),
979
+ options: [
980
+ { value: 'keep', label: ' Keep Files', hint: `Retain newly created files directly on current branch (${currentBranch})` },
981
+ { value: 'pr', label: ' Create Pull Request', hint: 'Push created files to a new branch & open a Pull Request' },
982
+ { value: 'revert', label: '️ Revert / Delete Files', hint: 'Remove created files immediately' },
983
+ ],
984
+ });
985
+ if (isCancel(actionChoice) || actionChoice === 'revert') {
986
+ const revSpinner = spinner();
987
+ revSpinner.start('Reverting Scout Agent\'s file creations...');
988
+ await handleUndo();
989
+ revSpinner.stop('Creation reverted successfully.');
990
+ continue;
991
+ }
992
+ if (actionChoice === 'pr') {
993
+ const prSpinner = spinner();
994
+ prSpinner.start('Creating Pull Request...');
995
+ const prRes = await createPullRequest({
996
+ branchPrefix: 'create',
997
+ title: `feat: Create ${affectedFiles.join(', ')}`,
998
+ affectedFiles,
999
+ commitMessage: `feat: Create ${affectedFiles.join(', ')}`,
1000
+ });
1001
+ prSpinner.stop('Pull Request workflow processed.');
1002
+ safeNote(`${chalk.green('')} ${prRes.message}\n` +
1003
+ ` ${chalk.dim('You remain on active branch:')} ${chalk.cyan(currentBranch)}`, ' Pull Request Status');
1004
+ continue;
1005
+ }
1006
+ safeNote(`${chalk.green('')} Saved ${chalk.bold.cyan(affectedFiles.length + ' file(s)')} on branch ${chalk.bold.green(currentBranch)}.\n` +
1007
+ ` ${chalk.dim('You can revert this creation anytime by running:')} ${chalk.cyan('scout undo')}`, ' Creation Confirmed');
1008
+ continue;
1009
+ }
1010
+ // -------------------------------------------------------------
1011
+ // Mode 5: Autonomous ReAct Tool-Calling Agent Loop
1012
+ // -------------------------------------------------------------
1013
+ let finalGoal = effectiveGoal || rawGoal;
1014
+ if (!finalGoal && options?.issue) {
1015
+ const report = loadScanReport();
1016
+ const issueId = Number(options.issue);
1017
+ const targetIssue = report.find((i) => i.id === issueId) || report[0];
1018
+ if (targetIssue) {
1019
+ finalGoal = `Fix issue #${targetIssue.id}: ${targetIssue.title} ${targetIssue.description}`;
1020
+ }
1021
+ }
1022
+ if (!finalGoal || !finalGoal.trim()) {
1023
+ safeNote(chalk.yellow('No active goal specified.'), ' Scout Agent');
1024
+ continue;
1025
+ }
1026
+ const git = simpleGit();
1027
+ const isRepo = await git.checkIsRepo();
1028
+ let currentBranch = 'main';
1029
+ if (isRepo) {
1030
+ try {
1031
+ const branchSummary = await git.branch();
1032
+ currentBranch = branchSummary.current || 'main';
1033
+ }
1034
+ catch { }
1035
+ }
1036
+ // Fast Multi-File Fix path via generateMultiCodeFix if --issue is set
1037
+ if (options?.issue) {
1038
+ const idSpinner = spinner();
1039
+ idSpinner.start('Scout AI: Identifying affected target files...');
1040
+ const targetFiles = await identifyTargetFiles(finalGoal, projectName, filesList, targetLang, options?.native);
1041
+ idSpinner.stop(targetFiles.length > 0 ? `Identified ${targetFiles.length} target file(s): ${targetFiles.join(', ')}` : 'Target identification complete.');
1042
+ if (targetFiles.length > 0) {
1043
+ const targetMap = {};
1044
+ for (const tf of targetFiles) {
1045
+ const absP = path.resolve(cwd, tf);
1046
+ if (fs.existsSync(absP)) {
1047
+ targetMap[tf] = fs.readFileSync(absP, 'utf-8');
1048
+ }
1049
+ }
1050
+ if (Object.keys(targetMap).length > 0) {
1051
+ const fixSpinner = spinner();
1052
+ fixSpinner.start(`Scout AI: Generating multi-file fixes for ${Object.keys(targetMap).join(', ')}...`);
1053
+ const multiFixRes = await generateMultiCodeFix(finalGoal, projectName, filesList, targetMap, targetLang, options?.native, (done, total, curFile) => {
1054
+ fixSpinner.message(`Generating fix for ${curFile} (${done}/${total})...`);
1055
+ });
1056
+ fixSpinner.stop('Multi-file fixes generated.');
1057
+ const backups = [];
1058
+ const affectedFiles = [];
1059
+ for (const item of multiFixRes.files) {
1060
+ const absPath = path.resolve(cwd, item.targetFile);
1061
+ if (fs.existsSync(absPath)) {
1062
+ backups.push({
1063
+ filePath: item.targetFile,
1064
+ originalContent: fs.readFileSync(absPath, 'utf-8'),
1065
+ });
1066
+ }
1067
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
1068
+ fs.writeFileSync(absPath, item.fixedCode, 'utf-8');
1069
+ affectedFiles.push(item.targetFile);
1070
+ }
1071
+ recordHistoryAction({
1072
+ command: `scout agent --issue ${options.issue}`,
1073
+ description: `Auto-fixed issue on ${affectedFiles.length} file(s)`,
1074
+ previousBranch: currentBranch,
1075
+ affectedFiles,
1076
+ backups,
1077
+ });
1078
+ saveAgentSession({
1079
+ goal: `Auto-fix issue #${options.issue}: ${finalGoal}`,
1080
+ summary: `Auto-fixed issue on ${affectedFiles.length} file(s): ${affectedFiles.join(', ')}`,
1081
+ modifiedFiles: affectedFiles,
1082
+ });
1083
+ 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');
1084
+ continue;
1085
+ }
1086
+ }
1087
+ }
1088
+ // Standard Agent Goal Execution: Pre-scan & identify target files ONLY if goal is a code modification task
1089
+ const hasModificationIntent = /\b(fix|edit|modify|update|refactor|patch|change|debug|error|bug|issue|solve|repair)\b/i.test(finalGoal) &&
1090
+ !/\b(lookup|look\s*up|who|what|why|where|how|explain|describe|tell|status|check|github|mcp|open|launch)\b/i.test(finalGoal);
1091
+ let targetFiles = [];
1092
+ if (hasModificationIntent && !options?.file) {
1093
+ const idSpinner = spinner();
1094
+ idSpinner.start('Scout Agent: Thinking');
1095
+ targetFiles = await identifyTargetFiles(finalGoal, projectName, filesList, targetLang, options?.native);
1096
+ idSpinner.stop(targetFiles.length > 0 ? `Target files resolved: ${targetFiles.map(f => `\`${f}\``).join(', ')}` : 'Analysis complete.');
1097
+ }
1098
+ safeNote(`${chalk.bold.cyan(' Scout Autonomous Execution Initialized')}\n` +
1099
+ `${chalk.dim('Goal:')} ${finalGoal}` +
1100
+ (targetFiles.length > 0 ? `\n${chalk.dim('Target Files:')} ${targetFiles.map(f => `\`${f}\``).join(', ')}` : ''), ' Scout Agent Execution');
1101
+ const execResult = options?.resume
1102
+ ? await agentLoop.resumeGoal(options.resume)
1103
+ : await agentLoop.executeGoal(finalGoal);
1104
+ const modifiedFiles = agentLoop.getModifiedFiles();
1105
+ const backups = agentLoop.getBackups();
1106
+ if (isRepo) {
1107
+ try {
1108
+ const branchSummary = await git.branch();
1109
+ currentBranch = branchSummary.current || 'main';
1110
+ }
1111
+ catch { }
1112
+ }
1113
+ if (modifiedFiles.length > 0) {
1114
+ recordHistoryAction({
1115
+ command: `scout agent "${finalGoal}"`,
1116
+ description: `Scout agent completed goal on ${modifiedFiles.length} file(s)`,
1117
+ previousBranch: currentBranch,
1118
+ affectedFiles: modifiedFiles,
1119
+ backups,
1120
+ });
1121
+ }
1122
+ const isCodeModTask = modifiedFiles.length > 0;
1123
+ const resultSectionTitle = isCodeModTask ? 'Execution Summary' : 'Answer & Findings';
1124
+ const teardownMd = `
1125
+ ### Agent Goal
1126
+ ${finalGoal}
1127
+
1128
+ ### Modified File(s)
1129
+ ${modifiedFiles.map((f) => ` • \`${f}\``).join('\n') || ' • None'}
1130
+
1131
+ ### ${resultSectionTitle}
1132
+ ${execResult.summary}
1133
+ `.trim();
1134
+ safeNote(renderMarkdown(teardownMd), isCodeModTask ? ' Scout Execution Teardown Report' : ' Scout Response');
1135
+ if (isAudioMode && execResult?.summary) {
1136
+ safeNote(` Speaking conversational response out loud...`, ' Scout Audio Output');
1137
+ const spokenClosing = formatConversationalClosing(execResult.summary);
1138
+ await speakText(spokenClosing);
1139
+ }
1140
+ if (modifiedFiles.length === 0) {
1141
+ if (isOneShot)
1142
+ break;
1143
+ continue;
1144
+ }
1145
+ if (options?.yes || !process.stdout.isTTY) {
1146
+ safeNote(`${chalk.green('')} Agent changes retained on ${modifiedFiles.length} file(s) (Branch: ${chalk.bold.green(currentBranch)}).\n` +
1147
+ ` ${chalk.dim('You can revert anytime by running:')} ${chalk.cyan('scout undo')}`, ' Agent Execution Confirmed');
1148
+ if (isOneShot)
1149
+ break;
1150
+ continue;
1151
+ }
1152
+ const actionChoice = await select({
1153
+ message: chalk.bold('Review Scout Agent result. What would you like to do?'),
1154
+ options: [
1155
+ { value: 'keep', label: ' Keep Changes', hint: `Retain agent changes on current branch (${currentBranch})` },
1156
+ { value: 'test', label: ' Run Verification Tests', hint: 'Execute test suite to confirm zero regressions' },
1157
+ { value: 'pr', label: ' Create Pull Request', hint: 'Push fix to a new branch & open a Pull Request' },
1158
+ { value: 'export', label: ' Export Session Report', hint: 'Save detailed session teardown report to .ft/' },
1159
+ { value: 'revert', label: '️ Revert Changes', hint: 'Restore original code immediately' },
1160
+ ],
1161
+ });
1162
+ if (actionChoice === 'revert') {
1163
+ const revSpinner = spinner();
1164
+ revSpinner.start('Reverting Agent changes...');
1165
+ await handleUndo();
1166
+ revSpinner.stop('Changes reverted successfully.');
1167
+ continue;
1168
+ }
1169
+ if (actionChoice === 'test') {
1170
+ const testSpin = spinner();
1171
+ testSpin.start('Running project verification test suite...');
1172
+ const testRes = await runProjectVerification(process.cwd(), options?.verifyCmd);
1173
+ if (testRes.valid) {
1174
+ testSpin.stop(chalk.green('Project verification tests passed cleanly!'));
1175
+ safeNote(`${chalk.green(' Verification Passed:')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Suite Clean');
1176
+ }
1177
+ else {
1178
+ testSpin.stop(chalk.red('Project verification test suite failed.'));
1179
+ safeNote(`${chalk.red('️ Verification Failed:')}\n${testRes.errors.join('\n')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Failure Warning');
1180
+ }
1181
+ continue;
1182
+ }
1183
+ if (actionChoice === 'export') {
1184
+ const ftDir = path.join(process.cwd(), '.ft', 'scout');
1185
+ if (!fs.existsSync(ftDir))
1186
+ fs.mkdirSync(ftDir, { recursive: true });
1187
+ const timeStamp = new Date().toISOString().replace(/[:.]/g, '-');
1188
+ const sessionPath = path.join(ftDir, `session-${timeStamp}.md`);
1189
+ const stats = agentLoop.getStats();
1190
+ const exportContent = `# Scout Agent Session Report
1191
+ Date: ${new Date().toLocaleString()}
1192
+ Project: ${projectName}
1193
+ Goal: ${finalGoal}
1194
+
1195
+ ## Session Statistics
1196
+ - Total Steps: ${stats.totalSteps}
1197
+ - Tool Calls: ${stats.totalToolCalls}
1198
+ - Modified Files Count: ${modifiedFiles.length}
1199
+
1200
+ ## Modified Files
1201
+ ${modifiedFiles.map(f => `- ${f}`).join('\n') || 'None'}
1202
+
1203
+ ## Execution Summary
1204
+ ${execResult.summary}
1205
+ `;
1206
+ fs.writeFileSync(sessionPath, exportContent, 'utf-8');
1207
+ safeNote(chalk.green(`Session report saved to: ${chalk.bold.cyan(path.relative(process.cwd(), sessionPath))}`), ' Session Exported');
1208
+ continue;
1209
+ }
1210
+ if (actionChoice === 'pr') {
1211
+ const prSpinner = spinner();
1212
+ prSpinner.start('Creating Pull Request...');
1213
+ const prRes = await createPullRequest({
1214
+ branchPrefix: 'agent',
1215
+ title: finalGoal,
1216
+ affectedFiles: modifiedFiles,
1217
+ commitMessage: `feat: ${finalGoal}`,
1218
+ });
1219
+ prSpinner.stop('Pull Request workflow processed.');
1220
+ safeNote(`${chalk.green('')} ${prRes.message}\n` +
1221
+ ` ${chalk.dim('You remain on active branch:')} ${chalk.cyan(currentBranch)}`, ' Pull Request Status');
1222
+ continue;
1223
+ }
1224
+ safeNote(`${chalk.green('')} Agent changes retained on ${modifiedFiles.length} file(s) (Branch: ${chalk.bold.green(currentBranch)}).\n` +
1225
+ ` ${chalk.dim('You can revert these changes anytime by running:')} ${chalk.cyan('scout undo')}`, ' Agent Workflow Finished');
1226
+ if (isOneShot)
1227
+ break;
1228
+ }
1229
+ }
1230
+ //# sourceMappingURL=agent.js.map