minovative-mind-cli 2.10.0 → 2.11.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/dist/services/agent/commandApproval.js +5 -2
- package/dist/services/agent/slashCommands.js +78 -51
- package/dist/services/agent-tools.d.ts +4 -3
- package/dist/services/agent-tools.js +32 -79
- package/dist/services/agent.d.ts +5 -6
- package/dist/services/agent.js +11 -15
- package/dist/services/ai.d.ts +19 -0
- package/dist/services/ai.js +108 -1
- package/dist/services/chatHistoryService.d.ts +95 -2
- package/dist/services/chatHistoryService.js +236 -9
- package/dist/services/contextAgent.js +161 -77
- package/dist/services/orchestration/investigationAgent.js +101 -84
- package/dist/services/orchestration/investigationOrchestrator.js +6 -2
- package/dist/services/orchestration/orchestrator.js +6 -3
- package/dist/services/orchestration/scopedTools.js +5 -0
- package/dist/services/orchestration/subAgent.d.ts +31 -1
- package/dist/services/orchestration/subAgent.js +153 -2
- package/dist/utils/analysisRunner.d.ts +29 -0
- package/dist/utils/analysisRunner.js +200 -5
- package/dist/utils/contextPrompts.d.ts +19 -3
- package/dist/utils/contextPrompts.js +144 -26
- package/dist/utils/historyPrompt.d.ts +92 -1
- package/dist/utils/historyPrompt.js +166 -2
- package/dist/utils/symbolExtractor.d.ts +12 -0
- package/dist/utils/symbolExtractor.js +946 -0
- package/dist/utils/systemPrompts.d.ts +3 -3
- package/dist/utils/systemPrompts.js +10 -7
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -10,17 +10,52 @@ const LANGUAGE_ALIASES = {
|
|
|
10
10
|
js: 'node',
|
|
11
11
|
javascript: 'node',
|
|
12
12
|
node: 'node',
|
|
13
|
+
cjs: 'node',
|
|
14
|
+
mjs: 'node',
|
|
13
15
|
ts: 'ts-node',
|
|
14
16
|
typescript: 'ts-node',
|
|
15
17
|
'ts-node': 'ts-node',
|
|
18
|
+
tsx: 'ts-node',
|
|
19
|
+
mts: 'ts-node',
|
|
20
|
+
cts: 'ts-node',
|
|
16
21
|
py: 'python',
|
|
22
|
+
py3: 'python',
|
|
17
23
|
python: 'python',
|
|
24
|
+
python3: 'python',
|
|
18
25
|
sh: 'bash',
|
|
19
26
|
bash: 'bash',
|
|
27
|
+
shell: 'bash',
|
|
28
|
+
zsh: 'bash',
|
|
20
29
|
go: 'go',
|
|
30
|
+
golang: 'go',
|
|
21
31
|
rs: 'rust',
|
|
22
32
|
rust: 'rust',
|
|
33
|
+
rustc: 'rust',
|
|
34
|
+
c: 'c',
|
|
35
|
+
cpp: 'cpp',
|
|
36
|
+
'c++': 'cpp',
|
|
37
|
+
cc: 'cpp',
|
|
38
|
+
cxx: 'cpp',
|
|
39
|
+
cplusplus: 'cpp',
|
|
40
|
+
rb: 'ruby',
|
|
41
|
+
ruby: 'ruby',
|
|
42
|
+
php: 'php',
|
|
43
|
+
java: 'java',
|
|
23
44
|
};
|
|
45
|
+
/** Supported canonical language names list. */
|
|
46
|
+
export const SUPPORTED_LANGUAGES = [
|
|
47
|
+
'node',
|
|
48
|
+
'ts-node',
|
|
49
|
+
'python',
|
|
50
|
+
'bash',
|
|
51
|
+
'go',
|
|
52
|
+
'rust',
|
|
53
|
+
'c',
|
|
54
|
+
'cpp',
|
|
55
|
+
'ruby',
|
|
56
|
+
'php',
|
|
57
|
+
'java',
|
|
58
|
+
];
|
|
24
59
|
/**
|
|
25
60
|
* Normalizes user/AI provided language string to a standard runtime identifier.
|
|
26
61
|
*/
|
|
@@ -28,6 +63,135 @@ export function normalizeLanguage(lang) {
|
|
|
28
63
|
const key = lang.toLowerCase().trim();
|
|
29
64
|
return LANGUAGE_ALIASES[key] || key;
|
|
30
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Detects programming language heuristic markers directly from script source code syntax.
|
|
68
|
+
* Useful when language is omitted or set to 'auto'.
|
|
69
|
+
*
|
|
70
|
+
* @param code - The source code to analyze.
|
|
71
|
+
* @returns Detected runtime identifier (e.g., 'python', 'rust', 'go', 'cpp', 'c', 'bash', 'ruby', 'php', 'java', 'ts-node', 'node').
|
|
72
|
+
*/
|
|
73
|
+
export function detectLanguageFromCode(code) {
|
|
74
|
+
const trimmed = code.trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
return 'node';
|
|
77
|
+
// Shebang check
|
|
78
|
+
if (trimmed.startsWith('#!')) {
|
|
79
|
+
const firstLine = trimmed.split('\n')[0].toLowerCase();
|
|
80
|
+
if (firstLine.includes('python'))
|
|
81
|
+
return 'python';
|
|
82
|
+
if (firstLine.includes('bash') || firstLine.includes('sh') || firstLine.includes('zsh'))
|
|
83
|
+
return 'bash';
|
|
84
|
+
if (firstLine.includes('node'))
|
|
85
|
+
return 'node';
|
|
86
|
+
if (firstLine.includes('ruby'))
|
|
87
|
+
return 'ruby';
|
|
88
|
+
if (firstLine.includes('php'))
|
|
89
|
+
return 'php';
|
|
90
|
+
}
|
|
91
|
+
// PHP marker
|
|
92
|
+
if (trimmed.startsWith('<?php') || trimmed.includes('<?php')) {
|
|
93
|
+
return 'php';
|
|
94
|
+
}
|
|
95
|
+
// Rust markers
|
|
96
|
+
if (/\b(fn\s+main\s*\(|println!\s*\(|eprintln!\s*\(|use\s+std::|impl\s+[A-Z]\w*|pub\s+fn\b|match\s+\w+\s*\{)/.test(code)) {
|
|
97
|
+
return 'rust';
|
|
98
|
+
}
|
|
99
|
+
// Go markers
|
|
100
|
+
if (/\bpackage\s+main\b/.test(code) ||
|
|
101
|
+
(/\bfunc\s+main\s*\(/.test(code) && /\bimport\s+(\(|")/.test(code))) {
|
|
102
|
+
return 'go';
|
|
103
|
+
}
|
|
104
|
+
// C++ markers
|
|
105
|
+
if (/#\s*include\s*<(iostream|vector|string|memory|algorithm|map|set|utility)>/.test(code) ||
|
|
106
|
+
/\bstd::(cout|cin|cerr|endl|vector|string|make_unique|make_shared)\b/.test(code) ||
|
|
107
|
+
/\btemplate\s*<\s*typename\b/.test(code)) {
|
|
108
|
+
return 'cpp';
|
|
109
|
+
}
|
|
110
|
+
// C markers
|
|
111
|
+
if (/#\s*include\s*<(stdio\.h|stdlib\.h|string\.h|unistd\.h|math\.h)>/.test(code) ||
|
|
112
|
+
(/\bprintf\s*\(/.test(code) && /\bint\s+main\s*\(/.test(code))) {
|
|
113
|
+
return 'c';
|
|
114
|
+
}
|
|
115
|
+
// Java markers
|
|
116
|
+
if (/\bpublic\s+class\s+[A-Z]\w*/.test(code) ||
|
|
117
|
+
/\bpublic\s+static\s+void\s+main\s*\(/.test(code) ||
|
|
118
|
+
/\bSystem\.(out|err)\.println\b/.test(code)) {
|
|
119
|
+
return 'java';
|
|
120
|
+
}
|
|
121
|
+
// Python markers
|
|
122
|
+
if (/\b(def\s+[a-zA-Z_]\w*\s*\(|import\s+[a-zA-Z_]\w*|from\s+[a-zA-Z_]\w*\s+import|if\s+__name__\s*==\s*['"]__main__['"])/.test(code) ||
|
|
123
|
+
(/:\s*$/.test(trimmed) && /\b(elif|except|finally|pass|yield)\b/.test(code))) {
|
|
124
|
+
return 'python';
|
|
125
|
+
}
|
|
126
|
+
// Ruby markers
|
|
127
|
+
if (/\b(def\s+[a-zA-Z_]\w*(\s*\(|\s*\n)|puts\s+|require_relative\s+|attr_accessor\s+:)/.test(code) &&
|
|
128
|
+
/\bend\b/.test(code)) {
|
|
129
|
+
return 'ruby';
|
|
130
|
+
}
|
|
131
|
+
// TypeScript markers
|
|
132
|
+
if (/\b(interface\s+[A-Z]\w*|type\s+[A-Z]\w*\s*=|:\s*(string|number|boolean|Record<|Array<|Promise<|void|unknown|never)\b|as\s+const\b)/.test(code)) {
|
|
133
|
+
return 'ts-node';
|
|
134
|
+
}
|
|
135
|
+
// Bash markers
|
|
136
|
+
if (/\b(echo\s+['"].*['"]|if\s+\[\s+.*\s+\];\s*then|fi\b|done\b|export\s+[A-Z_]+=\S+)/.test(code) &&
|
|
137
|
+
!code.includes('console.log')) {
|
|
138
|
+
return 'bash';
|
|
139
|
+
}
|
|
140
|
+
return 'node';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detects the dominant programming language / runtime for a workspace based on project manifest files.
|
|
144
|
+
*
|
|
145
|
+
* @param workspaceRoot - Path to the workspace root directory.
|
|
146
|
+
* @returns Detected runtime identifier (e.g., 'rust', 'go', 'python', 'cpp', 'ts-node', 'node').
|
|
147
|
+
*/
|
|
148
|
+
export async function detectProjectRuntime(workspaceRoot) {
|
|
149
|
+
const probeFiles = [
|
|
150
|
+
{ file: 'Cargo.toml', runtime: 'rust' },
|
|
151
|
+
{ file: 'go.mod', runtime: 'go' },
|
|
152
|
+
{ file: 'pyproject.toml', runtime: 'python' },
|
|
153
|
+
{ file: 'requirements.txt', runtime: 'python' },
|
|
154
|
+
{ file: 'Pipfile', runtime: 'python' },
|
|
155
|
+
{ file: 'setup.py', runtime: 'python' },
|
|
156
|
+
{ file: 'CMakeLists.txt', runtime: 'cpp' },
|
|
157
|
+
{ file: 'Gemfile', runtime: 'ruby' },
|
|
158
|
+
{ file: 'composer.json', runtime: 'php' },
|
|
159
|
+
{ file: 'pom.xml', runtime: 'java' },
|
|
160
|
+
{ file: 'build.gradle', runtime: 'java' },
|
|
161
|
+
{ file: 'tsconfig.json', runtime: 'ts-node' },
|
|
162
|
+
{ file: 'package.json', runtime: 'node' },
|
|
163
|
+
];
|
|
164
|
+
for (const { file, runtime } of probeFiles) {
|
|
165
|
+
try {
|
|
166
|
+
await fs.access(path.join(workspaceRoot, file));
|
|
167
|
+
return runtime;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Continue searching
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return 'node';
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolves the effective runtime language by combining explicit language input,
|
|
177
|
+
* source code syntax heuristics, and workspace project manifests.
|
|
178
|
+
*
|
|
179
|
+
* @param workspaceRoot - Path to workspace root directory.
|
|
180
|
+
* @param language - Optional language parameter passed by user/agent.
|
|
181
|
+
* @param code - Optional source code string to inspect.
|
|
182
|
+
*/
|
|
183
|
+
export async function resolveEffectiveRuntime(workspaceRoot, language, code) {
|
|
184
|
+
if (language && language.toLowerCase().trim() !== 'auto') {
|
|
185
|
+
return normalizeLanguage(language);
|
|
186
|
+
}
|
|
187
|
+
if (code && code.trim()) {
|
|
188
|
+
const detectedFromCode = detectLanguageFromCode(code);
|
|
189
|
+
if (detectedFromCode !== 'node') {
|
|
190
|
+
return detectedFromCode;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return detectProjectRuntime(workspaceRoot);
|
|
194
|
+
}
|
|
31
195
|
/**
|
|
32
196
|
* Detects whether the workspace package.json specifies `"type": "module"`.
|
|
33
197
|
* Returns `'module'` or `'commonjs'`.
|
|
@@ -72,6 +236,11 @@ export async function resolveScriptExtension(workspaceRoot, language, code) {
|
|
|
72
236
|
bash: '.sh',
|
|
73
237
|
go: '.go',
|
|
74
238
|
rust: '.rs',
|
|
239
|
+
c: '.c',
|
|
240
|
+
cpp: '.cpp',
|
|
241
|
+
ruby: '.rb',
|
|
242
|
+
php: '.php',
|
|
243
|
+
java: '.java',
|
|
75
244
|
};
|
|
76
245
|
return defaultExts[normLang] || '.js';
|
|
77
246
|
}
|
|
@@ -89,6 +258,7 @@ function buildTempPath(code, ext) {
|
|
|
89
258
|
*/
|
|
90
259
|
function buildCommand(language, scriptPath) {
|
|
91
260
|
const normLang = normalizeLanguage(language);
|
|
261
|
+
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
92
262
|
switch (normLang) {
|
|
93
263
|
case 'node':
|
|
94
264
|
return `node "${scriptPath}"`;
|
|
@@ -101,10 +271,23 @@ function buildCommand(language, scriptPath) {
|
|
|
101
271
|
case 'go':
|
|
102
272
|
return `go run "${scriptPath}"`;
|
|
103
273
|
case 'rust': {
|
|
104
|
-
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
105
274
|
const binPath = scriptPath.replace(/\.rs$/, binExt);
|
|
106
275
|
return `rustc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
107
276
|
}
|
|
277
|
+
case 'c': {
|
|
278
|
+
const binPath = scriptPath.replace(/\.c$/, binExt);
|
|
279
|
+
return `gcc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
280
|
+
}
|
|
281
|
+
case 'cpp': {
|
|
282
|
+
const binPath = scriptPath.replace(/\.cpp$/, binExt);
|
|
283
|
+
return `g++ -std=c++17 "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
284
|
+
}
|
|
285
|
+
case 'ruby':
|
|
286
|
+
return `ruby "${scriptPath}"`;
|
|
287
|
+
case 'php':
|
|
288
|
+
return `php "${scriptPath}"`;
|
|
289
|
+
case 'java':
|
|
290
|
+
return `java "${scriptPath}"`;
|
|
108
291
|
default:
|
|
109
292
|
return null;
|
|
110
293
|
}
|
|
@@ -134,30 +317,39 @@ function truncateOutput(text, max) {
|
|
|
134
317
|
export async function runEphemeralScript(workspaceRoot, language, code, options) {
|
|
135
318
|
const timeoutMs = options?.timeoutMs ?? 60_000;
|
|
136
319
|
const maxOutputChars = options?.maxOutputChars ?? 100_000;
|
|
137
|
-
const normLang =
|
|
320
|
+
const normLang = await resolveEffectiveRuntime(workspaceRoot, language, code);
|
|
138
321
|
const ext = await resolveScriptExtension(workspaceRoot, normLang, code);
|
|
139
322
|
const scriptPath = buildTempPath(code, ext);
|
|
140
323
|
const cmd = buildCommand(normLang, scriptPath);
|
|
141
324
|
if (!cmd) {
|
|
142
325
|
return {
|
|
143
326
|
stdout: '',
|
|
144
|
-
stderr: `Unsupported language runtime: "${language}". Supported:
|
|
327
|
+
stderr: `Unsupported language runtime: "${language}". Supported: ${SUPPORTED_LANGUAGES.join(', ')}`,
|
|
145
328
|
exitCode: 1,
|
|
146
329
|
};
|
|
147
330
|
}
|
|
148
331
|
// Collect all temp files created so we can clean them up unconditionally
|
|
149
332
|
const tempFiles = [scriptPath];
|
|
333
|
+
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
150
334
|
if (normLang === 'rust') {
|
|
151
|
-
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
152
335
|
tempFiles.push(scriptPath.replace(/\.rs$/, binExt));
|
|
336
|
+
tempFiles.push(scriptPath.replace(/\.rs$/, '.pdb'));
|
|
337
|
+
}
|
|
338
|
+
else if (normLang === 'c') {
|
|
339
|
+
tempFiles.push(scriptPath.replace(/\.c$/, binExt));
|
|
340
|
+
}
|
|
341
|
+
else if (normLang === 'cpp') {
|
|
342
|
+
tempFiles.push(scriptPath.replace(/\.cpp$/, binExt));
|
|
153
343
|
}
|
|
154
344
|
try {
|
|
155
345
|
await fs.writeFile(scriptPath, code, 'utf-8');
|
|
346
|
+
const mergedEnv = options?.env ? { ...process.env, ...options.env } : process.env;
|
|
156
347
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
157
348
|
cwd: workspaceRoot,
|
|
158
349
|
timeout: timeoutMs,
|
|
159
350
|
maxBuffer: 1024 * 1024, // 1 MB buffer
|
|
160
351
|
signal: options?.abortSignal,
|
|
352
|
+
env: mergedEnv,
|
|
161
353
|
});
|
|
162
354
|
return {
|
|
163
355
|
stdout: truncateOutput(stdout.trim(), maxOutputChars),
|
|
@@ -170,6 +362,9 @@ export async function runEphemeralScript(workspaceRoot, language, code, options)
|
|
|
170
362
|
if (err.name === 'AbortError' || err.message?.includes('abort')) {
|
|
171
363
|
return { stdout: '', stderr: 'Analysis script aborted by user.', exitCode: 130 };
|
|
172
364
|
}
|
|
365
|
+
if (err.killed && err.signal === 'SIGTERM') {
|
|
366
|
+
return { stdout: '', stderr: `Execution timed out after ${timeoutMs}ms.`, exitCode: 124 };
|
|
367
|
+
}
|
|
173
368
|
const stdout = truncateOutput((err.stdout || '').trim(), maxOutputChars);
|
|
174
369
|
const stderr = truncateOutput((err.stderr || '').trim(), maxOutputChars);
|
|
175
370
|
const exitCode = typeof err.code === 'number' ? err.code : 1;
|
|
@@ -179,7 +374,7 @@ export async function runEphemeralScript(workspaceRoot, language, code, options)
|
|
|
179
374
|
// Guarantee temp file cleanup regardless of success or failure
|
|
180
375
|
for (const tmpFile of tempFiles) {
|
|
181
376
|
try {
|
|
182
|
-
await fs.rm(tmpFile, { force: true });
|
|
377
|
+
await fs.rm(tmpFile, { force: true, recursive: true });
|
|
183
378
|
}
|
|
184
379
|
catch {
|
|
185
380
|
// Ignore cleanup errors — temp files will be reaped by the OS eventually
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Utility functions for preparing and sanitizing context-related
|
|
3
3
|
* injection strings to be sent to the AI model. Includes handling of CDATA block
|
|
4
|
-
* formatting
|
|
4
|
+
* formatting, character/token budgeting, and lightweight AST-scoped context extraction.
|
|
5
5
|
*/
|
|
6
6
|
import { ContextAgentResult } from '../services/contextAgent.js';
|
|
7
7
|
/**
|
|
8
8
|
* Sanitizes file content string to prevent nesting or breakout issues when wrapped in CDATA.
|
|
9
|
-
* Replaces occurrences of "]]>" with an escaped equivalent containing a zero-width space.
|
|
9
|
+
* Replaces occurrences of "]]\u200B>" with an escaped equivalent containing a zero-width space.
|
|
10
10
|
*
|
|
11
11
|
* @param content The raw content string to sanitize.
|
|
12
12
|
* @returns The sanitized string safe to place inside a CDATA section.
|
|
@@ -17,7 +17,23 @@ export declare function sanitizeForCDATA(content: string): string;
|
|
|
17
17
|
* This is used to inject project profiles, directories, summary, and relevant files'
|
|
18
18
|
* content into the prompt context for the LLM.
|
|
19
19
|
*
|
|
20
|
+
* Supports an optional character budget constraint to prevent oversized prompts.
|
|
21
|
+
*
|
|
20
22
|
* @param context The collected context data from ContextAgent.
|
|
23
|
+
* @param maxChars Optional maximum character budget for the generated prompt injection.
|
|
21
24
|
* @returns A formatted string containing project profile, structure, investigation summaries, and relevant file contents.
|
|
22
25
|
*/
|
|
23
|
-
export declare function buildContextInjection(context: ContextAgentResult): string;
|
|
26
|
+
export declare function buildContextInjection(context: ContextAgentResult, maxChars?: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* Constructs a lightweight, AST-scoped XML/Markdown-like project context string from ContextAgentResult.
|
|
29
|
+
* Uses `extractDeclarationsOutline` to replace full implementation bodies in relevant files with
|
|
30
|
+
* compact declarations outlines (types, interfaces, class skeletons, function signatures),
|
|
31
|
+
* drastically conserving context tokens during orchestration and sub-agent dispatch.
|
|
32
|
+
*
|
|
33
|
+
* Supports an optional character budget constraint.
|
|
34
|
+
*
|
|
35
|
+
* @param context The collected context data from ContextAgent.
|
|
36
|
+
* @param maxChars Optional maximum character budget for the generated prompt injection.
|
|
37
|
+
* @returns A formatted string containing project profile, structure, investigation summaries, and AST-scoped file outlines.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildScopedContextInjection(context: ContextAgentResult, maxChars?: number): string;
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Utility functions for preparing and sanitizing context-related
|
|
3
3
|
* injection strings to be sent to the AI model. Includes handling of CDATA block
|
|
4
|
-
* formatting
|
|
4
|
+
* formatting, character/token budgeting, and lightweight AST-scoped context extraction.
|
|
5
5
|
*/
|
|
6
6
|
import { changeLogger } from '../services/changeLogger.js';
|
|
7
|
+
import { extractDeclarationsOutline } from './symbolExtractor.js';
|
|
7
8
|
/**
|
|
8
9
|
* Sanitizes file content string to prevent nesting or breakout issues when wrapped in CDATA.
|
|
9
|
-
* Replaces occurrences of "]]>" with an escaped equivalent containing a zero-width space.
|
|
10
|
+
* Replaces occurrences of "]]\u200B>" with an escaped equivalent containing a zero-width space.
|
|
10
11
|
*
|
|
11
12
|
* @param content The raw content string to sanitize.
|
|
12
13
|
* @returns The sanitized string safe to place inside a CDATA section.
|
|
13
14
|
*/
|
|
14
15
|
export function sanitizeForCDATA(content) {
|
|
15
|
-
// Prevent CDATA breakout by escaping ]]
|
|
16
|
+
// Prevent CDATA breakout by escaping ]]>
|
|
16
17
|
return content.replace(new RegExp('\\]\\]>', 'g'), ']]\\\\u200B>');
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* content into the prompt context for the LLM.
|
|
20
|
+
* Formats the base context header (project profile, directory tree, investigation summary,
|
|
21
|
+
* web search findings, and recent changesets).
|
|
22
22
|
*
|
|
23
23
|
* @param context The collected context data from ContextAgent.
|
|
24
|
-
* @returns
|
|
24
|
+
* @returns Base markdown/XML context string before file sections.
|
|
25
25
|
*/
|
|
26
|
-
|
|
27
|
-
let
|
|
26
|
+
function buildBaseContext(context) {
|
|
27
|
+
let base = `<project_context>
|
|
28
28
|
## Project Profile
|
|
29
29
|
${context.projectType}
|
|
30
30
|
|
|
@@ -35,44 +35,162 @@ ${context.projectTree}
|
|
|
35
35
|
${context.summary}
|
|
36
36
|
`;
|
|
37
37
|
if (context.webSearchSummary) {
|
|
38
|
-
|
|
38
|
+
base += `
|
|
39
39
|
## Web Search Findings
|
|
40
40
|
${context.webSearchSummary}
|
|
41
41
|
`;
|
|
42
42
|
}
|
|
43
43
|
const changeHistory = changeLogger.getHistory();
|
|
44
44
|
if (changeHistory.length > 0) {
|
|
45
|
-
|
|
45
|
+
base += `
|
|
46
46
|
## Recent Workspace Changes Log (Current / Recent Sessions)
|
|
47
47
|
`;
|
|
48
48
|
for (const changeSet of changeHistory) {
|
|
49
49
|
const timeStr = new Date(changeSet.timestamp).toISOString();
|
|
50
50
|
const statusSuffix = changeSet.status ? ` [${changeSet.status}]` : '';
|
|
51
|
-
|
|
51
|
+
base += `- [${timeStr}] ${changeSet.description}${statusSuffix}\n`;
|
|
52
52
|
if (changeSet.changes && changeSet.changes.length > 0) {
|
|
53
53
|
for (const fileChange of changeSet.changes) {
|
|
54
|
-
|
|
54
|
+
base += ` - ${fileChange.action}: ${fileChange.filePath}\n`;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
return base;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Formats a single file entry wrapped in XML workspace_file and CDATA tags.
|
|
63
|
+
*
|
|
64
|
+
* @param filePath Path or alias of the file.
|
|
65
|
+
* @param contentText File content or declarations outline.
|
|
66
|
+
* @param scoped Whether this content represents an AST-scoped outline.
|
|
67
|
+
* @returns Formatted workspace_file block.
|
|
68
|
+
*/
|
|
69
|
+
function formatWorkspaceFileBlock(filePath, contentText, scoped = false) {
|
|
70
|
+
let aliasAttr = '';
|
|
71
|
+
if (filePath.startsWith('@')) {
|
|
72
|
+
const slashIndex = filePath.indexOf('/');
|
|
73
|
+
const alias = slashIndex === -1 ? filePath.substring(1) : filePath.substring(1, slashIndex);
|
|
74
|
+
aliasAttr = ` workspace="${alias}"`;
|
|
75
|
+
}
|
|
76
|
+
const scopedAttr = scoped ? ' scoped="outline"' : '';
|
|
77
|
+
return `<workspace_file path="${filePath}"${aliasAttr}${scopedAttr}>
|
|
70
78
|
<content_data><![CDATA[
|
|
71
79
|
${sanitizeForCDATA(contentText)}
|
|
72
80
|
]]\\u200B></content_data>
|
|
73
81
|
</workspace_file>\n`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Assembles and injects relevant file contents into the context injection string,
|
|
85
|
+
* adhering to character/token budget constraints if specified.
|
|
86
|
+
*
|
|
87
|
+
* @param baseContext The base header context string.
|
|
88
|
+
* @param relevantFiles Map of file paths to their content objects.
|
|
89
|
+
* @param maxChars Optional maximum total character budget for the entire injection.
|
|
90
|
+
* @param useScopedOutline If true, extracts compact AST declarations outlines instead of raw contents.
|
|
91
|
+
* @returns The complete context injection string.
|
|
92
|
+
*/
|
|
93
|
+
function assembleContextWithFiles(baseContext, relevantFiles, maxChars, useScopedOutline = false) {
|
|
94
|
+
const closingTag = '</project_context>';
|
|
95
|
+
const hasBudget = typeof maxChars === 'number' && maxChars > 0;
|
|
96
|
+
// If no files are present, return base context closed
|
|
97
|
+
if (!relevantFiles || relevantFiles.size === 0) {
|
|
98
|
+
let result = `${baseContext.trimEnd()}\n${closingTag}`;
|
|
99
|
+
if (hasBudget && result.length > maxChars) {
|
|
100
|
+
const budgetForBase = Math.max(0, maxChars - closingTag.length - 1);
|
|
101
|
+
result = `${baseContext.slice(0, budgetForBase).trimEnd()}\n${closingTag}`;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
let injection = baseContext;
|
|
106
|
+
// If base context itself exceeds budget, prune base context first
|
|
107
|
+
if (hasBudget && injection.length + closingTag.length > maxChars) {
|
|
108
|
+
const budgetForBase = Math.max(0, maxChars - closingTag.length - 1);
|
|
109
|
+
return `${injection.slice(0, budgetForBase).trimEnd()}\n${closingTag}`;
|
|
110
|
+
}
|
|
111
|
+
const filesHeader = '\n## Relevant File Contents\n';
|
|
112
|
+
if (hasBudget && injection.length + filesHeader.length + closingTag.length > maxChars) {
|
|
113
|
+
return `${injection.trimEnd()}\n${closingTag}`;
|
|
114
|
+
}
|
|
115
|
+
injection += filesHeader;
|
|
116
|
+
const fileEntries = Array.from(relevantFiles.entries());
|
|
117
|
+
const truncationNotice = '\n\n... [Content truncated to fit token budget] ...';
|
|
118
|
+
const safetyAllowance = 1; // for newline before closing tag
|
|
119
|
+
for (let i = 0; i < fileEntries.length; i++) {
|
|
120
|
+
const [filePath, contentObj] = fileEntries[i];
|
|
121
|
+
let contentText = contentObj.text;
|
|
122
|
+
if (useScopedOutline) {
|
|
123
|
+
contentText = extractDeclarationsOutline(contentText, filePath) || contentText;
|
|
124
|
+
}
|
|
125
|
+
if (!hasBudget) {
|
|
126
|
+
injection += formatWorkspaceFileBlock(filePath, contentText, useScopedOutline);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const remainingFilesCount = fileEntries.length - i - 1;
|
|
130
|
+
const omissionNoticeEstimate = remainingFilesCount > 0
|
|
131
|
+
? `<!-- ${remainingFilesCount} additional relevant file(s) omitted to stay within token budget -->\n`.length
|
|
132
|
+
: 0;
|
|
133
|
+
const remainingBudgetForThisFile = maxChars - injection.length - closingTag.length - omissionNoticeEstimate - safetyAllowance;
|
|
134
|
+
const fullBlock = formatWorkspaceFileBlock(filePath, contentText, useScopedOutline);
|
|
135
|
+
if (fullBlock.length <= remainingBudgetForThisFile) {
|
|
136
|
+
injection += fullBlock;
|
|
137
|
+
continue;
|
|
74
138
|
}
|
|
139
|
+
// Try partial/truncated block
|
|
140
|
+
const emptyBlock = formatWorkspaceFileBlock(filePath, '', useScopedOutline);
|
|
141
|
+
const wrapperOverhead = emptyBlock.length;
|
|
142
|
+
const availableContentChars = remainingBudgetForThisFile - wrapperOverhead - truncationNotice.length;
|
|
143
|
+
let includedTruncated = false;
|
|
144
|
+
if (availableContentChars > 50) {
|
|
145
|
+
const truncatedContent = contentText.slice(0, availableContentChars) + truncationNotice;
|
|
146
|
+
injection += formatWorkspaceFileBlock(filePath, truncatedContent, useScopedOutline);
|
|
147
|
+
includedTruncated = true;
|
|
148
|
+
}
|
|
149
|
+
const remainingFiles = fileEntries.length - (includedTruncated ? i + 1 : i);
|
|
150
|
+
if (remainingFiles > 0) {
|
|
151
|
+
const notice = `<!-- ${remainingFiles} additional relevant file(s) omitted to stay within token budget -->\n`;
|
|
152
|
+
if (injection.length + notice.length + closingTag.length + safetyAllowance <= maxChars) {
|
|
153
|
+
injection += notice;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
75
157
|
}
|
|
76
|
-
|
|
77
|
-
|
|
158
|
+
let result = `${injection.trimEnd()}\n${closingTag}`;
|
|
159
|
+
// Final hard boundary guard
|
|
160
|
+
if (hasBudget && result.length > maxChars) {
|
|
161
|
+
const keepChars = Math.max(0, maxChars - closingTag.length - 1);
|
|
162
|
+
result = `${result.slice(0, keepChars).trimEnd()}\n${closingTag}`;
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Constructs a structured XML/Markdown-like project context string from ContextAgentResult.
|
|
168
|
+
* This is used to inject project profiles, directories, summary, and relevant files'
|
|
169
|
+
* content into the prompt context for the LLM.
|
|
170
|
+
*
|
|
171
|
+
* Supports an optional character budget constraint to prevent oversized prompts.
|
|
172
|
+
*
|
|
173
|
+
* @param context The collected context data from ContextAgent.
|
|
174
|
+
* @param maxChars Optional maximum character budget for the generated prompt injection.
|
|
175
|
+
* @returns A formatted string containing project profile, structure, investigation summaries, and relevant file contents.
|
|
176
|
+
*/
|
|
177
|
+
export function buildContextInjection(context, maxChars) {
|
|
178
|
+
const baseContext = buildBaseContext(context);
|
|
179
|
+
return assembleContextWithFiles(baseContext, context.relevantFiles, maxChars, false);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Constructs a lightweight, AST-scoped XML/Markdown-like project context string from ContextAgentResult.
|
|
183
|
+
* Uses `extractDeclarationsOutline` to replace full implementation bodies in relevant files with
|
|
184
|
+
* compact declarations outlines (types, interfaces, class skeletons, function signatures),
|
|
185
|
+
* drastically conserving context tokens during orchestration and sub-agent dispatch.
|
|
186
|
+
*
|
|
187
|
+
* Supports an optional character budget constraint.
|
|
188
|
+
*
|
|
189
|
+
* @param context The collected context data from ContextAgent.
|
|
190
|
+
* @param maxChars Optional maximum character budget for the generated prompt injection.
|
|
191
|
+
* @returns A formatted string containing project profile, structure, investigation summaries, and AST-scoped file outlines.
|
|
192
|
+
*/
|
|
193
|
+
export function buildScopedContextInjection(context, maxChars) {
|
|
194
|
+
const baseContext = buildBaseContext(context);
|
|
195
|
+
return assembleContextWithFiles(baseContext, context.relevantFiles, maxChars, true);
|
|
78
196
|
}
|
|
@@ -12,11 +12,102 @@ export interface HistoryTextOptions {
|
|
|
12
12
|
initialValue?: string;
|
|
13
13
|
/** An array of previous commands or strings to allow navigation through. */
|
|
14
14
|
history?: string[];
|
|
15
|
+
/** Maximum number of history entries to keep in memory for navigation. Defaults to 100. */
|
|
16
|
+
maxHistoryLength?: number;
|
|
15
17
|
/** Callback to validate the user input. */
|
|
16
18
|
validate?: (value: string) => string | Error | undefined;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
21
|
+
* Configuration options for dynamic token budget calculation.
|
|
22
|
+
*/
|
|
23
|
+
export interface TokenBudgetConfig {
|
|
24
|
+
/** Total maximum tokens available in the context window (e.g., 1_000_000 for Gemini 1.5 Pro). */
|
|
25
|
+
totalBudget: number;
|
|
26
|
+
/** Tokens reserved for model generation/output. Defaults to 8,192. */
|
|
27
|
+
reservedForOutput?: number;
|
|
28
|
+
/** Tokens reserved for system instructions. Defaults to 4,096. */
|
|
29
|
+
systemPromptBudget?: number;
|
|
30
|
+
/** Percentage of remaining tokens to reserve as safety headroom (0.0 to 1.0). Defaults to 0.1 (10%). */
|
|
31
|
+
safetyMarginPct?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resulting token allocations across prompt components.
|
|
35
|
+
*/
|
|
36
|
+
export interface TokenBudgetAllocation {
|
|
37
|
+
/** Maximum tokens allocated for file context and workspace tree. */
|
|
38
|
+
contextBudget: number;
|
|
39
|
+
/** Maximum tokens allocated for conversation history. */
|
|
40
|
+
historyBudget: number;
|
|
41
|
+
/** Tokens reserved for model output generation. */
|
|
42
|
+
outputBudget: number;
|
|
43
|
+
/** Tokens reserved for system prompt & directives. */
|
|
44
|
+
systemBudget: number;
|
|
45
|
+
/** Headroom tokens reserved for safety / uncounted overhead. */
|
|
46
|
+
safetyMargin: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Approximate token count for a given text string using character and word heuristics.
|
|
50
|
+
* Defaults to a safe ratio of ~3.8 characters per token for multi-language code and text.
|
|
51
|
+
*
|
|
52
|
+
* @param text The input string to estimate.
|
|
53
|
+
* @returns Estimated number of tokens.
|
|
54
|
+
*/
|
|
55
|
+
export declare function estimateTokenCount(text: string | null | undefined): number;
|
|
56
|
+
/**
|
|
57
|
+
* Calculates dynamic token budget distributions for context injection, history retention,
|
|
58
|
+
* and system prompts based on the overall context window size.
|
|
59
|
+
*
|
|
60
|
+
* @param config Configuration parameters for the token budget.
|
|
61
|
+
* @returns An object containing granular token allocations.
|
|
62
|
+
*/
|
|
63
|
+
export declare function calculateDynamicTokenBudget(config: TokenBudgetConfig): TokenBudgetAllocation;
|
|
64
|
+
/**
|
|
65
|
+
* Prunes a text string so that its estimated token count does not exceed the specified maximum.
|
|
66
|
+
*
|
|
67
|
+
* @param text The text to prune.
|
|
68
|
+
* @param maxTokens Maximum allowable tokens for this text.
|
|
69
|
+
* @param options Pruning options.
|
|
70
|
+
* @returns Pruned string, with an optional truncation marker.
|
|
71
|
+
*/
|
|
72
|
+
export declare function pruneTextToTokenBudget(text: string, maxTokens: number, options?: {
|
|
73
|
+
/** Marker to append/prepend indicating truncation. */
|
|
74
|
+
truncationMarker?: string;
|
|
75
|
+
/** If true, prunes from the start (keeping the end). If false, prunes from the end (keeping the start). */
|
|
76
|
+
fromStart?: boolean;
|
|
77
|
+
}): string;
|
|
78
|
+
/**
|
|
79
|
+
* Optimizes an array of history strings by trimming whitespace, deduplicating consecutive items,
|
|
80
|
+
* removing empty entries, and capping the array length to a maximum threshold.
|
|
81
|
+
*
|
|
82
|
+
* @param history Raw array of history entries.
|
|
83
|
+
* @param maxEntries Maximum number of entries to retain. Defaults to 100.
|
|
84
|
+
* @returns Cleaned and bounded array of history entries.
|
|
85
|
+
*/
|
|
86
|
+
export declare function optimizeHistoryForContext(history: string[] | undefined, maxEntries?: number): string[];
|
|
87
|
+
/**
|
|
88
|
+
* Formats a list of conversation turns into a unified history context string, dynamically
|
|
89
|
+
* pruning older turns if the total estimated tokens exceed the allocated budget.
|
|
90
|
+
*
|
|
91
|
+
* @param history Array of conversation turns with role and text content.
|
|
92
|
+
* @param maxTokens Maximum allowable tokens for the formatted history.
|
|
93
|
+
* @param options Formatting options.
|
|
94
|
+
* @returns Formatted and budget-constrained conversation history string.
|
|
95
|
+
*/
|
|
96
|
+
export declare function formatHistoryWithTokenBudget(history: Array<{
|
|
97
|
+
role: string;
|
|
98
|
+
text?: string;
|
|
99
|
+
parts?: any[];
|
|
100
|
+
}>, maxTokens?: number, options?: {
|
|
101
|
+
/** Minimum number of recent turns to always keep regardless of budget if possible. Defaults to 2. */
|
|
102
|
+
keepRecentTurns?: number;
|
|
103
|
+
/** Custom label prefix for user turns. Defaults to "User". */
|
|
104
|
+
userLabel?: string;
|
|
105
|
+
/** Custom label prefix for assistant/model turns. Defaults to "Assistant". */
|
|
106
|
+
modelLabel?: string;
|
|
107
|
+
}): string;
|
|
108
|
+
/**
|
|
109
|
+
* A custom text prompt that supports command history navigation using Up/Down arrow keys
|
|
110
|
+
* with memory bounds and dynamic input optimization.
|
|
20
111
|
*
|
|
21
112
|
* @param opts - The configuration options for the prompt.
|
|
22
113
|
* @returns A promise that resolves to the user's input string or a symbol if cancelled.
|