ft-scout 1.0.9 → 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.
- package/.firebase/hosting.d2Vi.cache +9 -0
- package/.firebaserc +14 -0
- package/AUTONOMOUS_FEATURES.md +192 -0
- package/README.md +482 -29
- package/bin/ft.js +2 -0
- package/bin/src/commands/agent.d.ts.map +1 -0
- package/bin/src/commands/agent.js +1230 -0
- package/bin/src/commands/agent.js.map +1 -0
- package/bin/src/commands/audit.d.ts.map +1 -0
- package/bin/src/commands/audit.js +104 -0
- package/bin/src/commands/audit.js.map +1 -0
- package/bin/src/commands/authCommand.d.ts.map +1 -0
- package/bin/src/commands/authCommand.js +38 -0
- package/bin/src/commands/authCommand.js.map +1 -0
- package/bin/src/commands/benchmark.d.ts.map +1 -0
- package/bin/src/commands/benchmark.js +135 -0
- package/bin/src/commands/benchmark.js.map +1 -0
- package/bin/src/commands/brief.d.ts.map +1 -0
- package/bin/src/commands/brief.js +37 -0
- package/bin/src/commands/brief.js.map +1 -0
- package/bin/src/commands/configCommand.d.ts.map +1 -0
- package/bin/src/commands/configCommand.js +53 -0
- package/bin/src/commands/configCommand.js.map +1 -0
- package/bin/src/commands/dashboard.d.ts.map +1 -0
- package/bin/src/commands/dashboard.js +41 -0
- package/bin/src/commands/dashboard.js.map +1 -0
- package/bin/src/commands/history.d.ts.map +1 -0
- package/bin/src/commands/history.js +20 -0
- package/bin/src/commands/history.js.map +1 -0
- package/bin/src/commands/init.d.ts.map +1 -1
- package/bin/src/commands/init.js +408 -79
- package/bin/src/commands/init.js.map +1 -1
- package/bin/src/commands/login.d.ts.map +1 -0
- package/bin/src/commands/login.js +190 -0
- package/bin/src/commands/login.js.map +1 -0
- package/bin/src/commands/logout.d.ts.map +1 -0
- package/bin/src/commands/logout.js +15 -0
- package/bin/src/commands/logout.js.map +1 -0
- package/bin/src/commands/mcp.d.ts.map +1 -0
- package/bin/src/commands/mcp.js +393 -0
- package/bin/src/commands/mcp.js.map +1 -0
- package/bin/src/commands/owners.js +6 -6
- package/bin/src/commands/owners.js.map +1 -1
- package/bin/src/commands/plan.d.ts.map +1 -0
- package/bin/src/commands/plan.js +152 -0
- package/bin/src/commands/plan.js.map +1 -0
- package/bin/src/commands/quota.d.ts.map +1 -0
- package/bin/src/commands/quota.js +32 -0
- package/bin/src/commands/quota.js.map +1 -0
- package/bin/src/commands/recommend.d.ts.map +1 -0
- package/bin/src/commands/recommend.js +46 -0
- package/bin/src/commands/recommend.js.map +1 -0
- package/bin/src/commands/risky.d.ts.map +1 -1
- package/bin/src/commands/risky.js +235 -39
- package/bin/src/commands/risky.js.map +1 -1
- package/bin/src/commands/setup.d.ts.map +1 -1
- package/bin/src/commands/setup.js +411 -16
- package/bin/src/commands/setup.js.map +1 -1
- package/bin/src/commands/signup.d.ts.map +1 -0
- package/bin/src/commands/signup.js +118 -0
- package/bin/src/commands/signup.js.map +1 -0
- package/bin/src/commands/undo.d.ts.map +1 -0
- package/bin/src/commands/undo.js +59 -0
- package/bin/src/commands/undo.js.map +1 -0
- package/bin/src/commands/web.d.ts.map +1 -0
- package/bin/src/commands/web.js +90 -0
- package/bin/src/commands/web.js.map +1 -0
- package/bin/src/discovery/config.d.ts.map +1 -0
- package/bin/src/discovery/config.js +41 -0
- package/bin/src/discovery/config.js.map +1 -0
- package/bin/src/discovery/display.d.ts.map +1 -0
- package/bin/src/discovery/display.js +57 -0
- package/bin/src/discovery/display.js.map +1 -0
- package/bin/src/discovery/messages.d.ts.map +1 -0
- package/bin/src/discovery/messages.js +7 -0
- package/bin/src/discovery/messages.js.map +1 -0
- package/bin/src/discovery/signals.d.ts.map +1 -0
- package/bin/src/discovery/signals.js +33 -0
- package/bin/src/discovery/signals.js.map +1 -0
- package/bin/src/engine/actionVerifier.d.ts.map +1 -0
- package/bin/src/engine/actionVerifier.js +283 -0
- package/bin/src/engine/actionVerifier.js.map +1 -0
- package/bin/src/engine/agentEngine.d.ts.map +1 -0
- package/bin/src/engine/agentEngine.js +2188 -0
- package/bin/src/engine/agentEngine.js.map +1 -0
- package/bin/src/engine/agentState.d.ts.map +1 -0
- package/bin/src/engine/agentState.js +381 -0
- package/bin/src/engine/agentState.js.map +1 -0
- package/bin/src/engine/browser/browserController.d.ts.map +1 -0
- package/bin/src/engine/browser/browserController.js +737 -0
- package/bin/src/engine/browser/browserController.js.map +1 -0
- package/bin/src/engine/browser/index.d.ts.map +1 -0
- package/bin/src/engine/browser/index.js +2 -0
- package/bin/src/engine/browser/index.js.map +1 -0
- package/bin/src/engine/computer/computerController.d.ts.map +1 -0
- package/bin/src/engine/computer/computerController.js +773 -0
- package/bin/src/engine/computer/computerController.js.map +1 -0
- package/bin/src/engine/computer/computerDiagnostic.d.ts.map +1 -0
- package/bin/src/engine/computer/computerDiagnostic.js +71 -0
- package/bin/src/engine/computer/computerDiagnostic.js.map +1 -0
- package/bin/src/engine/computer/index.d.ts.map +1 -0
- package/bin/src/engine/computer/index.js +4 -0
- package/bin/src/engine/computer/index.js.map +1 -0
- package/bin/src/engine/computer/mockComputer.d.ts.map +1 -0
- package/bin/src/engine/computer/mockComputer.js +260 -0
- package/bin/src/engine/computer/mockComputer.js.map +1 -0
- package/bin/src/engine/computer/scoutDriver.cs +1161 -0
- package/bin/src/engine/computer/scout_driver.exe +0 -0
- package/bin/src/engine/git.d.ts.map +1 -1
- package/bin/src/engine/git.js +71 -0
- 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 +1264 -35
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/planner.d.ts.map +1 -0
- package/bin/src/engine/planner.js +321 -0
- package/bin/src/engine/planner.js.map +1 -0
- package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -0
- package/bin/src/engine/runtime/agentRuntime.js +1258 -0
- package/bin/src/engine/runtime/agentRuntime.js.map +1 -0
- package/bin/src/engine/runtime/index.d.ts.map +1 -0
- package/bin/src/engine/runtime/index.js +2 -0
- package/bin/src/engine/runtime/index.js.map +1 -0
- package/bin/src/engine/safety.d.ts.map +1 -0
- package/bin/src/engine/safety.js +316 -0
- package/bin/src/engine/safety.js.map +1 -0
- package/bin/src/engine/scoutAudioRecorder.cs +215 -0
- package/bin/src/engine/scout_audio_recorder.exe +0 -0
- package/bin/src/engine/toolRegistry.d.ts.map +1 -0
- package/bin/src/engine/toolRegistry.js +521 -0
- package/bin/src/engine/toolRegistry.js.map +1 -0
- package/bin/src/engine/verifier.d.ts.map +1 -0
- package/bin/src/engine/verifier.js +414 -0
- package/bin/src/engine/verifier.js.map +1 -0
- package/bin/src/engine/voiceEngine.d.ts.map +1 -0
- package/bin/src/engine/voiceEngine.js +474 -0
- package/bin/src/engine/voiceEngine.js.map +1 -0
- package/bin/src/engine/web.d.ts.map +1 -0
- package/bin/src/engine/web.js +161 -0
- package/bin/src/engine/web.js.map +1 -0
- package/bin/src/engine/workspace/agentWorkspace.d.ts.map +1 -0
- package/bin/src/engine/workspace/agentWorkspace.js +505 -0
- package/bin/src/engine/workspace/agentWorkspace.js.map +1 -0
- package/bin/src/engine/workspace/index.d.ts.map +1 -0
- package/bin/src/engine/workspace/index.js +2 -0
- package/bin/src/engine/workspace/index.js.map +1 -0
- package/bin/src/index.js +369 -19
- package/bin/src/index.js.map +1 -1
- package/bin/src/mcp/client.d.ts.map +1 -0
- package/bin/src/mcp/client.js +127 -0
- package/bin/src/mcp/client.js.map +1 -0
- package/bin/src/mcp/discovery.d.ts.map +1 -0
- package/bin/src/mcp/discovery.js +91 -0
- package/bin/src/mcp/discovery.js.map +1 -0
- package/bin/src/mcp/health.d.ts.map +1 -0
- package/bin/src/mcp/health.js +44 -0
- package/bin/src/mcp/health.js.map +1 -0
- package/bin/src/mcp/index.d.ts.map +1 -0
- package/bin/src/mcp/index.js +15 -0
- package/bin/src/mcp/index.js.map +1 -0
- package/bin/src/mcp/manager.d.ts.map +1 -0
- package/bin/src/mcp/manager.js +648 -0
- package/bin/src/mcp/manager.js.map +1 -0
- package/bin/src/mcp/permissions.d.ts.map +1 -0
- package/bin/src/mcp/permissions.js +50 -0
- package/bin/src/mcp/permissions.js.map +1 -0
- package/bin/src/mcp/persistence.d.ts.map +1 -0
- package/bin/src/mcp/persistence.js +171 -0
- package/bin/src/mcp/persistence.js.map +1 -0
- package/bin/src/mcp/recovery.d.ts.map +1 -0
- package/bin/src/mcp/recovery.js +75 -0
- package/bin/src/mcp/recovery.js.map +1 -0
- package/bin/src/mcp/telemetry.d.ts.map +1 -0
- package/bin/src/mcp/telemetry.js +82 -0
- package/bin/src/mcp/telemetry.js.map +1 -0
- package/bin/src/mcp/toolRouter.d.ts.map +1 -0
- package/bin/src/mcp/toolRouter.js +127 -0
- package/bin/src/mcp/toolRouter.js.map +1 -0
- package/bin/src/mcp/transport.d.ts.map +1 -0
- package/bin/src/mcp/transport.js +328 -0
- package/bin/src/mcp/transport.js.map +1 -0
- package/bin/src/mcp/types.d.ts.map +1 -0
- package/bin/src/mcp/types.js +6 -0
- package/bin/src/mcp/types.js.map +1 -0
- package/bin/src/server/server.d.ts.map +1 -0
- package/bin/src/server/server.js +989 -0
- package/bin/src/server/server.js.map +1 -0
- package/bin/src/utils/admin.d.ts.map +1 -0
- package/bin/src/utils/admin.js +231 -0
- package/bin/src/utils/admin.js.map +1 -0
- package/bin/src/utils/auditLogger.d.ts.map +1 -0
- package/bin/src/utils/auditLogger.js +70 -0
- package/bin/src/utils/auditLogger.js.map +1 -0
- package/bin/src/utils/auth.d.ts.map +1 -0
- package/bin/src/utils/auth.js +679 -0
- package/bin/src/utils/auth.js.map +1 -0
- package/bin/src/utils/branding.d.ts.map +1 -0
- package/bin/src/utils/branding.js +14 -0
- package/bin/src/utils/branding.js.map +1 -0
- package/bin/src/utils/config.d.ts.map +1 -1
- package/bin/src/utils/config.js +124 -3
- package/bin/src/utils/config.js.map +1 -1
- package/bin/src/utils/email.d.ts.map +1 -0
- package/bin/src/utils/email.js +267 -0
- package/bin/src/utils/email.js.map +1 -0
- package/bin/src/utils/firebaseAuth.d.ts.map +1 -0
- package/bin/src/utils/firebaseAuth.js +174 -0
- package/bin/src/utils/firebaseAuth.js.map +1 -0
- package/bin/src/utils/holoseneAuth.d.ts.map +1 -0
- package/bin/src/utils/holoseneAuth.js +307 -0
- package/bin/src/utils/holoseneAuth.js.map +1 -0
- package/bin/src/utils/language.d.ts.map +1 -0
- package/bin/src/utils/language.js +181 -0
- package/bin/src/utils/language.js.map +1 -0
- package/bin/src/utils/markdown.d.ts.map +1 -0
- package/bin/src/utils/markdown.js +215 -0
- package/bin/src/utils/markdown.js.map +1 -0
- package/bin/src/utils/payment.d.ts.map +1 -0
- package/bin/src/utils/payment.js +290 -0
- package/bin/src/utils/payment.js.map +1 -0
- package/bin/src/utils/pricing.d.ts.map +1 -0
- package/bin/src/utils/pricing.js +165 -0
- package/bin/src/utils/pricing.js.map +1 -0
- package/bin/src/utils/razorpay.d.ts.map +1 -0
- package/bin/src/utils/razorpay.js +144 -0
- package/bin/src/utils/razorpay.js.map +1 -0
- package/bin/src/utils/session.d.ts.map +1 -0
- package/bin/src/utils/session.js +82 -0
- package/bin/src/utils/session.js.map +1 -0
- package/bin/src/utils/updateCheck.d.ts.map +1 -0
- package/bin/src/utils/updateCheck.js +108 -0
- package/bin/src/utils/updateCheck.js.map +1 -0
- package/downloads/invoice-2026-03.pdf +1 -0
- package/firebase.json +74 -0
- package/package.json +49 -46
- package/web/ScoutFavicon.png +0 -0
- package/web/app.js +3944 -0
- package/web/styles.css +3081 -0
- package/.ft/context.json +0 -17
- package/email-template.txt +0 -44
package/bin/src/engine/llm.js
CHANGED
|
@@ -1,20 +1,552 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import OpenAI from 'openai';
|
|
2
3
|
import dotenv from 'dotenv';
|
|
3
4
|
import * as url from 'url';
|
|
4
5
|
import path from 'path';
|
|
6
|
+
import { getWebContext } from './web.js';
|
|
7
|
+
import { getLanguageInstruction } from '../utils/language.js';
|
|
8
|
+
import { loadAuthConfig } from '../utils/auth.js';
|
|
9
|
+
import { logAuditEvent } from '../utils/auditLogger.js';
|
|
5
10
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
6
|
-
// Attempt to load .env from the root of the CLI installation
|
|
11
|
+
// Attempt to load .env from the root of the CLI installation or current working directory
|
|
7
12
|
dotenv.config({ path: path.resolve(__dirname, '../../../.env'), quiet: true });
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
dotenv.config({ quiet: true });
|
|
14
|
+
const groqApiKey = process.env.GROQ_API_KEY;
|
|
15
|
+
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
16
|
+
const geminiApiKey = process.env.GEMINI_API_KEY;
|
|
17
|
+
const nvidiaApiKey = process.env.NVIDIA_API_KEY || process.env.NVCF_API_KEY;
|
|
18
|
+
const authConfig = loadAuthConfig();
|
|
19
|
+
const byokApiKey = authConfig.byokEnabled ? authConfig.customApiKey?.trim() : undefined;
|
|
20
|
+
const byokProvider = authConfig.customLlmProvider?.trim().toLowerCase();
|
|
21
|
+
const byokDefaultBaseUrls = {
|
|
22
|
+
openai: 'https://api.openai.com/v1',
|
|
23
|
+
groq: 'https://api.groq.com/openai/v1',
|
|
24
|
+
gemini: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
25
|
+
nvidia: 'https://integrate.api.nvidia.com/v1',
|
|
26
|
+
};
|
|
27
|
+
const byokBaseUrl = byokApiKey
|
|
28
|
+
? authConfig.customBaseUrl?.trim() || byokDefaultBaseUrls[byokProvider || '']
|
|
29
|
+
: undefined;
|
|
30
|
+
const isByokConfigured = Boolean(byokApiKey && byokBaseUrl);
|
|
31
|
+
const clientCache = new Map();
|
|
32
|
+
export function getClientForModel(modelName) {
|
|
33
|
+
if (isByokConfigured) {
|
|
34
|
+
if (!clientCache.has('byok')) {
|
|
35
|
+
clientCache.set('byok', new OpenAI({ apiKey: byokApiKey, baseURL: byokBaseUrl, timeout: 35000 }));
|
|
36
|
+
}
|
|
37
|
+
return clientCache.get('byok');
|
|
38
|
+
}
|
|
39
|
+
const modelLower = (modelName || '').toLowerCase();
|
|
40
|
+
if ((modelLower.startsWith('nvidia/') || modelLower.includes('nemotron') || modelLower.startsWith('deepseek-ai/')) && nvidiaApiKey) {
|
|
41
|
+
if (!clientCache.has('nvidia')) {
|
|
42
|
+
clientCache.set('nvidia', new OpenAI({ apiKey: nvidiaApiKey, baseURL: 'https://integrate.api.nvidia.com/v1', timeout: 35000 }));
|
|
43
|
+
}
|
|
44
|
+
return clientCache.get('nvidia');
|
|
45
|
+
}
|
|
46
|
+
if ((modelLower.startsWith('groq/') || modelLower.includes('groq') || modelLower.startsWith('qwen/') || modelLower.startsWith('llama-') || modelLower.startsWith('llama3-') || modelLower.startsWith('mixtral')) && groqApiKey) {
|
|
47
|
+
if (!clientCache.has('groq')) {
|
|
48
|
+
clientCache.set('groq', new OpenAI({ apiKey: groqApiKey, baseURL: 'https://api.groq.com/openai/v1', timeout: 20000 }));
|
|
49
|
+
}
|
|
50
|
+
return clientCache.get('groq');
|
|
51
|
+
}
|
|
52
|
+
if (modelLower.startsWith('gemini-') && geminiApiKey) {
|
|
53
|
+
if (!clientCache.has('gemini')) {
|
|
54
|
+
clientCache.set('gemini', new OpenAI({ apiKey: geminiApiKey, baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', timeout: 20000 }));
|
|
55
|
+
}
|
|
56
|
+
return clientCache.get('gemini');
|
|
57
|
+
}
|
|
58
|
+
if (modelLower.startsWith('gpt-') && openaiApiKey) {
|
|
59
|
+
if (!clientCache.has('openai')) {
|
|
60
|
+
clientCache.set('openai', new OpenAI({ apiKey: openaiApiKey, baseURL: 'https://api.openai.com/v1', timeout: 20000 }));
|
|
61
|
+
}
|
|
62
|
+
return clientCache.get('openai');
|
|
63
|
+
}
|
|
64
|
+
if (modelLower.startsWith('openai/')) {
|
|
65
|
+
if (groqApiKey) {
|
|
66
|
+
if (!clientCache.has('groq')) {
|
|
67
|
+
clientCache.set('groq', new OpenAI({ apiKey: groqApiKey, baseURL: 'https://api.groq.com/openai/v1', timeout: 20000 }));
|
|
68
|
+
}
|
|
69
|
+
return clientCache.get('groq');
|
|
70
|
+
}
|
|
71
|
+
if (nvidiaApiKey) {
|
|
72
|
+
if (!clientCache.has('nvidia')) {
|
|
73
|
+
clientCache.set('nvidia', new OpenAI({ apiKey: nvidiaApiKey, baseURL: 'https://integrate.api.nvidia.com/v1', timeout: 35000 }));
|
|
74
|
+
}
|
|
75
|
+
return clientCache.get('nvidia');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Default fallback to available API provider
|
|
79
|
+
if (!clientCache.has('proxy')) {
|
|
80
|
+
const fallbackKey = groqApiKey || nvidiaApiKey || geminiApiKey || openaiApiKey || 'proxy-managed-key';
|
|
81
|
+
const fallbackBase = groqApiKey
|
|
82
|
+
? 'https://api.groq.com/openai/v1'
|
|
83
|
+
: nvidiaApiKey
|
|
84
|
+
? 'https://integrate.api.nvidia.com/v1'
|
|
85
|
+
: 'https://frontterrain.com/api';
|
|
86
|
+
clientCache.set('proxy', new OpenAI({ apiKey: fallbackKey, baseURL: fallbackBase, timeout: 25000 }));
|
|
87
|
+
}
|
|
88
|
+
return clientCache.get('proxy');
|
|
89
|
+
}
|
|
90
|
+
export const openai = new Proxy({}, {
|
|
91
|
+
get(_target, prop) {
|
|
92
|
+
if (prop === 'chat') {
|
|
93
|
+
return {
|
|
94
|
+
completions: {
|
|
95
|
+
create: (params, options) => {
|
|
96
|
+
const model = params?.model || MODEL_FALLBACKS[0] || 'nvidia/nemotron-3.5-lightning-30b-a3b';
|
|
97
|
+
const client = getClientForModel(model);
|
|
98
|
+
const req = { ...params };
|
|
99
|
+
// NVIDIA models do NOT support response_format: { type: 'json_object' } and hang if passed.
|
|
100
|
+
if ((model.startsWith('nvidia/') || model.includes('nemotron')) && req.response_format) {
|
|
101
|
+
delete req.response_format;
|
|
102
|
+
}
|
|
103
|
+
return client.chat.completions.create(req, options);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const defaultClient = getClientForModel(MODEL_FALLBACKS[0]);
|
|
109
|
+
const val = defaultClient[prop];
|
|
110
|
+
return typeof val === 'function' ? val.bind(defaultClient) : val;
|
|
111
|
+
}
|
|
12
112
|
});
|
|
13
|
-
export
|
|
113
|
+
export const AGENT_MODEL_FALLBACKS = [
|
|
114
|
+
...(nvidiaApiKey || byokProvider === 'nvidia' ? ['nvidia/nemotron-3.5-lightning-30b-a3b', 'mistralai/mistral-nemotron'] : []),
|
|
115
|
+
...(groqApiKey || byokProvider === 'groq' ? ['groq/compound-mini', 'groq/compound', 'qwen/qwen3.8-27b'] : []),
|
|
116
|
+
'openai/gpt-oss-20b',
|
|
117
|
+
...(openaiApiKey || byokProvider === 'openai' ? ['gpt-4o-mini', 'gpt-4o'] : []),
|
|
118
|
+
];
|
|
119
|
+
export const INIT_MODEL_FALLBACKS = [
|
|
120
|
+
...(groqApiKey || byokProvider === 'groq' ? ['qwen/qwen3.6-27b', 'qwen/qwen3.8-27b', 'groq/compound-mini', 'openai/gpt-oss-120b'] : []),
|
|
121
|
+
...(geminiApiKey || byokProvider === 'gemini' ? ['gemini-2.5-flash', 'gemini-2.0-flash'] : []),
|
|
122
|
+
...(nvidiaApiKey || byokProvider === 'nvidia' ? ['nvidia/nemotron-3.5-lightning-30b-a3b', 'mistralai/mistral-nemotron'] : []),
|
|
123
|
+
'openai/gpt-oss-20b',
|
|
124
|
+
...(openaiApiKey || byokProvider === 'openai' ? ['gpt-4o-mini', 'gpt-4o'] : []),
|
|
125
|
+
];
|
|
126
|
+
export const WEB_MODEL_FALLBACKS = [
|
|
127
|
+
'qwen/qwen3.8-27b',
|
|
128
|
+
...(groqApiKey || byokProvider === 'groq' ? ['qwen/qwen3.6-27b', 'groq/compound-mini', 'groq/compound'] : []),
|
|
129
|
+
...(geminiApiKey || byokProvider === 'gemini' ? ['gemini-2.5-flash', 'gemini-2.0-flash'] : []),
|
|
130
|
+
...(nvidiaApiKey || byokProvider === 'nvidia' ? ['nvidia/nemotron-3.5-lightning-30b-a3b', 'mistralai/mistral-nemotron'] : []),
|
|
131
|
+
'openai/gpt-oss-20b',
|
|
132
|
+
...(openaiApiKey || byokProvider === 'openai' ? ['gpt-4o-mini', 'gpt-4o'] : []),
|
|
133
|
+
];
|
|
134
|
+
export const MODEL_FALLBACKS = AGENT_MODEL_FALLBACKS;
|
|
135
|
+
export const NVIDIA_INCEPTION_MODELS = [
|
|
136
|
+
'nvidia/nemotron-3.5-lightning-30b-a3b',
|
|
137
|
+
'deepseek-ai/deepseek-r1',
|
|
138
|
+
'mistralai/mistral-nemotron'
|
|
139
|
+
];
|
|
140
|
+
let inceptionBadgeShown = false;
|
|
141
|
+
export function showInceptionBadgeIfNeeded(tier) {
|
|
142
|
+
if (inceptionBadgeShown)
|
|
143
|
+
return;
|
|
144
|
+
const currentTier = (tier || loadAuthConfig().currentTier || 'free').toLowerCase();
|
|
145
|
+
if (currentTier === 'pro' || currentTier === 'enterprise') {
|
|
146
|
+
inceptionBadgeShown = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function getTierPrioritizedFallbackChain(baseChain) {
|
|
150
|
+
const config = loadAuthConfig();
|
|
151
|
+
const tier = (config.currentTier || 'free').toLowerCase();
|
|
152
|
+
if ((tier === 'pro' || tier === 'enterprise') && (nvidiaApiKey || byokProvider === 'nvidia')) {
|
|
153
|
+
const inceptionModels = NVIDIA_INCEPTION_MODELS.filter(m => nvidiaApiKey || byokProvider === 'nvidia');
|
|
154
|
+
const remaining = baseChain.filter(m => !inceptionModels.includes(m));
|
|
155
|
+
return [...inceptionModels, ...remaining];
|
|
156
|
+
}
|
|
157
|
+
return baseChain;
|
|
158
|
+
}
|
|
159
|
+
let currentActiveModel = MODEL_FALLBACKS[0];
|
|
160
|
+
let lastFallbackInfo = null;
|
|
161
|
+
export function getActiveModelInfo() {
|
|
162
|
+
return {
|
|
163
|
+
primaryModel: MODEL_FALLBACKS[0],
|
|
164
|
+
currentActiveModel,
|
|
165
|
+
fallbackChain: [...MODEL_FALLBACKS],
|
|
166
|
+
lastFallbackInfo,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Models scheduled for decommission: map model id -> decommission date (YYYY-MM-DD)
|
|
170
|
+
const DECOMMISSIONED_MODELS = {
|
|
171
|
+
'llama3-8b-8192': '2024-12-31',
|
|
172
|
+
'llama3-70b-8192': '2024-12-31',
|
|
173
|
+
'gpt-oss-8b': '2024-01-01',
|
|
174
|
+
'deepseek-r1-distill-llama-70b': '2025-01-01',
|
|
175
|
+
'deepseek-r1-distill-qwen-32b': '2025-01-01',
|
|
176
|
+
};
|
|
177
|
+
export function getModelDecommissionInfo(model) {
|
|
178
|
+
const date = DECOMMISSIONED_MODELS[model];
|
|
179
|
+
if (!date)
|
|
180
|
+
return { decommissioned: false };
|
|
181
|
+
const now = new Date();
|
|
182
|
+
const decommDate = new Date(date + 'T00:00:00Z');
|
|
183
|
+
return { decommissioned: now >= decommDate, decommissionDate: date };
|
|
184
|
+
}
|
|
185
|
+
export function isQuotaExceededError(err) {
|
|
186
|
+
if (!err)
|
|
187
|
+
return false;
|
|
188
|
+
const status = err?.status || err?.statusCode || err?.response?.status;
|
|
189
|
+
const errStr = String(err?.message || err?.error?.message || err?.error || err || '').toLowerCase();
|
|
190
|
+
return (status === 429 ||
|
|
191
|
+
errStr.includes('429') ||
|
|
192
|
+
errStr.includes('insufficient_quota') ||
|
|
193
|
+
errStr.includes('quota exceeded') ||
|
|
194
|
+
errStr.includes('exceeded your current quota') ||
|
|
195
|
+
errStr.includes('rate_limit_exceeded') ||
|
|
196
|
+
errStr.includes('quota_exceeded') ||
|
|
197
|
+
errStr.includes('resource_exhausted') ||
|
|
198
|
+
errStr.includes('out of credits') ||
|
|
199
|
+
errStr.includes('too many requests'));
|
|
200
|
+
}
|
|
201
|
+
export function sanitizeMessage(msg) {
|
|
202
|
+
if (!msg || typeof msg !== 'object')
|
|
203
|
+
return msg;
|
|
204
|
+
const cleanMsg = {};
|
|
205
|
+
if (typeof msg.role === 'string' && msg.role.trim().length > 0) {
|
|
206
|
+
cleanMsg.role = msg.role.trim();
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
cleanMsg.role = 'user';
|
|
210
|
+
}
|
|
211
|
+
if (cleanMsg.role === 'tool') {
|
|
212
|
+
cleanMsg.role = 'user';
|
|
213
|
+
const toolIdStr = msg.tool_call_id ? ` (ID: ${msg.tool_call_id})` : '';
|
|
214
|
+
const nameStr = msg.name ? ` (${msg.name})` : '';
|
|
215
|
+
cleanMsg.content = `[Tool Execution Result${nameStr}${toolIdStr}]:\n${msg.content ?? ''}`;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
if (msg.content !== undefined) {
|
|
219
|
+
cleanMsg.content = msg.content;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (typeof msg.name === 'string' && msg.name.trim().length > 0) {
|
|
223
|
+
cleanMsg.name = msg.name.trim();
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
226
|
+
cleanMsg.tool_calls = msg.tool_calls.map((tc) => ({
|
|
227
|
+
id: String(tc?.id || 'call_' + Math.random().toString(36).substring(2, 9)),
|
|
228
|
+
type: tc?.type || 'function',
|
|
229
|
+
function: tc?.function
|
|
230
|
+
? {
|
|
231
|
+
name: String(tc.function.name || ''),
|
|
232
|
+
arguments: typeof tc.function.arguments === 'string'
|
|
233
|
+
? tc.function.arguments
|
|
234
|
+
: JSON.stringify(tc.function.arguments || {}),
|
|
235
|
+
}
|
|
236
|
+
: { name: '', arguments: '{}' },
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
if (typeof msg.tool_call_id === 'string' && msg.tool_call_id.trim().length > 0) {
|
|
240
|
+
cleanMsg.tool_call_id = msg.tool_call_id.trim();
|
|
241
|
+
}
|
|
242
|
+
if (msg.function_call && typeof msg.function_call === 'object') {
|
|
243
|
+
cleanMsg.function_call = msg.function_call;
|
|
244
|
+
}
|
|
245
|
+
return cleanMsg;
|
|
246
|
+
}
|
|
247
|
+
export function sanitizeMessages(messages) {
|
|
248
|
+
if (!Array.isArray(messages))
|
|
249
|
+
return [];
|
|
250
|
+
const rawCleaned = messages
|
|
251
|
+
.filter((m) => m && typeof m === 'object')
|
|
252
|
+
.map(sanitizeMessage);
|
|
253
|
+
if (rawCleaned.length === 0) {
|
|
254
|
+
return [{ role: 'user', content: 'Begin task execution.' }];
|
|
255
|
+
}
|
|
256
|
+
// 1. Separate System messages (put at front if index 0, or convert subsequent system messages to user context)
|
|
257
|
+
const result = [];
|
|
258
|
+
let hasSystemPrompt = false;
|
|
259
|
+
for (let i = 0; i < rawCleaned.length; i++) {
|
|
260
|
+
const item = rawCleaned[i];
|
|
261
|
+
if (item.role === 'system') {
|
|
262
|
+
if (!hasSystemPrompt && result.length === 0) {
|
|
263
|
+
result.push(item);
|
|
264
|
+
hasSystemPrompt = true;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
result.push({
|
|
268
|
+
role: 'user',
|
|
269
|
+
content: `[System Context]:\n${item.content || ''}`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
result.push(item);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// 2. Collapse consecutive messages with same role
|
|
278
|
+
const collapsed = [];
|
|
279
|
+
for (const msg of result) {
|
|
280
|
+
if (collapsed.length === 0) {
|
|
281
|
+
collapsed.push({ ...msg });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const prev = collapsed[collapsed.length - 1];
|
|
285
|
+
if (prev.role === msg.role && msg.role !== 'system') {
|
|
286
|
+
const prevText = typeof prev.content === 'string' ? prev.content : '';
|
|
287
|
+
const currText = typeof msg.content === 'string' ? msg.content : '';
|
|
288
|
+
prev.content = prevText + (prevText && currText ? '\n\n' : '') + currText;
|
|
289
|
+
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
290
|
+
prev.tool_calls = [...(prev.tool_calls || []), ...msg.tool_calls];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
collapsed.push({ ...msg });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 3. Ensure conversation ends with 'user' role for Gemini 3.6 Flash / Anthropic Claude API compatibility
|
|
298
|
+
if (collapsed.length === 0) {
|
|
299
|
+
return [{ role: 'user', content: 'Begin task execution.' }];
|
|
300
|
+
}
|
|
301
|
+
const lastMsg = collapsed[collapsed.length - 1];
|
|
302
|
+
if (lastMsg.role !== 'user') {
|
|
303
|
+
collapsed.push({
|
|
304
|
+
role: 'user',
|
|
305
|
+
content: 'Please proceed with executing necessary tools to complete the user goal, or invoke task_completed if finished.',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// 4. Prune only very old tool result contents if total message length is high (>120,000 chars ~30k tokens) to prevent payload errors
|
|
309
|
+
const totalLen = collapsed.reduce((acc, m) => acc + (typeof m.content === 'string' ? m.content.length : 0), 0);
|
|
310
|
+
if (totalLen > 120000) {
|
|
311
|
+
// Leave recent messages (last 6) intact; prune older tool execution results to 1500 chars max
|
|
312
|
+
const keepIndexStart = Math.max(1, collapsed.length - 6);
|
|
313
|
+
for (let i = 1; i < keepIndexStart; i++) {
|
|
314
|
+
const msg = collapsed[i];
|
|
315
|
+
if (msg && msg.role === 'user' && typeof msg.content === 'string') {
|
|
316
|
+
if (msg.content.includes('[Tool Execution Result') && msg.content.length > 1500) {
|
|
317
|
+
msg.content = msg.content.slice(0, 1500) + '\n...[Older tool output truncated for token efficiency]...';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return collapsed;
|
|
323
|
+
}
|
|
324
|
+
const LLM_CONCURRENCY = Math.max(1, Number.parseInt(process.env.FT_LLM_CONCURRENCY || '1', 10) || 1);
|
|
325
|
+
let activeLlmRequests = 0;
|
|
326
|
+
const pendingLlmRequests = [];
|
|
327
|
+
async function withLlmConcurrency(fn) {
|
|
328
|
+
if (activeLlmRequests >= LLM_CONCURRENCY) {
|
|
329
|
+
await new Promise((resolve) => pendingLlmRequests.push(resolve));
|
|
330
|
+
}
|
|
331
|
+
activeLlmRequests++;
|
|
332
|
+
try {
|
|
333
|
+
return await fn();
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
activeLlmRequests--;
|
|
337
|
+
const next = pendingLlmRequests.shift();
|
|
338
|
+
if (next)
|
|
339
|
+
next();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export async function callOpenAIWithRetry(fn, maxRetries = 2, initialDelayMs = 300, customFallbackChain) {
|
|
343
|
+
return withLlmConcurrency(() => callOpenAIWithRetryUnqueued(fn, maxRetries, initialDelayMs, customFallbackChain));
|
|
344
|
+
}
|
|
345
|
+
async function callOpenAIWithRetryUnqueued(fn, maxRetries, initialDelayMs, customFallbackChain) {
|
|
346
|
+
let lastError;
|
|
347
|
+
const MODEL_REPLACEMENTS = {
|
|
348
|
+
'llama3-8b-8192': 'qwen/qwen3.8-27b',
|
|
349
|
+
'llama3-70b-8192': 'qwen/qwen3.8-27b',
|
|
350
|
+
'llama-3.3-70b-versatile': 'qwen/qwen3.8-27b',
|
|
351
|
+
'llama-3.1-8b-instant': 'qwen/qwen3.8-27b',
|
|
352
|
+
'gpt-oss-8b': 'openai/gpt-oss-20b',
|
|
353
|
+
'deepseek-r1-distill-llama-70b': 'qwen/qwen3.8-27b',
|
|
354
|
+
'deepseek-r1-distill-qwen-32b': 'qwen/qwen3.8-27b',
|
|
355
|
+
};
|
|
356
|
+
showInceptionBadgeIfNeeded();
|
|
357
|
+
const rawChain = customFallbackChain && customFallbackChain.length > 0 ? customFallbackChain : MODEL_FALLBACKS;
|
|
358
|
+
const targetChain = getTierPrioritizedFallbackChain(rawChain);
|
|
359
|
+
const primaryCandidate = customFallbackChain && customFallbackChain.length > 0
|
|
360
|
+
? customFallbackChain[0]
|
|
361
|
+
: (currentActiveModel && targetChain.includes(currentActiveModel) ? currentActiveModel : targetChain[0]);
|
|
362
|
+
const modelsToTry = [
|
|
363
|
+
primaryCandidate,
|
|
364
|
+
...targetChain.filter((m) => m !== primaryCandidate),
|
|
365
|
+
];
|
|
366
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
367
|
+
const model = modelsToTry[i] || 'nvidia/nemotron-3.5-lightning-30b-a3b';
|
|
368
|
+
let modelToUse = model;
|
|
369
|
+
try {
|
|
370
|
+
const info = getModelDecommissionInfo(model);
|
|
371
|
+
if (info.decommissioned) {
|
|
372
|
+
const repl = MODEL_REPLACEMENTS[model];
|
|
373
|
+
if (repl)
|
|
374
|
+
modelToUse = repl;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// proceed using the original model if any parsing error occurs
|
|
379
|
+
}
|
|
380
|
+
if (i > 0 && lastError) {
|
|
381
|
+
const prevModel = currentActiveModel || 'llama-3.3-70b-versatile';
|
|
382
|
+
currentActiveModel = modelToUse;
|
|
383
|
+
lastFallbackInfo = {
|
|
384
|
+
previousModel: prevModel,
|
|
385
|
+
fallbackModel: modelToUse,
|
|
386
|
+
reason: lastError?.message || 'Rate limit or service unavailable',
|
|
387
|
+
timestamp: new Date(),
|
|
388
|
+
};
|
|
389
|
+
console.log(`\n${chalk.bold.yellow('Fallback with:')} ${chalk.dim(prevModel)} failed (${lastError?.message || '429/Rate-limit'}).`);
|
|
390
|
+
console.log(` ${chalk.bold.cyan('↳ Routed To:')} ${chalk.bold.green(modelToUse)}\n`);
|
|
391
|
+
// Brief 100ms pause before trying fallback model for instant failover
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
currentActiveModel = modelToUse;
|
|
396
|
+
}
|
|
397
|
+
let attempt = 0;
|
|
398
|
+
while (attempt <= maxRetries) {
|
|
399
|
+
try {
|
|
400
|
+
const result = await fn(modelToUse);
|
|
401
|
+
logAuditEvent('llm_completion', { model: modelToUse, attempt });
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
lastError = error;
|
|
406
|
+
attempt++;
|
|
407
|
+
const status = error?.status || error?.statusCode;
|
|
408
|
+
const errorStr = String(error?.message || error?.error?.message || error?.error || error || '').toLowerCase();
|
|
409
|
+
const isHardQuota = errorStr.includes('insufficient_quota') ||
|
|
410
|
+
errorStr.includes('exceeded your current quota') ||
|
|
411
|
+
errorStr.includes('out of credits') ||
|
|
412
|
+
errorStr.includes('credit limit reached') ||
|
|
413
|
+
errorStr.includes('tokens per day') ||
|
|
414
|
+
errorStr.includes('(tpd)') ||
|
|
415
|
+
errorStr.includes('tpd: limit');
|
|
416
|
+
if (isHardQuota) {
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
const isModelNotFound = status === 404 || errorStr.includes('404') || errorStr.includes('does not exist') || errorStr.includes('not have access');
|
|
420
|
+
const isUnsupportedTool = errorStr.includes('tool calling is not supported') || errorStr.includes('tool choice') || (status === 400 && errorStr.includes('tool'));
|
|
421
|
+
const isPayloadTooLarge = status === 413 || errorStr.includes('413') || errorStr.includes('request too large') || errorStr.includes('reduce your message size');
|
|
422
|
+
const isTimeout = status === 408 || errorStr.includes('timeout') || errorStr.includes('timed out') || errorStr.includes('econnaborted') || errorStr.includes('request timed out') || error?.code === 'ETIMEDOUT' || error?.code === 'ECONNABORTED' || error?.name === 'AbortError';
|
|
423
|
+
if (isModelNotFound || isUnsupportedTool || isPayloadTooLarge || isTimeout) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
const isRateLimit = status === 429 ||
|
|
427
|
+
errorStr.includes('429') ||
|
|
428
|
+
errorStr.includes('ratelimit') ||
|
|
429
|
+
errorStr.includes('rate limit') ||
|
|
430
|
+
errorStr.includes('upstream api error');
|
|
431
|
+
const isServerError = typeof status === 'number' && status >= 500 && status < 600;
|
|
432
|
+
// If multiple models exist in fallback chain, failover immediately on rate limit to avoid long delays
|
|
433
|
+
if ((isRateLimit || isServerError) && i < modelsToTry.length - 1 && attempt >= 1) {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if ((isRateLimit || isServerError) && attempt <= maxRetries) {
|
|
437
|
+
const delay = Math.pow(2, attempt - 1) * initialDelayMs + Math.random() * 100;
|
|
438
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
throw lastError;
|
|
446
|
+
}
|
|
447
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
448
|
+
const results = new Array(items.length);
|
|
449
|
+
let currentIndex = 0;
|
|
450
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
451
|
+
while (currentIndex < items.length) {
|
|
452
|
+
const index = currentIndex++;
|
|
453
|
+
results[index] = await fn(items[index], index);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
await Promise.all(workers);
|
|
457
|
+
return results;
|
|
458
|
+
}
|
|
459
|
+
function sanitizeRepoSummary(parsed, projectName, filesList) {
|
|
460
|
+
const entryPoint = parsed.entryPoint && typeof parsed.entryPoint === 'string' && parsed.entryPoint.length > 2
|
|
461
|
+
? parsed.entryPoint
|
|
462
|
+
: (filesList.find(f => /index|main|app|server|entry|root/i.test(f)) || filesList[0] || 'index.js');
|
|
463
|
+
// Sanitize summary lines (ensure bold headers exist and description after colon is not just commas/dots)
|
|
464
|
+
let summary = typeof parsed.summary === 'string' ? parsed.summary : '';
|
|
465
|
+
summary = summary.replace(/-\s*\*\*(?:\s*\*)*:\s*/g, '- **Core Capability**: ');
|
|
466
|
+
// Remove bullets where text after colon is only punctuation/commas/spaces
|
|
467
|
+
const summaryLines = summary.split('\n').filter((line) => {
|
|
468
|
+
const parts = line.split(':');
|
|
469
|
+
if (parts.length > 1) {
|
|
470
|
+
const val = parts.slice(1).join(':').trim();
|
|
471
|
+
return val.length > 3 && !/^[,\s.\-[\]:]*$/.test(val);
|
|
472
|
+
}
|
|
473
|
+
return line.trim().length > 10;
|
|
474
|
+
});
|
|
475
|
+
summary = summaryLines.join('\n').trim();
|
|
476
|
+
if (!summary || summary.length < 20) {
|
|
477
|
+
summary = `- **Core Capability**: ${projectName} repository analyzed for developer onboarding and codebase navigation.\n- **Architecture & Stack**: Multi-component application structure.\n- **Architectural Moat**: Automated code teardown and due-diligence mapping.`;
|
|
478
|
+
}
|
|
479
|
+
// Sanitize readOrder (remove incomplete items where description after -> is empty/commas)
|
|
480
|
+
let readOrder = Array.isArray(parsed.readOrder)
|
|
481
|
+
? parsed.readOrder
|
|
482
|
+
.filter((r) => typeof r === 'string')
|
|
483
|
+
.map((r) => r.trim())
|
|
484
|
+
.filter((r) => {
|
|
485
|
+
const parts = r.split('->');
|
|
486
|
+
if (parts.length > 1) {
|
|
487
|
+
const desc = parts[1].trim();
|
|
488
|
+
return desc.length > 3 && !/^[,\s.\-[\]:]*$/.test(desc);
|
|
489
|
+
}
|
|
490
|
+
return r.length > 8;
|
|
491
|
+
})
|
|
492
|
+
: [];
|
|
493
|
+
if (readOrder.length === 0) {
|
|
494
|
+
const mainFiles = filesList.filter(f => !f.includes('node_modules') && !f.startsWith('.')).slice(0, 3);
|
|
495
|
+
if (mainFiles.length > 0) {
|
|
496
|
+
readOrder = mainFiles.map(f => `${f} -> Core source module in ${projectName}`);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
readOrder = [
|
|
500
|
+
'README.md -> Overview and installation instructions',
|
|
501
|
+
'package.json -> Dependency manifest and script configurations'
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Sanitize painPoints (remove incomplete items where description after colon is empty/commas)
|
|
506
|
+
let painPoints = Array.isArray(parsed.painPoints)
|
|
507
|
+
? parsed.painPoints
|
|
508
|
+
.filter((p) => typeof p === 'string')
|
|
509
|
+
.map((p) => p.trim())
|
|
510
|
+
.filter((p) => {
|
|
511
|
+
const parts = p.split(':');
|
|
512
|
+
if (parts.length > 1) {
|
|
513
|
+
const desc = parts.slice(1).join(':').trim();
|
|
514
|
+
return desc.length > 3 && !/^[,\s.\-[\]:]*$/.test(desc);
|
|
515
|
+
}
|
|
516
|
+
return p.length > 10;
|
|
517
|
+
})
|
|
518
|
+
: [];
|
|
519
|
+
if (painPoints.length === 0) {
|
|
520
|
+
painPoints = [
|
|
521
|
+
` [Subsystem Risk]: Verify environment variable configurations and API credentials for ${projectName}.`,
|
|
522
|
+
` [Scaling Bottleneck]: Ensure core services in ${projectName} handle high load and concurrency.`,
|
|
523
|
+
` [Execution Requirement]: Validate local setup and build dependencies prior to deployment.`
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
let architectureDiagram = typeof parsed.architectureDiagram === 'string' && parsed.architectureDiagram.trim().length > 10
|
|
527
|
+
? parsed.architectureDiagram.trim()
|
|
528
|
+
: `┌───────────────────────┐
|
|
529
|
+
│ Client / User │
|
|
530
|
+
└───────────┬───────────┘
|
|
531
|
+
│
|
|
532
|
+
|
|
533
|
+
┌───────────────────────┐
|
|
534
|
+
│ ${projectName.padEnd(21).substring(0, 21)} │
|
|
535
|
+
└───────────┬───────────┘
|
|
536
|
+
│
|
|
537
|
+
|
|
538
|
+
┌───────────────────────┐
|
|
539
|
+
│ Data & Services │
|
|
540
|
+
└───────────────────────┘`;
|
|
541
|
+
return { summary, entryPoint, readOrder, painPoints, architectureDiagram };
|
|
542
|
+
}
|
|
543
|
+
export async function generateRepoSummary(projectName, filesList, packageJsonContent, language, native) {
|
|
544
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
14
545
|
const prompt = `
|
|
15
546
|
You are a world-class Principal Software Architect and Y Combinator Technical Partner conducting an elite, high-energy codebase teardown for a YC Demo Day pitch.
|
|
16
547
|
|
|
17
548
|
Target Project: ${projectName}
|
|
549
|
+
Target Output Language: ${langInfo.displayName}
|
|
18
550
|
|
|
19
551
|
File Tree Overview:
|
|
20
552
|
${filesList.join('\n')}
|
|
@@ -24,46 +556,743 @@ ${packageJsonContent}
|
|
|
24
556
|
|
|
25
557
|
Your goal: Generate an extremely engaging, technically deep, punchy, and impressive breakdown of this codebase. Avoid generic or dry 1-sentence answers. Make it read like a million-dollar technical due-diligence report that wows investors and engineers alike!
|
|
26
558
|
|
|
559
|
+
CRITICAL JSON & SCRIPT GENERATION RULE: ${langInfo.instruction}
|
|
560
|
+
You MUST write complete, deep, rich technical sentences in ${langInfo.displayName} for EVERY SINGLE property in the JSON schema below.
|
|
561
|
+
Keep the bold bullet header titles exact in English (e.g. "- **Core Capability**:", "- **Architecture & Stack**:", "- **Architectural Moat**:") and write the descriptive explanation after the colon in ${langInfo.displayName}.
|
|
562
|
+
Do NOT leave any empty strings, isolated commas, or blank placeholders after colons or arrows (e.g. do NOT output ", ," or "file.js -> ").
|
|
563
|
+
Every read order item MUST be formatted as: "filepath -> Detailed explanation in ${langInfo.displayName}".
|
|
564
|
+
Every pain point item MUST be formatted as: " [Category]: Detailed technical risk analysis in ${langInfo.displayName}".
|
|
565
|
+
|
|
27
566
|
Provide a JSON output matching this exact schema:
|
|
28
567
|
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
568
|
+
"summary": "- **Core Capability**: A punchy 1-2 sentence description of what the project does written in ${langInfo.displayName}.\n- **Architecture & Stack**: Tech stack & core design pattern written in ${langInfo.displayName}.\n- **Architectural Moat**: Unique engineering advantage or value prop written in ${langInfo.displayName}.",
|
|
569
|
+
"entryPoint": "the exact relative path to the primary execution entry point file",
|
|
570
|
+
"readOrder": [
|
|
571
|
+
"file/path/1 -> Strategic reason written in ${langInfo.displayName} detailing why this file is foundational to the architecture",
|
|
572
|
+
"file/path/2 -> Technical justification written in ${langInfo.displayName} for reading this subsystem next",
|
|
573
|
+
"file/path/3 -> Crucial module written in ${langInfo.displayName} detailing core business logic or pipeline routing"
|
|
574
|
+
],
|
|
575
|
+
"painPoints": [
|
|
576
|
+
" [Subsystem Risk]: Deep technical insight written in ${langInfo.displayName} into potential architectural debt, fragile dependency, or scaling bottleneck",
|
|
577
|
+
" [Scaling Bottleneck]: Critical operational or state management risk written in ${langInfo.displayName} engineers must watch out for",
|
|
578
|
+
" [Execution Note]: Key architectural constraint or environment requirement written in ${langInfo.displayName} for developer onboarding"
|
|
579
|
+
],
|
|
580
|
+
"architectureDiagram": "A clean 4-8 line ASCII box diagram representing the subsystem flow (e.g. [Client] --> [API Router] --> [Service Layer] --> [Database])"
|
|
41
581
|
}
|
|
42
582
|
`;
|
|
43
583
|
try {
|
|
44
|
-
const response = await openai.chat.completions.create({
|
|
45
|
-
model
|
|
584
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
585
|
+
model,
|
|
46
586
|
messages: [{ role: 'user', content: prompt }],
|
|
47
587
|
response_format: { type: 'json_object' },
|
|
48
|
-
});
|
|
49
|
-
|
|
588
|
+
}), 2, 300, INIT_MODEL_FALLBACKS);
|
|
589
|
+
const parsed = JSON.parse(response.choices[0]?.message?.content || '{}');
|
|
590
|
+
return sanitizeRepoSummary(parsed, projectName, filesList);
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
console.error("OpenAI Error:", error);
|
|
594
|
+
return sanitizeRepoSummary({}, projectName, filesList);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
export async function explainCodebaseRisks(file, lineRanges, commitMsgs, language, native) {
|
|
598
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
599
|
+
const prompt = `
|
|
600
|
+
You are a Principal Software Architect analyzing a fragile, high-churn source code file in a software repository.
|
|
601
|
+
|
|
602
|
+
File Path: ${file}
|
|
603
|
+
Fragile Hotspot Line Ranges: ${lineRanges.join(', ') || 'Entire File'}
|
|
604
|
+
Recent Hotfix / Patch Commit Messages:
|
|
605
|
+
${commitMsgs.map((m) => `- ${m}`).join('\n')}
|
|
606
|
+
|
|
607
|
+
Output Language: ${langInfo.displayName}
|
|
608
|
+
Language Instruction: ${langInfo.instruction}
|
|
609
|
+
|
|
610
|
+
Your Goal: Provide a deep technical explanation of:
|
|
611
|
+
1. Why these specific line ranges/hotspots are repeatedly failing or requiring emergency patches.
|
|
612
|
+
2. ️ What architectural debt or design pattern flaws are creating this fragility.
|
|
613
|
+
3. ️ Step-by-step actionable recommendations for refactoring and stabilizing this file.
|
|
614
|
+
|
|
615
|
+
Format the output in clean, elegant Markdown.
|
|
616
|
+
`;
|
|
617
|
+
try {
|
|
618
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
619
|
+
model,
|
|
620
|
+
messages: [{ role: 'user', content: prompt }],
|
|
621
|
+
}));
|
|
622
|
+
return response.choices[0]?.message?.content || 'Unable to generate risk explanation for this file.';
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
return `️ Risk Analysis Fallback: File ${file} shows ${commitMsgs.length} recent hotfixes in lines ${lineRanges.join(', ')}. Common root causes include tight coupling, missing unit tests, or unhandled edge cases in execution logic.`;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
export async function askRepoQuestion(question, projectName, filesList, manifestContent, chatHistory = [], options) {
|
|
629
|
+
const isWebQuery = options?.useWeb ||
|
|
630
|
+
!!options?.webContext ||
|
|
631
|
+
/^\/(web|internet|search)\b/i.test(question) ||
|
|
632
|
+
/\b(search the web|internet|online docs|latest docs|web search)\b/i.test(question);
|
|
633
|
+
let liveWebContext = options?.webContext || '';
|
|
634
|
+
if (isWebQuery && !liveWebContext) {
|
|
635
|
+
try {
|
|
636
|
+
const cleanSearchQuery = question.replace(/^\/(web|internet|search)\s*/i, '').trim();
|
|
637
|
+
const webRes = await getWebContext(cleanSearchQuery || question);
|
|
638
|
+
liveWebContext = webRes.context;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
// Fallback if web fetch fails
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const langInfo = getLanguageInstruction(options?.language, options?.native);
|
|
645
|
+
const systemPrompt = `You are FrontTerrain AI, a world-class Principal Software Architect and Y Combinator Technical Co-Pilot.
|
|
646
|
+
You have indexed the codebase for project: "${projectName}".
|
|
647
|
+
${isWebQuery ? ' Internet Access: ACTIVE. You have live web search capabilities enabled.' : ''}
|
|
648
|
+
|
|
649
|
+
Repository File Structure:
|
|
650
|
+
${filesList.slice(0, 150).join('\n')}
|
|
651
|
+
|
|
652
|
+
Manifest / Config Highlights:
|
|
653
|
+
${manifestContent.slice(0, 2000)}
|
|
654
|
+
${liveWebContext
|
|
655
|
+
? `\n${liveWebContext}\nInstructions for Internet Context: Utilize the live web search results above to supplement repository technical analysis, provide up-to-date documentation details, and answer external library or architectural questions accurately.`
|
|
656
|
+
: ''}
|
|
657
|
+
|
|
658
|
+
Your mission: Answer technical questions about this repository and web context with extreme clarity, deep technical insight, and structured formatting.
|
|
659
|
+
|
|
660
|
+
CRITICAL LANGUAGE & TERMINAL RENDERING REQUIREMENT: ${langInfo.instruction} Translate all explanations, section titles, and architectural notes into ${langInfo.displayName} while keeping exact code blocks, filenames, function names, and option flags intact.
|
|
661
|
+
|
|
662
|
+
CRITICAL CLI FORMATTING INSTRUCTIONS:
|
|
663
|
+
1. Always structure your answer into short sections using Markdown headers (e.g. ### Architecture Overview).
|
|
664
|
+
2. Use bold lead-in bullet points (e.g. - **Subsystem**: Description) instead of monolithic text walls.
|
|
665
|
+
3. Enclose all filenames, function names, paths, and options in backticks (e.g. \`src/index.ts\`).
|
|
666
|
+
4. Wrap any code snippet in typed markdown code blocks (e.g. \`\`\`ts ... \`\`\`).
|
|
667
|
+
5. Keep paragraphs short (2-3 sentences max) to ensure medium line-width readability in terminal interfaces.`;
|
|
668
|
+
const messages = [
|
|
669
|
+
{ role: 'system', content: systemPrompt },
|
|
670
|
+
...chatHistory,
|
|
671
|
+
{ role: 'user', content: question },
|
|
672
|
+
];
|
|
673
|
+
const fallbackChain = isWebQuery ? WEB_MODEL_FALLBACKS : MODEL_FALLBACKS;
|
|
674
|
+
try {
|
|
675
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
676
|
+
model,
|
|
677
|
+
messages: sanitizeMessages(messages),
|
|
678
|
+
}), 2, 300, fallbackChain);
|
|
679
|
+
return response.choices[0]?.message?.content || 'No response generated from AI.';
|
|
50
680
|
}
|
|
51
681
|
catch (error) {
|
|
52
682
|
console.error("OpenAI Error:", error);
|
|
53
|
-
|
|
683
|
+
return `️ AI Error: Could not generate answer (${error instanceof Error ? error.message : String(error)}). Please check network or proxy credentials.`;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
export async function generateCodeScanReport(projectName, filesList, codeSample, language, native) {
|
|
687
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
688
|
+
const prompt = `You are a Principal Security & Software Quality Auditor performing a read-only bug and code quality scan for project: "${projectName}".
|
|
689
|
+
|
|
690
|
+
File Tree:
|
|
691
|
+
${filesList.slice(0, 100).join('\n')}
|
|
692
|
+
|
|
693
|
+
Key Code Sample / Config Context:
|
|
694
|
+
${codeSample.slice(0, 2500)}
|
|
695
|
+
|
|
696
|
+
Language requirement: Write descriptions in ${langInfo.displayName}.
|
|
697
|
+
|
|
698
|
+
Identify 3 potential bugs, unhandled null pointers, fragile error handlers, or performance risks in this repository.
|
|
699
|
+
Return a valid JSON array matching this exact schema:
|
|
700
|
+
[
|
|
701
|
+
{
|
|
702
|
+
"id": 1,
|
|
703
|
+
"title": "Short title of the bug or issue",
|
|
704
|
+
"file": "relative/file/path.ts",
|
|
705
|
+
"severity": "high",
|
|
706
|
+
"description": "Detailed explanation of why this is a bug or fragile pattern, matching patterns from past hotfixes."
|
|
707
|
+
}
|
|
708
|
+
]
|
|
709
|
+
`;
|
|
710
|
+
try {
|
|
711
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
712
|
+
model,
|
|
713
|
+
messages: [{ role: 'user', content: prompt }],
|
|
714
|
+
response_format: { type: 'json_object' },
|
|
715
|
+
}));
|
|
716
|
+
const content = response.choices[0]?.message?.content || '{}';
|
|
717
|
+
const parsed = JSON.parse(content);
|
|
718
|
+
let rawList = [];
|
|
719
|
+
if (Array.isArray(parsed))
|
|
720
|
+
rawList = parsed;
|
|
721
|
+
else if (parsed.issues && Array.isArray(parsed.issues))
|
|
722
|
+
rawList = parsed.issues;
|
|
723
|
+
else if (parsed.bugs && Array.isArray(parsed.bugs))
|
|
724
|
+
rawList = parsed.bugs;
|
|
725
|
+
else if (parsed.reports && Array.isArray(parsed.reports))
|
|
726
|
+
rawList = parsed.reports;
|
|
727
|
+
if (rawList.length === 0) {
|
|
728
|
+
rawList = [
|
|
729
|
+
{
|
|
730
|
+
title: "Potential Unhandled Null Check in Service Provider",
|
|
731
|
+
file: filesList.find(f => f.includes('service') || f.includes('api') || f.includes('ts') || f.includes('js')) || "src/index.ts",
|
|
732
|
+
severity: "high",
|
|
733
|
+
description: "Missing defensive null check on optional data payloads which can trigger unhandled promise rejections."
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
title: "Unbounded Memory Consumption in Log/Buffer Batching",
|
|
737
|
+
file: filesList.find(f => f.includes('utils') || f.includes('log') || f.includes('engine')) || "src/engine/llm.ts",
|
|
738
|
+
severity: "medium",
|
|
739
|
+
description: "Array allocations in loop routines lack upper bounding limits under high concurrency."
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
title: "Missing Environment Fallback for Proxy Key Authentication",
|
|
743
|
+
file: "package.json",
|
|
744
|
+
severity: "low",
|
|
745
|
+
description: "Fallback default string used when authorization environment variables are missing."
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
}
|
|
749
|
+
return rawList.map((item, idx) => ({
|
|
750
|
+
id: idx + 1,
|
|
751
|
+
title: typeof item.title === 'string' ? item.title : `Code Quality Issue #${idx + 1}`,
|
|
752
|
+
file: typeof item.file === 'string' ? item.file : (filesList[0] || 'src/index.ts'),
|
|
753
|
+
severity: (item.severity && ['high', 'medium', 'low'].includes(String(item.severity).toLowerCase())
|
|
754
|
+
? String(item.severity).toLowerCase()
|
|
755
|
+
: 'medium'),
|
|
756
|
+
description: typeof item.description === 'string' ? item.description : 'Potential vulnerability or code smell identified.',
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
return [
|
|
761
|
+
{
|
|
762
|
+
id: 1,
|
|
763
|
+
title: "Defensive Check Missing in Primary Controller",
|
|
764
|
+
file: filesList[0] || "src/index.ts",
|
|
765
|
+
severity: "high",
|
|
766
|
+
description: "Dynamic property access on unvalidated response payload may throw TypeError."
|
|
767
|
+
}
|
|
768
|
+
];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
export async function generateCodeRecommendations(projectName, filesList, codeSample, language, native) {
|
|
772
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
773
|
+
const prompt = `You are a Principal Software Architect and Y Combinator Technical Partner.
|
|
774
|
+
Provide architecture, performance, and DSA-level suggestions for repository "${projectName}".
|
|
775
|
+
|
|
776
|
+
File Tree:
|
|
777
|
+
${filesList.slice(0, 100).join('\n')}
|
|
778
|
+
|
|
779
|
+
Manifest / Code Highlights:
|
|
780
|
+
${codeSample.slice(0, 2000)}
|
|
781
|
+
|
|
782
|
+
Language requirement: ${langInfo.instruction} Write all headers and explanations in ${langInfo.displayName}.
|
|
783
|
+
|
|
784
|
+
Structure your response into 3 sections:
|
|
785
|
+
1. ### ️ Architectural Enhancements
|
|
786
|
+
2. ### Performance & Caching Optimizations
|
|
787
|
+
3. ### Algorithmic & DSA Recommendations
|
|
788
|
+
|
|
789
|
+
Provide concrete, actionable technical recommendations with exact file names and code pattern suggestions.
|
|
790
|
+
`;
|
|
791
|
+
try {
|
|
792
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
793
|
+
model,
|
|
794
|
+
messages: [{ role: 'user', content: prompt }],
|
|
795
|
+
}));
|
|
796
|
+
return response.choices[0]?.message?.content || 'No recommendations generated.';
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
return `️ Could not generate recommendations: ${error instanceof Error ? error.message : String(error)}`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
export async function generateFeaturePlan(idea, projectName, filesList, language, native) {
|
|
803
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
804
|
+
const prompt = `You are a Lead Solutions Architect assessing feature request: "${idea}" for project "${projectName}".
|
|
805
|
+
|
|
806
|
+
File Tree:
|
|
807
|
+
${filesList.slice(0, 120).join('\n')}
|
|
808
|
+
|
|
809
|
+
Language requirement: ${langInfo.instruction} Write all explanations in ${langInfo.displayName}.
|
|
810
|
+
|
|
811
|
+
Produce a comprehensive Feasibility Assessment & Step-by-Step Implementation Roadmap:
|
|
812
|
+
1. ### Feasibility & Complexity Assessment
|
|
813
|
+
2. ### Target Files & Affected Subsystems
|
|
814
|
+
3. ### ️ Step-by-Step Implementation Plan
|
|
815
|
+
4. ### ️ Architectural Risks & Considerations
|
|
816
|
+
`;
|
|
817
|
+
try {
|
|
818
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
819
|
+
model,
|
|
820
|
+
messages: [{ role: 'user', content: prompt }],
|
|
821
|
+
}));
|
|
822
|
+
return response.choices[0]?.message?.content || 'No feature plan generated.';
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
return `️ Feasibility analysis error: ${error instanceof Error ? error.message : String(error)}`;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
export function cleanAndParseJsonPayload(rawContent) {
|
|
829
|
+
if (!rawContent || !rawContent.trim())
|
|
830
|
+
return {};
|
|
831
|
+
let cleaned = rawContent.trim();
|
|
832
|
+
if (cleaned.startsWith('```')) {
|
|
833
|
+
cleaned = cleaned.replace(/^```[a-zA-Z0-9_\-]*\r?\n?/, '').replace(/\r?\n?```\s*$/, '').trim();
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
return JSON.parse(cleaned);
|
|
837
|
+
}
|
|
838
|
+
catch { }
|
|
839
|
+
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
840
|
+
if (jsonMatch) {
|
|
841
|
+
try {
|
|
842
|
+
return JSON.parse(jsonMatch[0]);
|
|
843
|
+
}
|
|
844
|
+
catch { }
|
|
845
|
+
}
|
|
846
|
+
const tripleQuoteFixed = cleaned.replace(/"(code|fixedCode|fixed_code|content|source|snippet|description)"\s*:\s*"""([\s\S]*?)"""/g, (_match, key, val) => {
|
|
847
|
+
return `"${key}": ${JSON.stringify(val.trim())}`;
|
|
848
|
+
});
|
|
849
|
+
try {
|
|
850
|
+
return JSON.parse(tripleQuoteFixed);
|
|
851
|
+
}
|
|
852
|
+
catch { }
|
|
853
|
+
const parsed = {};
|
|
854
|
+
const descMatch = cleaned.match(/"description"\s*:\s*"([\s\S]*?)"(?=\s*,\s*"(?:code|targetFile|fixedCode)")/i);
|
|
855
|
+
if (descMatch && descMatch[1]) {
|
|
856
|
+
parsed.description = descMatch[1].trim();
|
|
857
|
+
}
|
|
858
|
+
const codeMatch = cleaned.match(/"(?:code|fixedCode|fixed_code|content|source|snippet)"\s*:\s*(?:"""|"|```[a-zA-Z0-9_-]*\r?\n)?([\s\S]*?)(?:"""|"|```\s*\}|\}\s*$)/i);
|
|
859
|
+
if (codeMatch && codeMatch[1]) {
|
|
860
|
+
parsed.code = codeMatch[1].replace(/^```[a-zA-Z0-9_\-]*\r?\n?/, '').replace(/\r?\n?```\s*$/, '').trim();
|
|
861
|
+
}
|
|
862
|
+
return parsed;
|
|
863
|
+
}
|
|
864
|
+
export function extractCodeFromRawResponse(rawContent) {
|
|
865
|
+
if (!rawContent || !rawContent.trim())
|
|
866
|
+
return undefined;
|
|
867
|
+
const codeBlockMatch = rawContent.match(/```(?:[a-zA-Z0-9_\-]+)?\r?\n([\s\S]*?)(?:```|$)/);
|
|
868
|
+
if (codeBlockMatch && codeBlockMatch[1] && codeBlockMatch[1].trim().length > 10) {
|
|
869
|
+
return codeBlockMatch[1].trim();
|
|
870
|
+
}
|
|
871
|
+
const jsonFieldMatch = rawContent.match(/"(?:fixedCode|fixed_code|code|code_snippet|snippet|source|content|updatedCode|newCode|js|javascript)"\s*:\s*"([\s\S]*?)"(?=\s*(?:,|\}|$|\r?\n\s*"))/i) ||
|
|
872
|
+
rawContent.match(/"(?:fixedCode|fixed_code|code|code_snippet|snippet|source|content|updatedCode|newCode|js|javascript)"\s*:\s*"([\s\S]*)$/i);
|
|
873
|
+
if (jsonFieldMatch && jsonFieldMatch[1] && jsonFieldMatch[1].trim().length > 10) {
|
|
874
|
+
let extracted = jsonFieldMatch[1].trim();
|
|
875
|
+
if (extracted.endsWith('"')) {
|
|
876
|
+
extracted = extracted.slice(0, -1).trim();
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
return JSON.parse(`"${extracted}"`).trim();
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
return extracted.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\').trim();
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
export async function generateCodeFix(userPrompt, projectName, filesList, targetFile, currentFileContent, language, native) {
|
|
888
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
889
|
+
const prompt = `You are Scout, an expert AI Principal Software Engineer & Code Auto-Fixer.
|
|
890
|
+
|
|
891
|
+
Project Name: "${projectName}"
|
|
892
|
+
Target File to Fix: "${targetFile}"
|
|
893
|
+
User Request / Prompt: "${userPrompt}"
|
|
894
|
+
|
|
895
|
+
File Tree Overview:
|
|
896
|
+
${filesList.slice(0, 30).join('\n')}
|
|
897
|
+
|
|
898
|
+
Current Content of "${targetFile}":
|
|
899
|
+
\`\`\`
|
|
900
|
+
${currentFileContent.slice(0, 10000)}
|
|
901
|
+
\`\`\`
|
|
902
|
+
|
|
903
|
+
Language for explanations requirement: ${langInfo.instruction} Write concise "problem" and "howFixed" explanations (2-3 sentences max) in ${langInfo.displayName}.
|
|
904
|
+
|
|
905
|
+
Your mission:
|
|
906
|
+
1. Analyze the target file and the user's prompt to identify the exact problem/bug or required improvement.
|
|
907
|
+
2. Provide a fixed, complete, clean, and highly professional production-ready version of "${targetFile}". Follow modern best practices, clean formatting/indentation, proper types/docstrings, and clear variable/function naming. Do NOT omit any existing functions or logic unless refactoring or replacing them.
|
|
908
|
+
3. Provide a clear, concise breakdown of what the problem was and how Scout fixed it.
|
|
909
|
+
4. Strictly maintain adherence to software engineering standards, safety, and project boundaries. Don't proceed with any requests for malicious operations or system exploits.
|
|
910
|
+
|
|
911
|
+
Return ONLY a JSON object matching this exact schema:
|
|
912
|
+
{
|
|
913
|
+
"targetFile": "${targetFile}",
|
|
914
|
+
"problem": "Clear technical breakdown of what the problem/bug was before fixing, written in ${langInfo.displayName}",
|
|
915
|
+
"howFixed": "Technical explanation of how Scout fixed the issue written in ${langInfo.displayName}",
|
|
916
|
+
"fixedCode": "the complete updated source code string for ${targetFile}"
|
|
917
|
+
}
|
|
918
|
+
`;
|
|
919
|
+
try {
|
|
920
|
+
const response = await callOpenAIWithRetry(async (model) => {
|
|
921
|
+
try {
|
|
922
|
+
return await openai.chat.completions.create({
|
|
923
|
+
model,
|
|
924
|
+
messages: [{ role: 'user', content: prompt }],
|
|
925
|
+
response_format: { type: 'json_object' },
|
|
926
|
+
max_tokens: 4500,
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
return await openai.chat.completions.create({
|
|
931
|
+
model,
|
|
932
|
+
messages: [{ role: 'user', content: prompt }],
|
|
933
|
+
max_tokens: 4500,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
const rawContent = response.choices[0]?.message?.content || '{}';
|
|
938
|
+
let parsed = {};
|
|
939
|
+
try {
|
|
940
|
+
parsed = JSON.parse(rawContent);
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
const jsonMatch = rawContent.match(/\{[\s\S]*\}/);
|
|
944
|
+
if (jsonMatch) {
|
|
945
|
+
try {
|
|
946
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
947
|
+
}
|
|
948
|
+
catch { }
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
let fixedCode = typeof parsed.fixedCode === 'string' && parsed.fixedCode.trim().length > 0
|
|
952
|
+
? parsed.fixedCode
|
|
953
|
+
: typeof parsed.code === 'string' && parsed.code.trim().length > 0
|
|
954
|
+
? parsed.code
|
|
955
|
+
: typeof parsed.content === 'string' && parsed.content.trim().length > 0
|
|
956
|
+
? parsed.content
|
|
957
|
+
: typeof parsed.source === 'string' && parsed.source.trim().length > 0
|
|
958
|
+
? parsed.source
|
|
959
|
+
: typeof parsed.codeContent === 'string' && parsed.codeContent.trim().length > 0
|
|
960
|
+
? parsed.codeContent
|
|
961
|
+
: typeof parsed.fileContent === 'string' && parsed.fileContent.trim().length > 0
|
|
962
|
+
? parsed.fileContent
|
|
963
|
+
: typeof parsed.fixed_code === 'string' && parsed.fixed_code.trim().length > 0
|
|
964
|
+
? parsed.fixed_code
|
|
965
|
+
: typeof parsed.snippet === 'string' && parsed.snippet.trim().length > 0
|
|
966
|
+
? parsed.snippet
|
|
967
|
+
: typeof parsed.code_snippet === 'string' && parsed.code_snippet.trim().length > 0
|
|
968
|
+
? parsed.code_snippet
|
|
969
|
+
: typeof parsed.updatedCode === 'string' && parsed.updatedCode.trim().length > 0
|
|
970
|
+
? parsed.updatedCode
|
|
971
|
+
: typeof parsed.newCode === 'string' && parsed.newCode.trim().length > 0
|
|
972
|
+
? parsed.newCode
|
|
973
|
+
: typeof parsed.js === 'string' && parsed.js.trim().length > 0
|
|
974
|
+
? parsed.js
|
|
975
|
+
: typeof parsed.javascript === 'string' && parsed.javascript.trim().length > 0
|
|
976
|
+
? parsed.javascript
|
|
977
|
+
: typeof parsed.solution === 'string' && parsed.solution.trim().length > 0
|
|
978
|
+
? parsed.solution
|
|
979
|
+
: extractCodeFromRawResponse(rawContent) || currentFileContent;
|
|
980
|
+
// Clean up code block markdown fences if model wrapped fixedCode in ```
|
|
981
|
+
fixedCode = fixedCode.trim();
|
|
982
|
+
if (fixedCode.startsWith('```')) {
|
|
983
|
+
fixedCode = fixedCode.replace(/^```[a-zA-Z0-9_\-]*\r?\n?/, '').replace(/\r?\n?```\s*$/, '').trim();
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
targetFile: parsed.targetFile || targetFile,
|
|
987
|
+
problem: parsed.problem || `Issue identified based on user prompt: "${userPrompt}"`,
|
|
988
|
+
howFixed: parsed.howFixed || `Applied automated refactoring and fixes to ${targetFile}`,
|
|
989
|
+
fixedCode,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
throw new Error(`Failed to generate code fix via AI: ${error instanceof Error ? error.message : String(error)}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
export async function identifyTargetFiles(userPrompt, projectName, filesList, language, native) {
|
|
997
|
+
const prompt = `You are Scout, an AI Principal Software Engineer.
|
|
998
|
+
Project: "${projectName}"
|
|
999
|
+
User Request: "${userPrompt}"
|
|
1000
|
+
|
|
1001
|
+
File Tree Overview:
|
|
1002
|
+
${filesList.slice(0, 150).join('\n')}
|
|
1003
|
+
|
|
1004
|
+
Based on the user request, identify which specific file path(s) from the file tree list above need to be modified.
|
|
1005
|
+
Select ONLY the file(s) that strictly need modifications to fulfill the request. Unless the request explicitly asks to modify all files, prefer selecting only the single file or minimal set of files directly relevant.
|
|
1006
|
+
Return ONLY a JSON object matching this exact schema:
|
|
1007
|
+
{
|
|
1008
|
+
"targetFiles": ["relative/file/path1.ts", "relative/file/path2.html"]
|
|
1009
|
+
}
|
|
1010
|
+
`;
|
|
1011
|
+
try {
|
|
1012
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
1013
|
+
model,
|
|
1014
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1015
|
+
response_format: { type: 'json_object' },
|
|
1016
|
+
max_tokens: 500,
|
|
1017
|
+
}));
|
|
1018
|
+
const parsed = JSON.parse(response.choices[0]?.message?.content || '{}');
|
|
1019
|
+
if (Array.isArray(parsed.targetFiles)) {
|
|
1020
|
+
return parsed.targetFiles.filter((f) => typeof f === 'string' && filesList.includes(f));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
catch { }
|
|
1024
|
+
return [];
|
|
1025
|
+
}
|
|
1026
|
+
export async function generateMultiCodeFix(userPrompt, projectName, filesList, targetFilesMap, language, native, onProgress) {
|
|
1027
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
1028
|
+
const filePaths = Object.keys(targetFilesMap);
|
|
1029
|
+
if (filePaths.length === 1) {
|
|
1030
|
+
const singleFile = filePaths[0];
|
|
1031
|
+
const singleContent = targetFilesMap[singleFile];
|
|
1032
|
+
if (onProgress) {
|
|
1033
|
+
onProgress(0, 1, singleFile);
|
|
1034
|
+
}
|
|
1035
|
+
const res = await generateCodeFix(userPrompt, projectName, filesList, singleFile, singleContent, language, native);
|
|
1036
|
+
if (onProgress) {
|
|
1037
|
+
onProgress(1, 1, singleFile);
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
problem: res.problem,
|
|
1041
|
+
howFixed: res.howFixed,
|
|
1042
|
+
files: [{ targetFile: res.targetFile, problem: res.problem, howFixed: res.howFixed, fixedCode: res.fixedCode }],
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
let combinedFilesPrompt = '';
|
|
1046
|
+
let totalLength = 0;
|
|
1047
|
+
for (const f of filePaths) {
|
|
1048
|
+
const content = targetFilesMap[f] || '';
|
|
1049
|
+
totalLength += content.length;
|
|
1050
|
+
combinedFilesPrompt += `\n--- File: "${f}" ---\n\`\`\`\n${content.slice(0, 8000)}\n\`\`\`\n`;
|
|
1051
|
+
}
|
|
1052
|
+
if (totalLength < 25000 && filePaths.length <= 10) {
|
|
1053
|
+
const prompt = `You are Scout, an expert AI Principal Software Engineer & Code Auto-Fixer.
|
|
1054
|
+
|
|
1055
|
+
Project Name: "${projectName}"
|
|
1056
|
+
User Request / Prompt: "${userPrompt}"
|
|
1057
|
+
Target Files to Fix (${filePaths.length} files): ${filePaths.join(', ')}
|
|
1058
|
+
|
|
1059
|
+
File Tree Overview:
|
|
1060
|
+
${filesList.slice(0, 50).join('\n')}
|
|
1061
|
+
|
|
1062
|
+
Contents of Target Files:
|
|
1063
|
+
${combinedFilesPrompt}
|
|
1064
|
+
|
|
1065
|
+
Language for explanations requirement: ${langInfo.instruction} Write "problem" and "howFixed" explanations in ${langInfo.displayName}.
|
|
1066
|
+
|
|
1067
|
+
Your mission:
|
|
1068
|
+
1. Analyze ALL target files and the user prompt.
|
|
1069
|
+
2. Provide updated, complete production-ready code for EACH target file according to the user request. Do NOT omit existing logic unless refactoring or fulfilling the prompt.
|
|
1070
|
+
3. Return a JSON object matching this exact schema:
|
|
1071
|
+
{
|
|
1072
|
+
"problem": "Clear technical breakdown of what the issue was across these files written in ${langInfo.displayName}",
|
|
1073
|
+
"howFixed": "Technical explanation of how Scout fixed all target files written in ${langInfo.displayName}",
|
|
1074
|
+
"files": [
|
|
1075
|
+
{
|
|
1076
|
+
"targetFile": "relative/file/path",
|
|
1077
|
+
"fixedCode": "complete updated source code string for this file"
|
|
1078
|
+
}
|
|
1079
|
+
]
|
|
1080
|
+
}
|
|
1081
|
+
`;
|
|
1082
|
+
try {
|
|
1083
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
1084
|
+
model,
|
|
1085
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1086
|
+
response_format: { type: 'json_object' },
|
|
1087
|
+
max_tokens: 4000,
|
|
1088
|
+
}));
|
|
1089
|
+
const parsed = JSON.parse(response.choices[0]?.message?.content || '{}');
|
|
1090
|
+
const files = [];
|
|
1091
|
+
if (Array.isArray(parsed.files)) {
|
|
1092
|
+
for (const item of parsed.files) {
|
|
1093
|
+
if (item && typeof item.targetFile === 'string' && typeof item.fixedCode === 'string') {
|
|
1094
|
+
let code = item.fixedCode;
|
|
1095
|
+
if (code.trim().startsWith('```')) {
|
|
1096
|
+
code = code.trim().replace(/^```[a-zA-Z0-9_\-]*\r?\n?/, '').replace(/\r?\n?```\s*$/, '').trim();
|
|
1097
|
+
}
|
|
1098
|
+
files.push({
|
|
1099
|
+
targetFile: item.targetFile,
|
|
1100
|
+
fixedCode: code,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (files.length === filePaths.length) {
|
|
1106
|
+
return {
|
|
1107
|
+
problem: parsed.problem || `Issue identified across ${filePaths.length} files for prompt: "${userPrompt}"`,
|
|
1108
|
+
howFixed: parsed.howFixed || `Applied automated fixes to ${filePaths.length} files.`,
|
|
1109
|
+
files,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
catch { }
|
|
1114
|
+
}
|
|
1115
|
+
let completedCount = 0;
|
|
1116
|
+
const CONCURRENCY_LIMIT = 2;
|
|
1117
|
+
const results = await mapWithConcurrency(filePaths, CONCURRENCY_LIMIT, async (f) => {
|
|
1118
|
+
const content = targetFilesMap[f] || '';
|
|
1119
|
+
if (onProgress) {
|
|
1120
|
+
onProgress(completedCount, filePaths.length, f);
|
|
1121
|
+
}
|
|
1122
|
+
let resItem;
|
|
1123
|
+
try {
|
|
1124
|
+
const singleRes = await generateCodeFix(userPrompt, projectName, filesList, f, content, language, native);
|
|
1125
|
+
resItem = {
|
|
1126
|
+
targetFile: f,
|
|
1127
|
+
problem: singleRes.problem,
|
|
1128
|
+
howFixed: singleRes.howFixed,
|
|
1129
|
+
fixedCode: singleRes.fixedCode,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
resItem = {
|
|
1134
|
+
targetFile: f,
|
|
1135
|
+
fixedCode: content,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
completedCount++;
|
|
1139
|
+
if (onProgress) {
|
|
1140
|
+
onProgress(completedCount, filePaths.length, f);
|
|
1141
|
+
}
|
|
1142
|
+
return resItem;
|
|
1143
|
+
});
|
|
1144
|
+
return {
|
|
1145
|
+
problem: `Issue identified across ${filePaths.length} files matching prompt: "${userPrompt}"`,
|
|
1146
|
+
howFixed: `Updated ${filePaths.length} files to resolve user request.`,
|
|
1147
|
+
files: results,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
export async function generateCodeFile(filePath, userPrompt, projectName, filesList, language, native) {
|
|
1151
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
1152
|
+
const prompt = `You are Scout, an expert AI Software Architect & Code Generator.
|
|
1153
|
+
|
|
1154
|
+
Project Name: "${projectName}"
|
|
1155
|
+
Target File to Create: "${filePath}"
|
|
1156
|
+
User Instructions / Prompt: "${userPrompt}"
|
|
1157
|
+
|
|
1158
|
+
File Tree Context:
|
|
1159
|
+
${filesList.slice(0, 80).join('\n')}
|
|
1160
|
+
|
|
1161
|
+
Language requirement: ${langInfo.instruction} Write the "description" explanation in ${langInfo.displayName}.
|
|
1162
|
+
|
|
1163
|
+
Your mission:
|
|
1164
|
+
1. Generate complete, clean, highly professional, modular, and production-ready source code for "${filePath}".
|
|
1165
|
+
2. Maintain strict code elegance: modern language standards, proper syntax, clear variable/function names, explicit types (if TypeScript) or docstrings, defensive null checks, robust error handling, clean formatting, and industry best practices.
|
|
1166
|
+
3. Provide a clear description of what this code does.
|
|
1167
|
+
|
|
1168
|
+
Return ONLY a JSON object matching this exact schema:
|
|
1169
|
+
{
|
|
1170
|
+
"targetFile": "${filePath}",
|
|
1171
|
+
"description": "Clear description of the generated code and its capabilities written in ${langInfo.displayName}",
|
|
1172
|
+
"code": "complete source code string for ${filePath}"
|
|
1173
|
+
}
|
|
1174
|
+
`;
|
|
1175
|
+
try {
|
|
1176
|
+
const response = await callOpenAIWithRetry(async (model) => {
|
|
1177
|
+
try {
|
|
1178
|
+
return await openai.chat.completions.create({
|
|
1179
|
+
model,
|
|
1180
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1181
|
+
response_format: { type: 'json_object' },
|
|
1182
|
+
max_tokens: 3500,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
catch {
|
|
1186
|
+
return await openai.chat.completions.create({
|
|
1187
|
+
model,
|
|
1188
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1189
|
+
max_tokens: 3500,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
const rawContent = response.choices[0]?.message?.content || '{}';
|
|
1194
|
+
const parsed = cleanAndParseJsonPayload(rawContent);
|
|
1195
|
+
let code = typeof parsed.code === 'string' && parsed.code.trim().length > 0
|
|
1196
|
+
? parsed.code
|
|
1197
|
+
: typeof parsed.fixedCode === 'string' && parsed.fixedCode.trim().length > 0
|
|
1198
|
+
? parsed.fixedCode
|
|
1199
|
+
: typeof parsed.content === 'string' && parsed.content.trim().length > 0
|
|
1200
|
+
? parsed.content
|
|
1201
|
+
: typeof parsed.source === 'string' && parsed.source.trim().length > 0
|
|
1202
|
+
? parsed.source
|
|
1203
|
+
: typeof parsed.codeContent === 'string' && parsed.codeContent.trim().length > 0
|
|
1204
|
+
? parsed.codeContent
|
|
1205
|
+
: typeof parsed.fileContent === 'string' && parsed.fileContent.trim().length > 0
|
|
1206
|
+
? parsed.fileContent
|
|
1207
|
+
: typeof parsed.fixed_code === 'string' && parsed.fixed_code.trim().length > 0
|
|
1208
|
+
? parsed.fixed_code
|
|
1209
|
+
: typeof parsed.snippet === 'string' && parsed.snippet.trim().length > 0
|
|
1210
|
+
? parsed.snippet
|
|
1211
|
+
: typeof parsed.code_snippet === 'string' && parsed.code_snippet.trim().length > 0
|
|
1212
|
+
? parsed.code_snippet
|
|
1213
|
+
: typeof parsed.python === 'string' && parsed.python.trim().length > 0
|
|
1214
|
+
? parsed.python
|
|
1215
|
+
: typeof parsed.py === 'string' && parsed.py.trim().length > 0
|
|
1216
|
+
? parsed.py
|
|
1217
|
+
: typeof parsed.solution === 'string' && parsed.solution.trim().length > 0
|
|
1218
|
+
? parsed.solution
|
|
1219
|
+
: extractCodeFromRawResponse(rawContent) ||
|
|
1220
|
+
(rawContent && !rawContent.trim().startsWith('{') ? rawContent.trim() : '');
|
|
1221
|
+
if (!code && parsed && typeof parsed === 'object') {
|
|
1222
|
+
for (const val of Object.values(parsed)) {
|
|
1223
|
+
if (typeof val === 'string' &&
|
|
1224
|
+
val.trim().length > 15 &&
|
|
1225
|
+
(val.includes('\n') || val.includes('def ') || val.includes('function') || val.includes('import') || val.includes('class'))) {
|
|
1226
|
+
code = val.trim();
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
code = code.trim();
|
|
1232
|
+
if (code.startsWith('```')) {
|
|
1233
|
+
code = code.replace(/^```[a-zA-Z0-9_\-]*\r?\n?/, '').replace(/\r?\n?```\s*$/, '').trim();
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
targetFile: parsed.targetFile || filePath,
|
|
1237
|
+
description: parsed.description || `Generated code for ${filePath}`,
|
|
1238
|
+
code,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
catch (error) {
|
|
1242
|
+
console.error("OpenAI Code Generation Error:", error);
|
|
1243
|
+
throw new Error(`Failed to generate code file via AI: ${error instanceof Error ? error.message : String(error)}`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
export async function analyzeCommandError(command, exitCode, output, projectName, filesList, language, native) {
|
|
1247
|
+
const langInfo = getLanguageInstruction(language, native);
|
|
1248
|
+
const prompt = `You are Scout, an expert AI Systems & Build Engineer diagnosing a command failure.
|
|
1249
|
+
|
|
1250
|
+
Project: "${projectName}"
|
|
1251
|
+
Executed Command: \`${command}\`
|
|
1252
|
+
Exit Code: ${exitCode}
|
|
1253
|
+
|
|
1254
|
+
File Tree Overview:
|
|
1255
|
+
${filesList.slice(0, 80).join('\n')}
|
|
1256
|
+
|
|
1257
|
+
Command Output / Failure Logs:
|
|
1258
|
+
\`\`\`
|
|
1259
|
+
${output.slice(0, 8000)}
|
|
1260
|
+
\`\`\`
|
|
1261
|
+
|
|
1262
|
+
Language requirement: ${langInfo.instruction} Write "rootCause" and "suggestedFix" in ${langInfo.displayName}.
|
|
1263
|
+
|
|
1264
|
+
Diagnose why the command failed and identify which source file in the project likely needs to be fixed.
|
|
1265
|
+
|
|
1266
|
+
Return ONLY a JSON object matching this exact schema:
|
|
1267
|
+
{
|
|
1268
|
+
"command": "${command}",
|
|
1269
|
+
"exitCode": ${exitCode},
|
|
1270
|
+
"rootCause": "Clear explanation of why the command failed written in ${langInfo.displayName}",
|
|
1271
|
+
"suggestedFix": "Concrete suggestion on how to resolve the failure written in ${langInfo.displayName}",
|
|
1272
|
+
"targetFileToFix": "relative path to the source file that caused the failure, or empty string if unknown"
|
|
1273
|
+
}
|
|
1274
|
+
`;
|
|
1275
|
+
try {
|
|
1276
|
+
const response = await callOpenAIWithRetry((model) => openai.chat.completions.create({
|
|
1277
|
+
model,
|
|
1278
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1279
|
+
response_format: { type: 'json_object' },
|
|
1280
|
+
}));
|
|
1281
|
+
const parsed = JSON.parse(response.choices[0]?.message?.content || '{}');
|
|
1282
|
+
return {
|
|
1283
|
+
command,
|
|
1284
|
+
exitCode,
|
|
1285
|
+
rootCause: parsed.rootCause || 'Command process returned a non-zero exit status.',
|
|
1286
|
+
suggestedFix: parsed.suggestedFix || 'Check output logs for syntax or runtime errors.',
|
|
1287
|
+
targetFileToFix: parsed.targetFileToFix || undefined,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
catch (error) {
|
|
54
1291
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'src/index.ts -> Primary router establishing CLI hooks, subcommand registration, and execution workflow',
|
|
60
|
-
'src/engine/llm.ts -> Core AI engine managing prompt orchestration and LLM proxy synthesis'
|
|
61
|
-
],
|
|
62
|
-
painPoints: [
|
|
63
|
-
'⚡ AI Gateway Orchestration: Ensure API keys or FrontTerrain Proxy authentication headers are active for live AI synthesis.',
|
|
64
|
-
'⚡ Asynchronous Git Churn Analysis: High-volume repository scanning requires optimized commit log batching for large-scale codebases.',
|
|
65
|
-
'⚡ Module Resolution & ESM Boundaries: Strict TypeScript transpilation requires precise extension matching across ESM module dependencies.'
|
|
66
|
-
],
|
|
1292
|
+
command,
|
|
1293
|
+
exitCode,
|
|
1294
|
+
rootCause: `Command failed with code ${exitCode}. AI diagnosis error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1295
|
+
suggestedFix: 'Inspect terminal output above for missing packages or compiler errors.',
|
|
67
1296
|
};
|
|
68
1297
|
}
|
|
69
1298
|
}
|