@git.zone/tsdoc 1.6.1 → 1.7.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/ts/cli.ts CHANGED
@@ -31,18 +31,18 @@ export const run = async () => {
31
31
  tsdocCli.addCommand('aidoc').subscribe(async (argvArg) => {
32
32
  const aidocInstance = new AiDoc();
33
33
  await aidocInstance.start();
34
-
34
+
35
35
  // Get context token count if requested
36
36
  if (argvArg.tokens || argvArg.showTokens) {
37
37
  logger.log('info', `Calculating context token count...`);
38
38
  const tokenCount = await aidocInstance.getProjectContextTokenCount(paths.cwd);
39
39
  logger.log('ok', `Total context token count: ${tokenCount}`);
40
-
40
+
41
41
  if (argvArg.tokensOnly) {
42
42
  return; // Exit early if we only want token count
43
43
  }
44
44
  }
45
-
45
+
46
46
  logger.log('info', `Generating new readme...`);
47
47
  logger.log('info', `This may take some time...`);
48
48
  await aidocInstance.buildReadme(paths.cwd);
@@ -54,67 +54,50 @@ export const run = async () => {
54
54
  tsdocCli.addCommand('tokens').subscribe(async (argvArg) => {
55
55
  const aidocInstance = new AiDoc();
56
56
  await aidocInstance.start();
57
-
57
+
58
58
  logger.log('info', `Calculating context token count...`);
59
-
60
- // Determine context mode based on args
61
- let contextMode: context.ContextMode = 'full';
62
- if (argvArg.trim || argvArg.trimmed) {
63
- contextMode = 'trimmed';
64
- } else if (argvArg.summarize || argvArg.summarized) {
65
- contextMode = 'summarized';
66
- }
67
-
59
+
68
60
  // Get task type if specified
69
61
  let taskType: context.TaskType | undefined = undefined;
70
62
  if (argvArg.task) {
71
63
  if (['readme', 'commit', 'description'].includes(argvArg.task)) {
72
64
  taskType = argvArg.task as context.TaskType;
73
65
  } else {
74
- logger.log('warn', `Unknown task type: ${argvArg.task}. Using default context.`);
66
+ logger.log('warn', `Unknown task type: ${argvArg.task}. Using default (readme).`);
67
+ taskType = 'readme';
75
68
  }
69
+ } else {
70
+ // Default to readme if no task specified
71
+ taskType = 'readme';
76
72
  }
77
-
78
- // Use enhanced context
73
+
74
+ // Use iterative context building
79
75
  const taskFactory = new context.TaskContextFactory(paths.cwd);
80
76
  await taskFactory.initialize();
81
-
82
- let contextResult: context.IContextResult;
83
-
77
+
78
+ let contextResult: context.IIterativeContextResult;
79
+
84
80
  if (argvArg.all) {
85
81
  // Show stats for all task types
86
82
  const stats = await taskFactory.getTokenStats();
87
-
83
+
88
84
  logger.log('ok', 'Token statistics by task:');
89
85
  for (const [task, data] of Object.entries(stats)) {
90
86
  logger.log('info', `\n${task.toUpperCase()}:`);
91
87
  logger.log('info', ` Tokens: ${data.tokenCount}`);
92
88
  logger.log('info', ` Token savings: ${data.savings}`);
93
89
  logger.log('info', ` Files: ${data.includedFiles} included, ${data.trimmedFiles} trimmed, ${data.excludedFiles} excluded`);
94
-
90
+
95
91
  // Calculate percentage of model context
96
92
  const o4MiniPercentage = (data.tokenCount / 200000 * 100).toFixed(2);
97
93
  logger.log('info', ` Context usage: ${o4MiniPercentage}% of o4-mini (200K tokens)`);
98
94
  }
99
-
95
+
100
96
  return;
101
97
  }
102
-
103
- if (taskType) {
104
- // Get context for specific task
105
- contextResult = await taskFactory.createContextForTask(taskType);
106
- } else {
107
- // Get generic context with specified mode
108
- const enhancedContext = new context.EnhancedContext(paths.cwd);
109
- await enhancedContext.initialize();
110
- enhancedContext.setContextMode(contextMode);
111
-
112
- if (argvArg.maxTokens) {
113
- enhancedContext.setTokenBudget(parseInt(argvArg.maxTokens, 10));
114
- }
115
-
116
- contextResult = await enhancedContext.buildContext();
117
- }
98
+
99
+ // Get context for specific task
100
+ contextResult = await taskFactory.createContextForTask(taskType);
118
101
 
119
102
  // Display results
120
103
  logger.log('ok', `Total context token count: ${contextResult.tokenCount}`);
@@ -9,7 +9,8 @@ import type {
9
9
  ICacheConfig,
10
10
  IAnalyzerConfig,
11
11
  IPrioritizationWeights,
12
- ITierConfig
12
+ ITierConfig,
13
+ IIterativeConfig
13
14
  } from './types.js';
14
15
 
15
16
  /**
@@ -98,6 +99,13 @@ export class ConfigManager {
98
99
  essential: { minScore: 0.8, trimLevel: 'none' },
99
100
  important: { minScore: 0.5, trimLevel: 'light' },
100
101
  optional: { minScore: 0.2, trimLevel: 'aggressive' }
102
+ },
103
+ iterative: {
104
+ maxIterations: 5,
105
+ firstPassFileLimit: 10,
106
+ subsequentPassFileLimit: 5,
107
+ temperature: 0.3,
108
+ model: 'gpt-4-turbo-preview'
101
109
  }
102
110
  };
103
111
  }
@@ -156,15 +164,15 @@ export class ConfigManager {
156
164
  */
157
165
  private mergeConfigs(defaultConfig: IContextConfig, userConfig: Partial<IContextConfig>): IContextConfig {
158
166
  const result: IContextConfig = { ...defaultConfig };
159
-
167
+
160
168
  // Merge top-level properties
161
169
  if (userConfig.maxTokens !== undefined) result.maxTokens = userConfig.maxTokens;
162
170
  if (userConfig.defaultMode !== undefined) result.defaultMode = userConfig.defaultMode;
163
-
171
+
164
172
  // Merge task-specific settings
165
173
  if (userConfig.taskSpecificSettings) {
166
174
  result.taskSpecificSettings = result.taskSpecificSettings || {};
167
-
175
+
168
176
  // For each task type, merge settings
169
177
  (['readme', 'commit', 'description'] as TaskType[]).forEach(taskType => {
170
178
  if (userConfig.taskSpecificSettings?.[taskType]) {
@@ -175,7 +183,7 @@ export class ConfigManager {
175
183
  }
176
184
  });
177
185
  }
178
-
186
+
179
187
  // Merge trimming configuration
180
188
  if (userConfig.trimming) {
181
189
  result.trimming = {
@@ -216,6 +224,14 @@ export class ConfigManager {
216
224
  };
217
225
  }
218
226
 
227
+ // Merge iterative configuration
228
+ if (userConfig.iterative) {
229
+ result.iterative = {
230
+ ...result.iterative,
231
+ ...userConfig.iterative
232
+ };
233
+ }
234
+
219
235
  return result;
220
236
  }
221
237
 
@@ -331,6 +347,19 @@ export class ConfigManager {
331
347
  };
332
348
  }
333
349
 
350
+ /**
351
+ * Get iterative configuration
352
+ */
353
+ public getIterativeConfig(): IIterativeConfig {
354
+ return this.config.iterative || {
355
+ maxIterations: 5,
356
+ firstPassFileLimit: 10,
357
+ subsequentPassFileLimit: 5,
358
+ temperature: 0.3,
359
+ model: 'gpt-4-turbo-preview'
360
+ };
361
+ }
362
+
334
363
  /**
335
364
  * Clear the config cache (force reload on next access)
336
365
  */
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import * as fs from 'fs';
3
3
  import type { ICacheEntry, ICacheConfig } from './types.js';
4
+ import { logger } from '../logging.js';
4
5
 
5
6
  /**
6
7
  * ContextCache provides persistent caching of file contents and token counts
@@ -22,7 +22,9 @@ import type {
22
22
  ICacheEntry,
23
23
  IFileDependencies,
24
24
  IFileAnalysis,
25
- IAnalysisResult
25
+ IAnalysisResult,
26
+ IIterativeConfig,
27
+ IIterativeContextResult
26
28
  } from './types.js';
27
29
 
28
30
  export {
@@ -54,5 +56,7 @@ export type {
54
56
  ICacheEntry,
55
57
  IFileDependencies,
56
58
  IFileAnalysis,
57
- IAnalysisResult
59
+ IAnalysisResult,
60
+ IIterativeConfig,
61
+ IIterativeContextResult
58
62
  };
@@ -0,0 +1,467 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as fs from 'fs';
3
+ import { logger } from '../logging.js';
4
+ import type {
5
+ TaskType,
6
+ IFileMetadata,
7
+ IFileInfo,
8
+ IIterativeContextResult,
9
+ IIterationState,
10
+ IFileSelectionDecision,
11
+ IContextSufficiencyDecision,
12
+ IIterativeConfig,
13
+ } from './types.js';
14
+ import { LazyFileLoader } from './lazy-file-loader.js';
15
+ import { ContextCache } from './context-cache.js';
16
+ import { ContextAnalyzer } from './context-analyzer.js';
17
+ import { ConfigManager } from './config-manager.js';
18
+
19
+ /**
20
+ * Iterative context builder that uses AI to intelligently select files
21
+ * across multiple iterations until sufficient context is gathered
22
+ */
23
+ export class IterativeContextBuilder {
24
+ private projectRoot: string;
25
+ private lazyLoader: LazyFileLoader;
26
+ private cache: ContextCache;
27
+ private analyzer: ContextAnalyzer;
28
+ private config: Required<IIterativeConfig>;
29
+ private tokenBudget: number = 190000;
30
+ private openaiInstance: plugins.smartai.OpenAiProvider;
31
+
32
+ /**
33
+ * Creates a new IterativeContextBuilder
34
+ * @param projectRoot - Root directory of the project
35
+ * @param config - Iterative configuration
36
+ */
37
+ constructor(projectRoot: string, config?: Partial<IIterativeConfig>) {
38
+ this.projectRoot = projectRoot;
39
+ this.lazyLoader = new LazyFileLoader(projectRoot);
40
+ this.cache = new ContextCache(projectRoot);
41
+ this.analyzer = new ContextAnalyzer(projectRoot);
42
+
43
+ // Default configuration
44
+ this.config = {
45
+ maxIterations: config?.maxIterations ?? 5,
46
+ firstPassFileLimit: config?.firstPassFileLimit ?? 10,
47
+ subsequentPassFileLimit: config?.subsequentPassFileLimit ?? 5,
48
+ temperature: config?.temperature ?? 0.3,
49
+ model: config?.model ?? 'gpt-4-turbo-preview',
50
+ };
51
+
52
+ }
53
+
54
+ /**
55
+ * Initialize the builder
56
+ */
57
+ public async initialize(): Promise<void> {
58
+ await this.cache.init();
59
+ const configManager = ConfigManager.getInstance();
60
+ await configManager.initialize(this.projectRoot);
61
+ this.tokenBudget = configManager.getMaxTokens();
62
+
63
+ // Initialize OpenAI instance
64
+ const qenvInstance = new plugins.qenv.Qenv();
65
+ const openaiToken = await qenvInstance.getEnvVarOnDemand('OPENAI_TOKEN');
66
+ if (!openaiToken) {
67
+ throw new Error('OPENAI_TOKEN environment variable is required for iterative context building');
68
+ }
69
+ this.openaiInstance = new plugins.smartai.OpenAiProvider({
70
+ openaiToken,
71
+ });
72
+ await this.openaiInstance.start();
73
+ }
74
+
75
+ /**
76
+ * Build context iteratively using AI decision making
77
+ * @param taskType - Type of task being performed
78
+ * @returns Complete iterative context result
79
+ */
80
+ public async buildContextIteratively(taskType: TaskType): Promise<IIterativeContextResult> {
81
+ const startTime = Date.now();
82
+ logger.log('info', '🤖 Starting iterative context building...');
83
+ logger.log('info', ` Task: ${taskType}, Budget: ${this.tokenBudget} tokens, Max iterations: ${this.config.maxIterations}`);
84
+
85
+ // Phase 1: Scan project files for metadata
86
+ logger.log('info', '📋 Scanning project files...');
87
+ const metadata = await this.scanProjectFiles(taskType);
88
+ const totalEstimatedTokens = metadata.reduce((sum, m) => sum + m.estimatedTokens, 0);
89
+ logger.log('info', ` Found ${metadata.length} files (~${totalEstimatedTokens} estimated tokens)`);
90
+
91
+ // Phase 2: Analyze files for initial prioritization
92
+ logger.log('info', '🔍 Analyzing file dependencies and importance...');
93
+ const analysis = await this.analyzer.analyze(metadata, taskType, []);
94
+ logger.log('info', ` Analysis complete in ${analysis.analysisDuration}ms`);
95
+
96
+ // Track state across iterations
97
+ const iterations: IIterationState[] = [];
98
+ let totalTokensUsed = 0;
99
+ let apiCallCount = 0;
100
+ let loadedContent = '';
101
+ const includedFiles: IFileInfo[] = [];
102
+
103
+ // Phase 3: Iterative file selection and loading
104
+ for (let iteration = 1; iteration <= this.config.maxIterations; iteration++) {
105
+ const iterationStart = Date.now();
106
+ logger.log('info', `\n🤔 Iteration ${iteration}/${this.config.maxIterations}: Asking AI which files to examine...`);
107
+
108
+ const remainingBudget = this.tokenBudget - totalTokensUsed;
109
+ logger.log('info', ` Token budget remaining: ${remainingBudget}/${this.tokenBudget} (${Math.round((remainingBudget / this.tokenBudget) * 100)}%)`);
110
+
111
+ // Get AI decision on which files to load
112
+ const decision = await this.getFileSelectionDecision(
113
+ metadata,
114
+ analysis.files.slice(0, 30), // Top 30 files by importance
115
+ taskType,
116
+ iteration,
117
+ totalTokensUsed,
118
+ remainingBudget,
119
+ loadedContent
120
+ );
121
+ apiCallCount++;
122
+
123
+ logger.log('info', ` AI reasoning: ${decision.reasoning}`);
124
+ logger.log('info', ` AI requested ${decision.filesToLoad.length} files`);
125
+
126
+ // Load requested files
127
+ const iterationFiles: IFileInfo[] = [];
128
+ let iterationTokens = 0;
129
+
130
+ if (decision.filesToLoad.length > 0) {
131
+ logger.log('info', '📥 Loading requested files...');
132
+
133
+ for (const filePath of decision.filesToLoad) {
134
+ try {
135
+ const fileInfo = await this.loadFile(filePath);
136
+ if (totalTokensUsed + fileInfo.tokenCount! <= this.tokenBudget) {
137
+ const formattedFile = this.formatFileForContext(fileInfo);
138
+ loadedContent += formattedFile;
139
+ includedFiles.push(fileInfo);
140
+ iterationFiles.push(fileInfo);
141
+ iterationTokens += fileInfo.tokenCount!;
142
+ totalTokensUsed += fileInfo.tokenCount!;
143
+
144
+ logger.log('info', ` ✓ ${fileInfo.relativePath} (${fileInfo.tokenCount} tokens)`);
145
+ } else {
146
+ logger.log('warn', ` ✗ ${fileInfo.relativePath} - would exceed budget, skipping`);
147
+ }
148
+ } catch (error) {
149
+ logger.log('warn', ` ✗ Failed to load ${filePath}: ${error.message}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Record iteration state
155
+ const iterationDuration = Date.now() - iterationStart;
156
+ iterations.push({
157
+ iteration,
158
+ filesLoaded: iterationFiles,
159
+ tokensUsed: iterationTokens,
160
+ totalTokensUsed,
161
+ decision,
162
+ duration: iterationDuration,
163
+ });
164
+
165
+ logger.log('info', ` Iteration ${iteration} complete: ${iterationFiles.length} files loaded, ${iterationTokens} tokens used`);
166
+
167
+ // Check if we should continue
168
+ if (totalTokensUsed >= this.tokenBudget * 0.95) {
169
+ logger.log('warn', '⚠️ Approaching token budget limit, stopping iterations');
170
+ break;
171
+ }
172
+
173
+ // Ask AI if context is sufficient
174
+ if (iteration < this.config.maxIterations) {
175
+ logger.log('info', '🤔 Asking AI if context is sufficient...');
176
+ const sufficiencyDecision = await this.evaluateContextSufficiency(
177
+ loadedContent,
178
+ taskType,
179
+ iteration,
180
+ totalTokensUsed,
181
+ remainingBudget - iterationTokens
182
+ );
183
+ apiCallCount++;
184
+
185
+ logger.log('info', ` AI decision: ${sufficiencyDecision.sufficient ? '✅ SUFFICIENT' : '⏭️ NEEDS MORE'}`);
186
+ logger.log('info', ` Reasoning: ${sufficiencyDecision.reasoning}`);
187
+
188
+ if (sufficiencyDecision.sufficient) {
189
+ logger.log('ok', '✅ Context building complete - AI determined context is sufficient');
190
+ break;
191
+ }
192
+ }
193
+ }
194
+
195
+ const totalDuration = Date.now() - startTime;
196
+ logger.log('ok', `\n✅ Iterative context building complete!`);
197
+ logger.log('info', ` Files included: ${includedFiles.length}`);
198
+ logger.log('info', ` Token usage: ${totalTokensUsed}/${this.tokenBudget} (${Math.round((totalTokensUsed / this.tokenBudget) * 100)}%)`);
199
+ logger.log('info', ` Iterations: ${iterations.length}, API calls: ${apiCallCount}`);
200
+ logger.log('info', ` Total duration: ${(totalDuration / 1000).toFixed(2)}s`);
201
+
202
+ return {
203
+ context: loadedContent,
204
+ tokenCount: totalTokensUsed,
205
+ includedFiles,
206
+ trimmedFiles: [],
207
+ excludedFiles: [],
208
+ tokenSavings: 0,
209
+ iterationCount: iterations.length,
210
+ iterations,
211
+ apiCallCount,
212
+ totalDuration,
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Scan project files based on task type
218
+ */
219
+ private async scanProjectFiles(taskType: TaskType): Promise<IFileMetadata[]> {
220
+ const configManager = ConfigManager.getInstance();
221
+ const taskConfig = configManager.getTaskConfig(taskType);
222
+
223
+ const includeGlobs = taskConfig?.includePaths?.map(p => `${p}/**/*.ts`) || [
224
+ 'ts/**/*.ts',
225
+ 'ts*/**/*.ts'
226
+ ];
227
+
228
+ const configGlobs = [
229
+ 'package.json',
230
+ 'readme.md',
231
+ 'readme.hints.md',
232
+ 'npmextra.json'
233
+ ];
234
+
235
+ return await this.lazyLoader.scanFiles([...configGlobs, ...includeGlobs]);
236
+ }
237
+
238
+ /**
239
+ * Get AI decision on which files to load
240
+ */
241
+ private async getFileSelectionDecision(
242
+ allMetadata: IFileMetadata[],
243
+ analyzedFiles: any[],
244
+ taskType: TaskType,
245
+ iteration: number,
246
+ tokensUsed: number,
247
+ remainingBudget: number,
248
+ loadedContent: string
249
+ ): Promise<IFileSelectionDecision> {
250
+ const isFirstIteration = iteration === 1;
251
+ const fileLimit = isFirstIteration
252
+ ? this.config.firstPassFileLimit
253
+ : this.config.subsequentPassFileLimit;
254
+
255
+ const systemPrompt = this.buildFileSelectionPrompt(
256
+ allMetadata,
257
+ analyzedFiles,
258
+ taskType,
259
+ iteration,
260
+ tokensUsed,
261
+ remainingBudget,
262
+ loadedContent,
263
+ fileLimit
264
+ );
265
+
266
+ const response = await this.openaiInstance.chat({
267
+ systemMessage: `You are an AI assistant that helps select the most relevant files for code analysis.
268
+ You must respond ONLY with valid JSON that can be parsed with JSON.parse().
269
+ Do not wrap the JSON in markdown code blocks or add any other text.`,
270
+ userMessage: systemPrompt,
271
+ messageHistory: [],
272
+ });
273
+
274
+ // Parse JSON response, handling potential markdown formatting
275
+ const content = response.message.replace('```json', '').replace('```', '').trim();
276
+ const parsed = JSON.parse(content);
277
+
278
+ return {
279
+ reasoning: parsed.reasoning || 'No reasoning provided',
280
+ filesToLoad: parsed.files_to_load || [],
281
+ estimatedTokensNeeded: parsed.estimated_tokens_needed,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Build prompt for file selection
287
+ */
288
+ private buildFileSelectionPrompt(
289
+ metadata: IFileMetadata[],
290
+ analyzedFiles: any[],
291
+ taskType: TaskType,
292
+ iteration: number,
293
+ tokensUsed: number,
294
+ remainingBudget: number,
295
+ loadedContent: string,
296
+ fileLimit: number
297
+ ): string {
298
+ const taskDescriptions = {
299
+ readme: 'generating a comprehensive README that explains the project\'s purpose, features, and API',
300
+ commit: 'analyzing code changes to generate an intelligent commit message',
301
+ description: 'generating a concise project description for package.json',
302
+ };
303
+
304
+ const alreadyLoadedFiles = loadedContent
305
+ ? loadedContent.split('\n======').slice(1).map(section => {
306
+ const match = section.match(/START OF FILE (.+?) ======/);
307
+ return match ? match[1] : '';
308
+ }).filter(Boolean)
309
+ : [];
310
+
311
+ const availableFiles = metadata
312
+ .filter(m => !alreadyLoadedFiles.includes(m.relativePath))
313
+ .map(m => {
314
+ const analysis = analyzedFiles.find(a => a.path === m.path);
315
+ return `- ${m.relativePath} (${m.size} bytes, ~${m.estimatedTokens} tokens${analysis ? `, importance: ${analysis.importanceScore.toFixed(2)}` : ''})`;
316
+ })
317
+ .join('\n');
318
+
319
+ return `You are building context for ${taskDescriptions[taskType]} in a TypeScript project.
320
+
321
+ ITERATION: ${iteration}
322
+ TOKENS USED: ${tokensUsed}/${tokensUsed + remainingBudget} (${Math.round((tokensUsed / (tokensUsed + remainingBudget)) * 100)}%)
323
+ REMAINING BUDGET: ${remainingBudget} tokens
324
+
325
+ ${alreadyLoadedFiles.length > 0 ? `FILES ALREADY LOADED:\n${alreadyLoadedFiles.map(f => `- ${f}`).join('\n')}\n\n` : ''}AVAILABLE FILES (not yet loaded):
326
+ ${availableFiles}
327
+
328
+ Your task: Select up to ${fileLimit} files that will give you the MOST understanding for this ${taskType} task.
329
+
330
+ ${iteration === 1 ? `This is the FIRST iteration. Focus on:
331
+ - Main entry points (index.ts, main exports)
332
+ - Core classes and interfaces
333
+ - Package configuration
334
+ ` : `This is iteration ${iteration}. You've already seen some files. Now focus on:
335
+ - Files that complement what you've already loaded
336
+ - Dependencies of already-loaded files
337
+ - Missing pieces for complete understanding
338
+ `}
339
+
340
+ Consider:
341
+ 1. File importance scores (if provided)
342
+ 2. File paths (ts/index.ts is likely more important than ts/internal/utils.ts)
343
+ 3. Token efficiency (prefer smaller files if they provide good information)
344
+ 4. Remaining budget (${remainingBudget} tokens)
345
+
346
+ Respond in JSON format:
347
+ {
348
+ "reasoning": "Brief explanation of why you're selecting these files",
349
+ "files_to_load": ["path/to/file1.ts", "path/to/file2.ts"],
350
+ "estimated_tokens_needed": 15000
351
+ }`;
352
+ }
353
+
354
+ /**
355
+ * Evaluate if current context is sufficient
356
+ */
357
+ private async evaluateContextSufficiency(
358
+ loadedContent: string,
359
+ taskType: TaskType,
360
+ iteration: number,
361
+ tokensUsed: number,
362
+ remainingBudget: number
363
+ ): Promise<IContextSufficiencyDecision> {
364
+ const prompt = `You have been building context for a ${taskType} task across ${iteration} iterations.
365
+
366
+ CURRENT STATE:
367
+ - Tokens used: ${tokensUsed}
368
+ - Remaining budget: ${remainingBudget}
369
+ - Files loaded: ${loadedContent.split('\n======').length - 1}
370
+
371
+ CONTEXT SO FAR:
372
+ ${loadedContent.substring(0, 3000)}... (truncated for brevity)
373
+
374
+ Question: Do you have SUFFICIENT context to successfully complete the ${taskType} task?
375
+
376
+ Consider:
377
+ - For README: Do you understand the project's purpose, main features, API surface, and usage patterns?
378
+ - For commit: Do you understand what changed and why?
379
+ - For description: Do you understand the project's core value proposition?
380
+
381
+ Respond in JSON format:
382
+ {
383
+ "sufficient": true or false,
384
+ "reasoning": "Detailed explanation of your decision"
385
+ }`;
386
+
387
+ const response = await this.openaiInstance.chat({
388
+ systemMessage: `You are an AI assistant that evaluates whether gathered context is sufficient for a task.
389
+ You must respond ONLY with valid JSON that can be parsed with JSON.parse().
390
+ Do not wrap the JSON in markdown code blocks or add any other text.`,
391
+ userMessage: prompt,
392
+ messageHistory: [],
393
+ });
394
+
395
+ // Parse JSON response, handling potential markdown formatting
396
+ const content = response.message.replace('```json', '').replace('```', '').trim();
397
+ const parsed = JSON.parse(content);
398
+
399
+ return {
400
+ sufficient: parsed.sufficient || false,
401
+ reasoning: parsed.reasoning || 'No reasoning provided',
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Load a single file with caching
407
+ */
408
+ private async loadFile(filePath: string): Promise<IFileInfo> {
409
+ // Try cache first
410
+ const cached = await this.cache.get(filePath);
411
+ if (cached) {
412
+ return {
413
+ path: filePath,
414
+ relativePath: plugins.path.relative(this.projectRoot, filePath),
415
+ contents: cached.contents,
416
+ tokenCount: cached.tokenCount,
417
+ };
418
+ }
419
+
420
+ // Load from disk
421
+ const contents = await plugins.smartfile.fs.toStringSync(filePath);
422
+ const tokenCount = this.countTokens(contents);
423
+ const relativePath = plugins.path.relative(this.projectRoot, filePath);
424
+
425
+ // Cache it
426
+ const stats = await fs.promises.stat(filePath);
427
+ await this.cache.set({
428
+ path: filePath,
429
+ contents,
430
+ tokenCount,
431
+ mtime: Math.floor(stats.mtimeMs),
432
+ cachedAt: Date.now(),
433
+ });
434
+
435
+ return {
436
+ path: filePath,
437
+ relativePath,
438
+ contents,
439
+ tokenCount,
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Format a file for inclusion in context
445
+ */
446
+ private formatFileForContext(file: IFileInfo): string {
447
+ return `
448
+ ====== START OF FILE ${file.relativePath} ======
449
+
450
+ ${file.contents}
451
+
452
+ ====== END OF FILE ${file.relativePath} ======
453
+ `;
454
+ }
455
+
456
+ /**
457
+ * Count tokens in text
458
+ */
459
+ private countTokens(text: string): number {
460
+ try {
461
+ const tokens = plugins.gptTokenizer.encode(text);
462
+ return tokens.length;
463
+ } catch (error) {
464
+ return Math.ceil(text.length / 4);
465
+ }
466
+ }
467
+ }