minovative-mind-cli 2.9.1 → 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.
Files changed (34) hide show
  1. package/README.md +14 -6
  2. package/dist/services/agent/commandApproval.js +5 -2
  3. package/dist/services/agent/slashCommands.js +90 -53
  4. package/dist/services/agent-tools.d.ts +4 -3
  5. package/dist/services/agent-tools.js +32 -79
  6. package/dist/services/agent.d.ts +5 -6
  7. package/dist/services/agent.js +11 -15
  8. package/dist/services/ai.d.ts +21 -1
  9. package/dist/services/ai.js +236 -11
  10. package/dist/services/chatHistoryService.d.ts +95 -2
  11. package/dist/services/chatHistoryService.js +236 -9
  12. package/dist/services/contextAgent.js +196 -81
  13. package/dist/services/investigationComplexity.d.ts +1 -1
  14. package/dist/services/investigationComplexity.js +1 -1
  15. package/dist/services/orchestration/investigationAgent.js +101 -84
  16. package/dist/services/orchestration/investigationCache.d.ts +80 -5
  17. package/dist/services/orchestration/investigationCache.js +570 -41
  18. package/dist/services/orchestration/investigationOrchestrator.js +17 -4
  19. package/dist/services/orchestration/orchestrator.js +6 -3
  20. package/dist/services/orchestration/scopedTools.js +5 -0
  21. package/dist/services/orchestration/subAgent.d.ts +31 -1
  22. package/dist/services/orchestration/subAgent.js +153 -2
  23. package/dist/utils/analysisRunner.d.ts +29 -0
  24. package/dist/utils/analysisRunner.js +200 -5
  25. package/dist/utils/contextPrompts.d.ts +20 -4
  26. package/dist/utils/contextPrompts.js +158 -23
  27. package/dist/utils/historyPrompt.d.ts +92 -1
  28. package/dist/utils/historyPrompt.js +166 -2
  29. package/dist/utils/symbolExtractor.d.ts +12 -0
  30. package/dist/utils/symbolExtractor.js +946 -0
  31. package/dist/utils/systemPrompts.d.ts +5 -4
  32. package/dist/utils/systemPrompts.js +46 -14
  33. package/oclif.manifest.json +1 -1
  34. package/package.json +2 -2
@@ -1,12 +1,12 @@
1
1
  /**
2
- * @fileoverview Utility functions for preparing and sanitizing context-related
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 to prevent nesting or breaking XML-like structure.
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,29 +1,30 @@
1
1
  /**
2
- * @fileoverview Utility functions for preparing and sanitizing context-related
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 to prevent nesting or breaking XML-like structure.
4
+ * formatting, character/token budgeting, and lightweight AST-scoped context extraction.
5
5
  */
6
+ import { changeLogger } from '../services/changeLogger.js';
7
+ import { extractDeclarationsOutline } from './symbolExtractor.js';
6
8
  /**
7
9
  * Sanitizes file content string to prevent nesting or breakout issues when wrapped in CDATA.
8
- * 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.
9
11
  *
10
12
  * @param content The raw content string to sanitize.
11
13
  * @returns The sanitized string safe to place inside a CDATA section.
12
14
  */
13
15
  export function sanitizeForCDATA(content) {
14
- // Prevent CDATA breakout by escaping ]]\\u200B>
16
+ // Prevent CDATA breakout by escaping ]]>
15
17
  return content.replace(new RegExp('\\]\\]>', 'g'), ']]\\\\u200B>');
16
18
  }
17
19
  /**
18
- * Constructs a structured XML/Markdown-like project context string from ContextAgentResult.
19
- * This is used to inject project profiles, directories, summary, and relevant files'
20
- * 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).
21
22
  *
22
23
  * @param context The collected context data from ContextAgent.
23
- * @returns A formatted string containing project profile, structure, investigation summaries, and relevant file contents.
24
+ * @returns Base markdown/XML context string before file sections.
24
25
  */
25
- export function buildContextInjection(context) {
26
- let injection = `<project_context>
26
+ function buildBaseContext(context) {
27
+ let base = `<project_context>
27
28
  ## Project Profile
28
29
  ${context.projectType}
29
30
 
@@ -34,28 +35,162 @@ ${context.projectTree}
34
35
  ${context.summary}
35
36
  `;
36
37
  if (context.webSearchSummary) {
37
- injection += `
38
+ base += `
38
39
  ## Web Search Findings
39
40
  ${context.webSearchSummary}
40
41
  `;
41
42
  }
42
- if (context.relevantFiles.size > 0) {
43
- injection += `\n## Relevant File Contents\n`;
44
- for (const [filePath, contentObj] of context.relevantFiles.entries()) {
45
- const contentText = contentObj.text;
46
- let aliasAttr = '';
47
- if (filePath.startsWith('@')) {
48
- const slashIndex = filePath.indexOf('/');
49
- const alias = slashIndex === -1 ? filePath.substring(1) : filePath.substring(1, slashIndex);
50
- aliasAttr = ` workspace="${alias}"`;
43
+ const changeHistory = changeLogger.getHistory();
44
+ if (changeHistory.length > 0) {
45
+ base += `
46
+ ## Recent Workspace Changes Log (Current / Recent Sessions)
47
+ `;
48
+ for (const changeSet of changeHistory) {
49
+ const timeStr = new Date(changeSet.timestamp).toISOString();
50
+ const statusSuffix = changeSet.status ? ` [${changeSet.status}]` : '';
51
+ base += `- [${timeStr}] ${changeSet.description}${statusSuffix}\n`;
52
+ if (changeSet.changes && changeSet.changes.length > 0) {
53
+ for (const fileChange of changeSet.changes) {
54
+ base += ` - ${fileChange.action}: ${fileChange.filePath}\n`;
55
+ }
51
56
  }
52
- injection += `<workspace_file path="${filePath}"${aliasAttr}>
57
+ }
58
+ }
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}>
53
78
  <content_data><![CDATA[
54
79
  ${sanitizeForCDATA(contentText)}
55
80
  ]]\\u200B></content_data>
56
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;
57
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;
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;
157
+ }
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}`;
58
163
  }
59
- injection += `</project_context>`;
60
- return injection;
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);
61
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
- * A custom text prompt that supports command history navigation using Up/Down arrow keys.
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.
@@ -1,5 +1,167 @@
1
1
  import { TextPrompt } from '@clack/core';
2
2
  import pc from 'picocolors';
3
+ /**
4
+ * Approximate token count for a given text string using character and word heuristics.
5
+ * Defaults to a safe ratio of ~3.8 characters per token for multi-language code and text.
6
+ *
7
+ * @param text The input string to estimate.
8
+ * @returns Estimated number of tokens.
9
+ */
10
+ export function estimateTokenCount(text) {
11
+ if (!text)
12
+ return 0;
13
+ const length = text.length;
14
+ if (length === 0)
15
+ return 0;
16
+ // Code and structured JSON typically have slightly higher token density (fewer chars/token)
17
+ // than plain English prose. 3.8 chars/token provides a conservative and safe ceiling.
18
+ return Math.ceil(length / 3.8);
19
+ }
20
+ /**
21
+ * Calculates dynamic token budget distributions for context injection, history retention,
22
+ * and system prompts based on the overall context window size.
23
+ *
24
+ * @param config Configuration parameters for the token budget.
25
+ * @returns An object containing granular token allocations.
26
+ */
27
+ export function calculateDynamicTokenBudget(config) {
28
+ const { totalBudget, reservedForOutput = 8192, systemPromptBudget = 4096, safetyMarginPct = 0.1, } = config;
29
+ const rawAvailable = Math.max(0, totalBudget - reservedForOutput - systemPromptBudget);
30
+ const safetyMargin = Math.round(rawAvailable * Math.min(Math.max(safetyMarginPct, 0), 0.5));
31
+ const usableBudget = Math.max(0, rawAvailable - safetyMargin);
32
+ // Allocate 60% of usable budget to workspace context (files, tree, tools) and 40% to conversation history
33
+ const contextBudget = Math.round(usableBudget * 0.6);
34
+ const historyBudget = usableBudget - contextBudget;
35
+ return {
36
+ contextBudget,
37
+ historyBudget,
38
+ outputBudget: reservedForOutput,
39
+ systemBudget: systemPromptBudget,
40
+ safetyMargin,
41
+ };
42
+ }
43
+ /**
44
+ * Prunes a text string so that its estimated token count does not exceed the specified maximum.
45
+ *
46
+ * @param text The text to prune.
47
+ * @param maxTokens Maximum allowable tokens for this text.
48
+ * @param options Pruning options.
49
+ * @returns Pruned string, with an optional truncation marker.
50
+ */
51
+ export function pruneTextToTokenBudget(text, maxTokens, options) {
52
+ if (!text || maxTokens <= 0)
53
+ return '';
54
+ const currentEstimated = estimateTokenCount(text);
55
+ if (currentEstimated <= maxTokens) {
56
+ return text;
57
+ }
58
+ const maxChars = Math.floor(maxTokens * 3.8);
59
+ const marker = options?.truncationMarker ?? '\n... [Context truncated to fit token budget] ...\n';
60
+ const effectiveMaxChars = Math.max(0, maxChars - marker.length);
61
+ if (options?.fromStart) {
62
+ const keepSlice = text.slice(text.length - effectiveMaxChars);
63
+ return `${marker}${keepSlice}`;
64
+ }
65
+ const keepSlice = text.slice(0, effectiveMaxChars);
66
+ return `${keepSlice}${marker}`;
67
+ }
68
+ /**
69
+ * Optimizes an array of history strings by trimming whitespace, deduplicating consecutive items,
70
+ * removing empty entries, and capping the array length to a maximum threshold.
71
+ *
72
+ * @param history Raw array of history entries.
73
+ * @param maxEntries Maximum number of entries to retain. Defaults to 100.
74
+ * @returns Cleaned and bounded array of history entries.
75
+ */
76
+ export function optimizeHistoryForContext(history, maxEntries = 100) {
77
+ if (!history || !Array.isArray(history))
78
+ return [];
79
+ const cleaned = [];
80
+ for (let i = 0; i < history.length; i++) {
81
+ const entry = history[i]?.trim();
82
+ if (!entry)
83
+ continue;
84
+ // Deduplicate consecutive identical entries
85
+ if (cleaned.length > 0 && cleaned[cleaned.length - 1] === entry) {
86
+ continue;
87
+ }
88
+ cleaned.push(entry);
89
+ }
90
+ if (cleaned.length > maxEntries) {
91
+ return cleaned.slice(cleaned.length - maxEntries);
92
+ }
93
+ return cleaned;
94
+ }
95
+ /**
96
+ * Formats a list of conversation turns into a unified history context string, dynamically
97
+ * pruning older turns if the total estimated tokens exceed the allocated budget.
98
+ *
99
+ * @param history Array of conversation turns with role and text content.
100
+ * @param maxTokens Maximum allowable tokens for the formatted history.
101
+ * @param options Formatting options.
102
+ * @returns Formatted and budget-constrained conversation history string.
103
+ */
104
+ export function formatHistoryWithTokenBudget(history, maxTokens = 16384, options) {
105
+ if (!history || history.length === 0 || maxTokens <= 0)
106
+ return '';
107
+ const keepRecent = options?.keepRecentTurns ?? 2;
108
+ const userLabel = options?.userLabel ?? 'User';
109
+ const modelLabel = options?.modelLabel ?? 'Assistant';
110
+ // Extract plain text for each turn
111
+ const turns = [];
112
+ for (const item of history) {
113
+ const label = item.role === 'user' ? userLabel : modelLabel;
114
+ let text = item.text || '';
115
+ if (!text && Array.isArray(item.parts)) {
116
+ text = item.parts
117
+ .map((p) => {
118
+ if (typeof p === 'string')
119
+ return p;
120
+ if (p.text)
121
+ return p.text;
122
+ if (p.functionCall)
123
+ return `[Tool Call: ${p.functionCall.name}]`;
124
+ if (p.functionResponse)
125
+ return `[Tool Result: ${p.functionResponse.name}]`;
126
+ return '';
127
+ })
128
+ .filter(Boolean)
129
+ .join('\n');
130
+ }
131
+ text = text.trim();
132
+ if (text) {
133
+ turns.push({
134
+ label,
135
+ text,
136
+ tokens: estimateTokenCount(`${label}: ${text}\n`),
137
+ });
138
+ }
139
+ }
140
+ if (turns.length === 0)
141
+ return '';
142
+ // Build from the newest turns backwards to prioritize recent context
143
+ const selectedTurns = [];
144
+ let consumedTokens = 0;
145
+ let isTruncated = false;
146
+ for (let i = turns.length - 1; i >= 0; i--) {
147
+ const turn = turns[i];
148
+ const formatted = `${turn.label}: ${turn.text}`;
149
+ const turnTokens = turn.tokens;
150
+ if (consumedTokens + turnTokens <= maxTokens || selectedTurns.length < keepRecent) {
151
+ selectedTurns.unshift(formatted);
152
+ consumedTokens += turnTokens;
153
+ }
154
+ else {
155
+ isTruncated = true;
156
+ break;
157
+ }
158
+ }
159
+ let result = selectedTurns.join('\n\n');
160
+ if (isTruncated) {
161
+ result = `[Older conversation history pruned to fit token budget]\n\n${result}`;
162
+ }
163
+ return result;
164
+ }
3
165
  // Emulate clack/prompts styling characters
4
166
  const S_BAR = '│';
5
167
  const S_BAR_END = '└';
@@ -23,13 +185,15 @@ function symbol(state) {
23
185
  }
24
186
  }
25
187
  /**
26
- * A custom text prompt that supports command history navigation using Up/Down arrow keys.
188
+ * A custom text prompt that supports command history navigation using Up/Down arrow keys
189
+ * with memory bounds and dynamic input optimization.
27
190
  *
28
191
  * @param opts - The configuration options for the prompt.
29
192
  * @returns A promise that resolves to the user's input string or a symbol if cancelled.
30
193
  */
31
194
  export const historyText = (opts) => {
32
- const history = opts.history || [];
195
+ const maxLen = opts.maxHistoryLength ?? 100;
196
+ const history = optimizeHistoryForContext(opts.history || [], maxLen);
33
197
  let historyIndex = -1;
34
198
  const prompt = new TextPrompt({
35
199
  validate: opts.validate,
@@ -31,3 +31,15 @@ export declare function extractSymbolMetadata(content: string, filePath: string,
31
31
  * @returns The filtered source string containing only the matched symbol blocks and omission markers
32
32
  */
33
33
  export declare function extractSymbols(content: string, filePath: string, targetElements: string[]): string;
34
+ /**
35
+ * Extracts a compact declarations outline (type/interface definitions, class skeletons,
36
+ * and function signatures without full implementation bodies) across TypeScript/JavaScript,
37
+ * Python, Go, and Rust.
38
+ *
39
+ * This dramatically reduces token usage when injecting multi-file context into prompts.
40
+ *
41
+ * @param content - Raw source code content
42
+ * @param filePath - File path used to infer language and syntax rules
43
+ * @returns Compact declarations outline string
44
+ */
45
+ export declare function extractDeclarationsOutline(content: string, filePath: string): string;