@git.zone/tsdoc 1.5.2 → 1.6.1

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.
@@ -1,7 +1,10 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { ContextMode, IContextResult, IFileInfo, TaskType } from './types.js';
2
+ import type { ContextMode, IContextResult, IFileInfo, TaskType, IFileMetadata } from './types.js';
3
3
  import { ContextTrimmer } from './context-trimmer.js';
4
4
  import { ConfigManager } from './config-manager.js';
5
+ import { LazyFileLoader } from './lazy-file-loader.js';
6
+ import { ContextCache } from './context-cache.js';
7
+ import { ContextAnalyzer } from './context-analyzer.js';
5
8
 
6
9
  /**
7
10
  * Enhanced ProjectContext that supports context optimization strategies
@@ -10,6 +13,9 @@ export class EnhancedContext {
10
13
  private projectDir: string;
11
14
  private trimmer: ContextTrimmer;
12
15
  private configManager: ConfigManager;
16
+ private lazyLoader: LazyFileLoader;
17
+ private cache: ContextCache;
18
+ private analyzer: ContextAnalyzer;
13
19
  private contextMode: ContextMode = 'trimmed';
14
20
  private tokenBudget: number = 190000; // Default for o4-mini
15
21
  private contextResult: IContextResult = {
@@ -29,6 +35,13 @@ export class EnhancedContext {
29
35
  this.projectDir = projectDirArg;
30
36
  this.configManager = ConfigManager.getInstance();
31
37
  this.trimmer = new ContextTrimmer(this.configManager.getTrimConfig());
38
+ this.lazyLoader = new LazyFileLoader(projectDirArg);
39
+ this.cache = new ContextCache(projectDirArg, this.configManager.getCacheConfig());
40
+ this.analyzer = new ContextAnalyzer(
41
+ projectDirArg,
42
+ this.configManager.getPrioritizationWeights(),
43
+ this.configManager.getTierConfig()
44
+ );
32
45
  }
33
46
 
34
47
  /**
@@ -38,6 +51,7 @@ export class EnhancedContext {
38
51
  await this.configManager.initialize(this.projectDir);
39
52
  this.tokenBudget = this.configManager.getMaxTokens();
40
53
  this.trimmer.updateConfig(this.configManager.getTrimConfig());
54
+ await this.cache.init();
41
55
  }
42
56
 
43
57
  /**
@@ -55,75 +69,17 @@ export class EnhancedContext {
55
69
  public setTokenBudget(maxTokens: number): void {
56
70
  this.tokenBudget = maxTokens;
57
71
  }
58
-
59
- /**
60
- * Gather files from the project
61
- * @param includePaths Optional paths to include
62
- * @param excludePaths Optional paths to exclude
63
- */
64
- public async gatherFiles(includePaths?: string[], excludePaths?: string[]): Promise<Record<string, plugins.smartfile.SmartFile | plugins.smartfile.SmartFile[]>> {
65
- const smartfilePackageJSON = await plugins.smartfile.SmartFile.fromFilePath(
66
- plugins.path.join(this.projectDir, 'package.json'),
67
- this.projectDir,
68
- );
69
-
70
- const smartfilesReadme = await plugins.smartfile.SmartFile.fromFilePath(
71
- plugins.path.join(this.projectDir, 'readme.md'),
72
- this.projectDir,
73
- );
74
72
 
75
- const smartfilesReadmeHints = await plugins.smartfile.SmartFile.fromFilePath(
76
- plugins.path.join(this.projectDir, 'readme.hints.md'),
77
- this.projectDir,
78
- );
79
-
80
- const smartfilesNpmextraJSON = await plugins.smartfile.SmartFile.fromFilePath(
81
- plugins.path.join(this.projectDir, 'npmextra.json'),
82
- this.projectDir,
83
- );
84
-
85
- // Use provided include paths or default to all TypeScript files
86
- const includeGlobs = includePaths?.map(path => `${path}/**/*.ts`) || ['ts*/**/*.ts'];
87
-
88
- // Get TypeScript files
89
- const smartfilesModPromises = includeGlobs.map(glob =>
90
- plugins.smartfile.fs.fileTreeToObject(this.projectDir, glob)
91
- );
92
-
93
- const smartfilesModArrays = await Promise.all(smartfilesModPromises);
94
-
95
- // Flatten the arrays
96
- const smartfilesMod: plugins.smartfile.SmartFile[] = [];
97
- smartfilesModArrays.forEach(array => {
98
- smartfilesMod.push(...array);
99
- });
100
-
101
- // Get test files if not excluded
102
- let smartfilesTest: plugins.smartfile.SmartFile[] = [];
103
- if (!excludePaths?.includes('test/')) {
104
- smartfilesTest = await plugins.smartfile.fs.fileTreeToObject(
105
- this.projectDir,
106
- 'test/**/*.ts',
107
- );
108
- }
109
-
110
- return {
111
- smartfilePackageJSON,
112
- smartfilesReadme,
113
- smartfilesReadmeHints,
114
- smartfilesNpmextraJSON,
115
- smartfilesMod,
116
- smartfilesTest,
117
- };
118
- }
119
-
120
73
  /**
121
- * Convert files to context string
122
- * @param files The files to convert
123
- * @param mode The context mode to use
74
+ * Convert files to context with smart analysis and prioritization
75
+ * @param metadata - File metadata to analyze
76
+ * @param taskType - Task type for context-aware prioritization
77
+ * @param mode - Context mode to use
78
+ * @returns Context string
124
79
  */
125
- public async convertFilesToContext(
126
- files: plugins.smartfile.SmartFile[],
80
+ public async convertFilesToContextWithAnalysis(
81
+ metadata: IFileMetadata[],
82
+ taskType: TaskType,
127
83
  mode: ContextMode = this.contextMode
128
84
  ): Promise<string> {
129
85
  // Reset context result
@@ -135,140 +91,173 @@ export class EnhancedContext {
135
91
  excludedFiles: [],
136
92
  tokenSavings: 0
137
93
  };
138
-
94
+
95
+ // Analyze files for smart prioritization
96
+ const analysis = await this.analyzer.analyze(metadata, taskType, []);
97
+
98
+ // Sort files by importance score (highest first)
99
+ const sortedAnalysis = [...analysis.files].sort(
100
+ (a, b) => b.importanceScore - a.importanceScore
101
+ );
102
+
103
+ // Filter out excluded tier
104
+ const relevantFiles = sortedAnalysis.filter(f => f.tier !== 'excluded');
105
+
139
106
  let totalTokenCount = 0;
140
107
  let totalOriginalTokens = 0;
141
-
142
- // Sort files by importance (for now just a simple alphabetical sort)
143
- // Later this could be enhanced with more sophisticated prioritization
144
- const sortedFiles = [...files].sort((a, b) => a.relative.localeCompare(b.relative));
145
-
146
108
  const processedFiles: string[] = [];
147
-
148
- for (const smartfile of sortedFiles) {
149
- // Calculate original token count
150
- const originalContent = smartfile.contents.toString();
151
- const originalTokenCount = this.countTokens(originalContent);
152
- totalOriginalTokens += originalTokenCount;
153
-
154
- // Apply trimming based on mode
155
- let processedContent = originalContent;
156
-
157
- if (mode !== 'full') {
158
- processedContent = this.trimmer.trimFile(
159
- smartfile.relative,
160
- originalContent,
161
- mode
162
- );
163
- }
164
-
165
- // Calculate new token count
166
- const processedTokenCount = this.countTokens(processedContent);
167
-
168
- // Check if we have budget for this file
169
- if (totalTokenCount + processedTokenCount > this.tokenBudget) {
170
- // We don't have budget for this file
171
- this.contextResult.excludedFiles.push({
172
- path: smartfile.path,
173
- contents: originalContent,
174
- relativePath: smartfile.relative,
175
- tokenCount: originalTokenCount
176
- });
177
- continue;
178
- }
179
-
180
- // Format the file for context
181
- const formattedContent = `
182
- ====== START OF FILE ${smartfile.relative} ======
183
-
109
+
110
+ // Load files with cache support
111
+ for (const fileAnalysis of relevantFiles) {
112
+ try {
113
+ // Check cache first
114
+ let contents: string;
115
+ let originalTokenCount: number;
116
+
117
+ const cached = await this.cache.get(fileAnalysis.path);
118
+ if (cached) {
119
+ contents = cached.contents;
120
+ originalTokenCount = cached.tokenCount;
121
+ } else {
122
+ // Load file
123
+ const fileData = await plugins.smartfile.fs.toStringSync(fileAnalysis.path);
124
+ contents = fileData;
125
+ originalTokenCount = this.countTokens(contents);
126
+
127
+ // Cache it
128
+ await this.cache.set({
129
+ path: fileAnalysis.path,
130
+ contents,
131
+ tokenCount: originalTokenCount,
132
+ mtime: Date.now(),
133
+ cachedAt: Date.now()
134
+ });
135
+ }
136
+
137
+ totalOriginalTokens += originalTokenCount;
138
+
139
+ // Apply tier-based trimming
140
+ let processedContent = contents;
141
+ let trimLevel: 'none' | 'light' | 'aggressive' = 'light';
142
+
143
+ if (fileAnalysis.tier === 'essential') {
144
+ trimLevel = 'none';
145
+ } else if (fileAnalysis.tier === 'important') {
146
+ trimLevel = 'light';
147
+ } else if (fileAnalysis.tier === 'optional') {
148
+ trimLevel = 'aggressive';
149
+ }
150
+
151
+ // Apply trimming based on mode and tier
152
+ if (mode !== 'full' && trimLevel !== 'none') {
153
+ const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
154
+ processedContent = this.trimmer.trimFileWithLevel(
155
+ relativePath,
156
+ contents,
157
+ trimLevel
158
+ );
159
+ }
160
+
161
+ // Calculate token count
162
+ const processedTokenCount = this.countTokens(processedContent);
163
+
164
+ // Check token budget
165
+ if (totalTokenCount + processedTokenCount > this.tokenBudget) {
166
+ // We don't have budget for this file
167
+ const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
168
+ this.contextResult.excludedFiles.push({
169
+ path: fileAnalysis.path,
170
+ contents,
171
+ relativePath,
172
+ tokenCount: originalTokenCount,
173
+ importanceScore: fileAnalysis.importanceScore
174
+ });
175
+ continue;
176
+ }
177
+
178
+ // Format the file for context
179
+ const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
180
+ const formattedContent = `
181
+ ====== START OF FILE ${relativePath} ======
182
+
184
183
  ${processedContent}
185
-
186
- ====== END OF FILE ${smartfile.relative} ======
187
- `;
188
-
189
- processedFiles.push(formattedContent);
190
- totalTokenCount += processedTokenCount;
191
-
192
- // Track file in appropriate list
193
- const fileInfo: IFileInfo = {
194
- path: smartfile.path,
195
- contents: processedContent,
196
- relativePath: smartfile.relative,
197
- tokenCount: processedTokenCount
198
- };
199
-
200
- if (mode === 'full' || processedContent === originalContent) {
201
- this.contextResult.includedFiles.push(fileInfo);
202
- } else {
203
- this.contextResult.trimmedFiles.push(fileInfo);
204
- this.contextResult.tokenSavings += (originalTokenCount - processedTokenCount);
184
+
185
+ ====== END OF FILE ${relativePath} ======
186
+ `;
187
+
188
+ processedFiles.push(formattedContent);
189
+ totalTokenCount += processedTokenCount;
190
+
191
+ // Track file in appropriate list
192
+ const fileInfo: IFileInfo = {
193
+ path: fileAnalysis.path,
194
+ contents: processedContent,
195
+ relativePath,
196
+ tokenCount: processedTokenCount,
197
+ importanceScore: fileAnalysis.importanceScore
198
+ };
199
+
200
+ if (trimLevel === 'none' || processedContent === contents) {
201
+ this.contextResult.includedFiles.push(fileInfo);
202
+ } else {
203
+ this.contextResult.trimmedFiles.push(fileInfo);
204
+ this.contextResult.tokenSavings += (originalTokenCount - processedTokenCount);
205
+ }
206
+ } catch (error) {
207
+ console.warn(`Failed to process file ${fileAnalysis.path}:`, error.message);
205
208
  }
206
209
  }
207
-
210
+
208
211
  // Join all processed files
209
212
  const context = processedFiles.join('\n');
210
-
213
+
211
214
  // Update context result
212
215
  this.contextResult.context = context;
213
216
  this.contextResult.tokenCount = totalTokenCount;
214
-
217
+
215
218
  return context;
216
219
  }
217
-
220
+
218
221
  /**
219
- * Build context for the project
220
- * @param taskType Optional task type for task-specific context
222
+ * Build context for the project using smart analysis
223
+ * @param taskType Task type for context-aware prioritization (defaults to 'description')
221
224
  */
222
225
  public async buildContext(taskType?: TaskType): Promise<IContextResult> {
223
226
  // Initialize if needed
224
227
  if (this.tokenBudget === 0) {
225
228
  await this.initialize();
226
229
  }
227
-
228
- // Get task-specific configuration if a task type is provided
229
- if (taskType) {
230
- const taskConfig = this.configManager.getTaskConfig(taskType);
231
- if (taskConfig.mode) {
232
- this.setContextMode(taskConfig.mode);
233
- }
234
- }
235
-
236
- // Gather files
237
- const taskConfig = taskType ? this.configManager.getTaskConfig(taskType) : undefined;
238
- const files = await this.gatherFiles(
239
- taskConfig?.includePaths,
240
- taskConfig?.excludePaths
241
- );
242
-
243
- // Convert files to context
244
- // Create an array of all files to process
245
- const allFiles: plugins.smartfile.SmartFile[] = [];
246
-
247
- // Add individual files
248
- if (files.smartfilePackageJSON) allFiles.push(files.smartfilePackageJSON as plugins.smartfile.SmartFile);
249
- if (files.smartfilesReadme) allFiles.push(files.smartfilesReadme as plugins.smartfile.SmartFile);
250
- if (files.smartfilesReadmeHints) allFiles.push(files.smartfilesReadmeHints as plugins.smartfile.SmartFile);
251
- if (files.smartfilesNpmextraJSON) allFiles.push(files.smartfilesNpmextraJSON as plugins.smartfile.SmartFile);
252
-
253
- // Add arrays of files
254
- if (files.smartfilesMod) {
255
- if (Array.isArray(files.smartfilesMod)) {
256
- allFiles.push(...files.smartfilesMod);
257
- } else {
258
- allFiles.push(files.smartfilesMod);
259
- }
260
- }
261
-
262
- if (files.smartfilesTest) {
263
- if (Array.isArray(files.smartfilesTest)) {
264
- allFiles.push(...files.smartfilesTest);
265
- } else {
266
- allFiles.push(files.smartfilesTest);
267
- }
230
+
231
+ // Smart context building always requires a task type for optimal prioritization
232
+ // Default to 'description' if not provided
233
+ const effectiveTaskType = taskType || 'description';
234
+
235
+ // Get task-specific configuration
236
+ const taskConfig = this.configManager.getTaskConfig(effectiveTaskType);
237
+ if (taskConfig.mode) {
238
+ this.setContextMode(taskConfig.mode);
268
239
  }
269
-
270
- const context = await this.convertFilesToContext(allFiles);
271
-
240
+
241
+ // Build globs for scanning
242
+ const includeGlobs = taskConfig?.includePaths?.map(p => `${p}/**/*.ts`) || [
243
+ 'ts/**/*.ts',
244
+ 'ts*/**/*.ts'
245
+ ];
246
+
247
+ // Add config files
248
+ const configGlobs = [
249
+ 'package.json',
250
+ 'readme.md',
251
+ 'readme.hints.md',
252
+ 'npmextra.json'
253
+ ];
254
+
255
+ // Scan files for metadata (fast, doesn't load contents)
256
+ const metadata = await this.lazyLoader.scanFiles([...configGlobs, ...includeGlobs]);
257
+
258
+ // Use smart analyzer to build context with intelligent prioritization
259
+ await this.convertFilesToContextWithAnalysis(metadata, effectiveTaskType, this.contextMode);
260
+
272
261
  return this.contextResult;
273
262
  }
274
263
 
@@ -2,14 +2,27 @@ import { EnhancedContext } from './enhanced-context.js';
2
2
  import { TaskContextFactory } from './task-context-factory.js';
3
3
  import { ConfigManager } from './config-manager.js';
4
4
  import { ContextTrimmer } from './context-trimmer.js';
5
- import type {
6
- ContextMode,
7
- IContextConfig,
5
+ import { LazyFileLoader } from './lazy-file-loader.js';
6
+ import { ContextCache } from './context-cache.js';
7
+ import { ContextAnalyzer } from './context-analyzer.js';
8
+ import type {
9
+ ContextMode,
10
+ IContextConfig,
8
11
  IContextResult,
9
12
  IFileInfo,
10
13
  ITrimConfig,
11
14
  ITaskConfig,
12
- TaskType
15
+ TaskType,
16
+ ICacheConfig,
17
+ IAnalyzerConfig,
18
+ IPrioritizationWeights,
19
+ ITierConfig,
20
+ ITierSettings,
21
+ IFileMetadata,
22
+ ICacheEntry,
23
+ IFileDependencies,
24
+ IFileAnalysis,
25
+ IAnalysisResult
13
26
  } from './types.js';
14
27
 
15
28
  export {
@@ -18,6 +31,9 @@ export {
18
31
  TaskContextFactory,
19
32
  ConfigManager,
20
33
  ContextTrimmer,
34
+ LazyFileLoader,
35
+ ContextCache,
36
+ ContextAnalyzer,
21
37
  };
22
38
 
23
39
  // Types
@@ -28,5 +44,15 @@ export type {
28
44
  IFileInfo,
29
45
  ITrimConfig,
30
46
  ITaskConfig,
31
- TaskType
47
+ TaskType,
48
+ ICacheConfig,
49
+ IAnalyzerConfig,
50
+ IPrioritizationWeights,
51
+ ITierConfig,
52
+ ITierSettings,
53
+ IFileMetadata,
54
+ ICacheEntry,
55
+ IFileDependencies,
56
+ IFileAnalysis,
57
+ IAnalysisResult
32
58
  };
@@ -0,0 +1,191 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as fs from 'fs';
3
+ import type { IFileMetadata, IFileInfo } from './types.js';
4
+
5
+ /**
6
+ * LazyFileLoader handles efficient file loading by:
7
+ * - Scanning files for metadata without loading contents
8
+ * - Providing fast file size and token estimates
9
+ * - Loading contents only when requested
10
+ * - Parallel loading of selected files
11
+ */
12
+ export class LazyFileLoader {
13
+ private projectRoot: string;
14
+ private metadataCache: Map<string, IFileMetadata> = new Map();
15
+
16
+ /**
17
+ * Creates a new LazyFileLoader
18
+ * @param projectRoot - Root directory of the project
19
+ */
20
+ constructor(projectRoot: string) {
21
+ this.projectRoot = projectRoot;
22
+ }
23
+
24
+ /**
25
+ * Scans files in given globs and creates metadata without loading contents
26
+ * @param globs - File patterns to scan (e.g., ['ts/**\/*.ts', 'test/**\/*.ts'])
27
+ * @returns Array of file metadata
28
+ */
29
+ public async scanFiles(globs: string[]): Promise<IFileMetadata[]> {
30
+ const metadata: IFileMetadata[] = [];
31
+
32
+ for (const globPattern of globs) {
33
+ try {
34
+ const smartFiles = await plugins.smartfile.fs.fileTreeToObject(this.projectRoot, globPattern);
35
+ const fileArray = Array.isArray(smartFiles) ? smartFiles : [smartFiles];
36
+
37
+ for (const smartFile of fileArray) {
38
+ try {
39
+ const meta = await this.getMetadata(smartFile.path);
40
+ metadata.push(meta);
41
+ } catch (error) {
42
+ // Skip files that can't be read
43
+ console.warn(`Failed to get metadata for ${smartFile.path}:`, error.message);
44
+ }
45
+ }
46
+ } catch (error) {
47
+ // Skip patterns that don't match any files
48
+ console.warn(`No files found for pattern ${globPattern}`);
49
+ }
50
+ }
51
+
52
+ return metadata;
53
+ }
54
+
55
+ /**
56
+ * Gets metadata for a single file without loading contents
57
+ * @param filePath - Absolute path to the file
58
+ * @returns File metadata
59
+ */
60
+ public async getMetadata(filePath: string): Promise<IFileMetadata> {
61
+ // Check cache first
62
+ if (this.metadataCache.has(filePath)) {
63
+ const cached = this.metadataCache.get(filePath)!;
64
+ const currentStats = await fs.promises.stat(filePath);
65
+
66
+ // Return cached if file hasn't changed
67
+ if (cached.mtime === Math.floor(currentStats.mtimeMs)) {
68
+ return cached;
69
+ }
70
+ }
71
+
72
+ // Get file stats
73
+ const stats = await fs.promises.stat(filePath);
74
+ const relativePath = plugins.path.relative(this.projectRoot, filePath);
75
+
76
+ // Estimate tokens: rough estimate of ~4 characters per token
77
+ // This is faster than reading and tokenizing the entire file
78
+ const estimatedTokens = Math.ceil(stats.size / 4);
79
+
80
+ const metadata: IFileMetadata = {
81
+ path: filePath,
82
+ relativePath,
83
+ size: stats.size,
84
+ mtime: Math.floor(stats.mtimeMs),
85
+ estimatedTokens,
86
+ };
87
+
88
+ // Cache the metadata
89
+ this.metadataCache.set(filePath, metadata);
90
+
91
+ return metadata;
92
+ }
93
+
94
+ /**
95
+ * Loads file contents for selected files in parallel
96
+ * @param metadata - Array of file metadata to load
97
+ * @param tokenizer - Function to calculate accurate token count
98
+ * @returns Array of complete file info with contents
99
+ */
100
+ public async loadFiles(
101
+ metadata: IFileMetadata[],
102
+ tokenizer: (content: string) => number
103
+ ): Promise<IFileInfo[]> {
104
+ // Load files in parallel
105
+ const loadPromises = metadata.map(async (meta) => {
106
+ try {
107
+ const contents = await plugins.smartfile.fs.toStringSync(meta.path);
108
+ const tokenCount = tokenizer(contents);
109
+
110
+ const fileInfo: IFileInfo = {
111
+ path: meta.path,
112
+ relativePath: meta.relativePath,
113
+ contents,
114
+ tokenCount,
115
+ importanceScore: meta.importanceScore,
116
+ };
117
+
118
+ return fileInfo;
119
+ } catch (error) {
120
+ console.warn(`Failed to load file ${meta.path}:`, error.message);
121
+ return null;
122
+ }
123
+ });
124
+
125
+ // Wait for all loads to complete and filter out failures
126
+ const results = await Promise.all(loadPromises);
127
+ return results.filter((r): r is IFileInfo => r !== null);
128
+ }
129
+
130
+ /**
131
+ * Loads a single file with contents
132
+ * @param filePath - Absolute path to the file
133
+ * @param tokenizer - Function to calculate accurate token count
134
+ * @returns Complete file info with contents
135
+ */
136
+ public async loadFile(
137
+ filePath: string,
138
+ tokenizer: (content: string) => number
139
+ ): Promise<IFileInfo> {
140
+ const meta = await this.getMetadata(filePath);
141
+ const contents = await plugins.smartfile.fs.toStringSync(filePath);
142
+ const tokenCount = tokenizer(contents);
143
+ const relativePath = plugins.path.relative(this.projectRoot, filePath);
144
+
145
+ return {
146
+ path: filePath,
147
+ relativePath,
148
+ contents,
149
+ tokenCount,
150
+ importanceScore: meta.importanceScore,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Updates importance scores for metadata entries
156
+ * @param scores - Map of file paths to importance scores
157
+ */
158
+ public updateImportanceScores(scores: Map<string, number>): void {
159
+ for (const [path, score] of scores) {
160
+ const meta = this.metadataCache.get(path);
161
+ if (meta) {
162
+ meta.importanceScore = score;
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Clears the metadata cache
169
+ */
170
+ public clearCache(): void {
171
+ this.metadataCache.clear();
172
+ }
173
+
174
+ /**
175
+ * Gets total estimated tokens for all cached metadata
176
+ */
177
+ public getTotalEstimatedTokens(): number {
178
+ let total = 0;
179
+ for (const meta of this.metadataCache.values()) {
180
+ total += meta.estimatedTokens;
181
+ }
182
+ return total;
183
+ }
184
+
185
+ /**
186
+ * Gets cached metadata entries
187
+ */
188
+ public getCachedMetadata(): IFileMetadata[] {
189
+ return Array.from(this.metadataCache.values());
190
+ }
191
+ }