ctxman 3.0.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 (190) hide show
  1. package/.ctxman/custom-profiles.example.json +20 -0
  2. package/.ctxman/llm-profiles.json +131 -0
  3. package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
  4. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  5. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  6. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  7. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  8. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  9. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  10. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  11. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  12. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  13. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  14. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  15. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  16. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  17. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  18. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  19. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  20. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  21. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  22. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  23. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  24. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  25. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  26. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  27. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  28. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  29. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  30. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  31. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  32. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  33. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  34. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  35. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  36. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  37. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  38. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  39. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  40. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  41. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  42. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  43. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  44. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  45. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  46. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  47. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  48. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  49. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  50. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  51. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  52. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  53. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  54. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  55. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  56. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  57. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  58. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  59. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  60. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  61. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  62. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  63. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  64. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  65. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  66. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
  67. package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
  68. package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
  69. package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
  70. package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
  71. package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
  72. package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
  73. package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
  74. package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
  75. package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
  76. package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
  77. package/.ctxman/wizard-profiles/full/.contextignore +23 -0
  78. package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
  79. package/.ctxman/wizard-profiles/full/.methodignore +5 -0
  80. package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
  81. package/.ctxman/wizard-profiles/full/profile.json +28 -0
  82. package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
  83. package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
  84. package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
  85. package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
  86. package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
  87. package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
  88. package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
  89. package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
  90. package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
  91. package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
  92. package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
  93. package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
  94. package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
  95. package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
  96. package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
  97. package/README.md +1167 -0
  98. package/bin/cli.js +650 -0
  99. package/bin/cm-ask.js +95 -0
  100. package/bin/cm-gitingest.js +134 -0
  101. package/bin/cm-update.js +175 -0
  102. package/bin/mcp-server.js +31 -0
  103. package/ctxman.js +236 -0
  104. package/examples/README.md +298 -0
  105. package/examples/custom-llm-profiles.example.json +19 -0
  106. package/examples/wizard-profiles/code-review/.contextignore +41 -0
  107. package/examples/wizard-profiles/code-review/.contextinclude +30 -0
  108. package/examples/wizard-profiles/code-review/.methodignore +26 -0
  109. package/examples/wizard-profiles/code-review/.methodinclude +47 -0
  110. package/examples/wizard-profiles/code-review/profile.json +26 -0
  111. package/examples/wizard-profiles/documentation/.contextignore +21 -0
  112. package/examples/wizard-profiles/documentation/.contextinclude +24 -0
  113. package/examples/wizard-profiles/documentation/.methodignore +19 -0
  114. package/examples/wizard-profiles/documentation/.methodinclude +44 -0
  115. package/examples/wizard-profiles/documentation/profile.json +27 -0
  116. package/examples/wizard-profiles/full/.contextignore +23 -0
  117. package/examples/wizard-profiles/full/.contextinclude +38 -0
  118. package/examples/wizard-profiles/full/.methodignore +5 -0
  119. package/examples/wizard-profiles/full/.methodinclude +5 -0
  120. package/examples/wizard-profiles/full/profile.json +28 -0
  121. package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
  122. package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
  123. package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
  124. package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
  125. package/examples/wizard-profiles/llm-explain/profile.json +27 -0
  126. package/examples/wizard-profiles/minimal/.contextignore +27 -0
  127. package/examples/wizard-profiles/minimal/.contextinclude +14 -0
  128. package/examples/wizard-profiles/minimal/.methodignore +22 -0
  129. package/examples/wizard-profiles/minimal/.methodinclude +18 -0
  130. package/examples/wizard-profiles/minimal/profile.json +26 -0
  131. package/examples/wizard-profiles/security-audit/.contextignore +36 -0
  132. package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
  133. package/examples/wizard-profiles/security-audit/.methodignore +29 -0
  134. package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
  135. package/examples/wizard-profiles/security-audit/profile.json +34 -0
  136. package/index.js +68 -0
  137. package/lib/agents/pr-failure-agent.js +201 -0
  138. package/lib/analyzers/go-method-analyzer.js +126 -0
  139. package/lib/analyzers/method-analyzer.js +457 -0
  140. package/lib/analyzers/token-calculator.js +577 -0
  141. package/lib/api/mcp/prompts.js +319 -0
  142. package/lib/api/mcp/resources.js +269 -0
  143. package/lib/api/mcp/server.js +339 -0
  144. package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
  145. package/lib/api/rest/server.js +405 -0
  146. package/lib/cache/CacheManager.js +388 -0
  147. package/lib/core/Analyzer.js +180 -0
  148. package/lib/core/ContextBuilder.js +325 -0
  149. package/lib/core/Reporter.js +270 -0
  150. package/lib/core/Scanner.js +129 -0
  151. package/lib/formatters/format-registry.js +419 -0
  152. package/lib/formatters/gitingest-formatter.js +746 -0
  153. package/lib/formatters/toon-formatter-v1.3.js +731 -0
  154. package/lib/formatters/toon-formatter.js +291 -0
  155. package/lib/integrations/git/BlameTracker.js +142 -0
  156. package/lib/integrations/git/DiffAnalyzer.js +260 -0
  157. package/lib/integrations/git/GitClient.js +336 -0
  158. package/lib/parsers/gitignore-parser.js +127 -0
  159. package/lib/parsers/method-filter-parser.js +50 -0
  160. package/lib/plugins/ExporterPlugin.js +102 -0
  161. package/lib/plugins/LanguagePlugin.js +100 -0
  162. package/lib/plugins/PluginManager.js +302 -0
  163. package/lib/rag/DataSourcePlugin.js +31 -0
  164. package/lib/rag/EmbeddingProvider.js +75 -0
  165. package/lib/rag/Indexer.js +83 -0
  166. package/lib/rag/VectorStoreFactory.js +24 -0
  167. package/lib/rag/sources/FileDataSource.js +70 -0
  168. package/lib/rag/stores/BaseVectorStore.js +43 -0
  169. package/lib/rag/stores/ChromaDBStore.js +85 -0
  170. package/lib/rag/stores/LanceDBStore.js +109 -0
  171. package/lib/rag/stores/PineconeStore.js +74 -0
  172. package/lib/ui/dashboard.js +109 -0
  173. package/lib/ui/index.js +15 -0
  174. package/lib/ui/progress-bar.js +110 -0
  175. package/lib/ui/select-input.js +51 -0
  176. package/lib/ui/wizard.js +359 -0
  177. package/lib/utils/clipboard-utils.js +66 -0
  178. package/lib/utils/config-utils.js +94 -0
  179. package/lib/utils/error-handler.js +145 -0
  180. package/lib/utils/file-utils.js +89 -0
  181. package/lib/utils/format-converter.js +325 -0
  182. package/lib/utils/git-utils.js +337 -0
  183. package/lib/utils/llm-detector.js +409 -0
  184. package/lib/utils/logger.js +278 -0
  185. package/lib/utils/token-utils.js +200 -0
  186. package/lib/utils/tokenizer-adapter.js +454 -0
  187. package/lib/utils/updater.js +565 -0
  188. package/lib/watch/FileWatcher.js +208 -0
  189. package/lib/watch/IncrementalAnalyzer.js +206 -0
  190. package/package.json +118 -0
@@ -0,0 +1,746 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import MethodAnalyzer from '../analyzers/method-analyzer.js';
4
+ import MethodFilterParser from '../parsers/method-filter-parser.js';
5
+ import ConfigUtils from '../utils/config-utils.js';
6
+ import TokenUtils from '../utils/token-utils.js';
7
+ import FileUtils from '../utils/file-utils.js';
8
+
9
+ /**
10
+ * GitIngest-style Digest Formatter
11
+ * Formats analyzed code into a single prompt-friendly text file
12
+ * Automatically applies method-level filtering if .methodinclude or .methodignore exists
13
+ *
14
+ * v2.3.0 Features:
15
+ * - Smart chunking for large repositories
16
+ * - Multiple chunking strategies (smart, size, file, directory, dependency)
17
+ * - Cross-chunk reference preservation
18
+ * - Chunk metadata and navigation
19
+ */
20
+ class GitIngestFormatter {
21
+ constructor(projectRoot, stats, analysisResults, options = {}) {
22
+ this.projectRoot = projectRoot;
23
+ this.stats = stats;
24
+ this.analysisResults = analysisResults;
25
+ this.options = options;
26
+
27
+ // Chunking configuration (v2.3.3: Enhanced)
28
+ this.chunking = {
29
+ enabled: options.chunking?.enabled || false,
30
+ strategy: options.chunking?.strategy || 'smart',
31
+ maxTokensPerChunk: options.chunking?.maxTokensPerChunk || 100000,
32
+ overlap: options.chunking?.overlap || 500,
33
+ preserveContext: options.chunking?.preserveContext !== false,
34
+ includeMetadata: options.chunking?.includeMetadata !== false, // v2.3.3
35
+ crossReferences: options.chunking?.crossReferences !== false // v2.3.3
36
+ };
37
+
38
+ // Auto-detect method filtering configuration
39
+ this.methodFilterEnabled = this.detectMethodFilters();
40
+ if (this.methodFilterEnabled) {
41
+ this.methodAnalyzer = new MethodAnalyzer();
42
+ this.methodFilter = this.initMethodFilter();
43
+ }
44
+ }
45
+
46
+ detectMethodFilters() {
47
+ return ConfigUtils.detectMethodFilters(this.projectRoot);
48
+ }
49
+
50
+ initMethodFilter() {
51
+ return ConfigUtils.initMethodFilter(this.projectRoot);
52
+ }
53
+
54
+ generateDigest() {
55
+ // Check if chunking is enabled
56
+ if (this.chunking.enabled) {
57
+ return this.generateChunkedDigest();
58
+ }
59
+
60
+ let digest = '';
61
+
62
+ // Header with project info
63
+ digest += this.generateSummary();
64
+ digest += '\n';
65
+
66
+ // Directory tree structure
67
+ digest += this.generateTree();
68
+ digest += '\n';
69
+
70
+ // File contents
71
+ digest += this.generateFileContents();
72
+
73
+ return digest;
74
+ }
75
+
76
+ /**
77
+ * Generate chunked digest for large repositories
78
+ * Returns array of chunk objects with metadata
79
+ * v2.3.3: Enhanced with overlap and cross-references
80
+ */
81
+ generateChunkedDigest() {
82
+ const chunks = this.createChunks();
83
+ const chunkData = [];
84
+
85
+ // Add overlap between chunks (v2.3.3)
86
+ if (this.chunking.overlap > 0) {
87
+ this.addChunkOverlap(chunks);
88
+ }
89
+
90
+ for (let i = 0; i < chunks.length; i++) {
91
+ const chunk = chunks[i];
92
+ const chunkId = `chunk-${i + 1}`;
93
+
94
+ let digest = '';
95
+
96
+ // Chunk header
97
+ digest += this.generateChunkHeader(i + 1, chunks.length, chunk);
98
+ digest += '\n';
99
+
100
+ // Summary for this chunk
101
+ digest += this.generateChunkSummary(chunk);
102
+ digest += '\n';
103
+
104
+ // Directory tree for files in this chunk
105
+ digest += this.generateChunkTree(chunk);
106
+ digest += '\n';
107
+
108
+ // Chunk metadata (v2.3.3)
109
+ if (this.chunking.includeMetadata) {
110
+ digest += this.generateChunkMetadata(chunk, i, chunks);
111
+ digest += '\n';
112
+ }
113
+
114
+ // File contents
115
+ digest += this.generateChunkFileContents(chunk);
116
+
117
+ // Navigation info
118
+ digest += '\n';
119
+ digest += this.generateNavigationInfo(i, chunks.length);
120
+
121
+ chunkData.push({
122
+ id: chunkId,
123
+ index: i + 1,
124
+ total: chunks.length,
125
+ content: digest,
126
+ files: chunk.files.map(f => f.relativePath),
127
+ tokens: chunk.tokens,
128
+ metadata: chunk.metadata || {},
129
+ hasOverlap: chunk.hasOverlap || false, // v2.3.3
130
+ overlapFiles: chunk.overlapFiles || [] // v2.3.3
131
+ });
132
+ }
133
+
134
+ return chunkData;
135
+ }
136
+
137
+ /**
138
+ * Add overlap between chunks
139
+ * v2.3.3: Ensures context continuity
140
+ */
141
+ addChunkOverlap(chunks) {
142
+ for (let i = 0; i < chunks.length - 1; i++) {
143
+ const currentChunk = chunks[i];
144
+ const nextChunk = chunks[i + 1];
145
+
146
+ // Get last N tokens worth of files from current chunk
147
+ const overlapFiles = this.getOverlapFiles(currentChunk, this.chunking.overlap);
148
+
149
+ if (overlapFiles.length > 0) {
150
+ // Prepend to next chunk
151
+ nextChunk.files = [...overlapFiles, ...nextChunk.files];
152
+ nextChunk.hasOverlap = true;
153
+ nextChunk.overlapFiles = overlapFiles.map(f => f.relativePath);
154
+
155
+ // Update token count
156
+ const overlapTokens = overlapFiles.reduce((sum, f) => sum + f.tokens, 0);
157
+ nextChunk.tokens += overlapTokens;
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get files for overlap based on token count
164
+ * v2.3.3
165
+ */
166
+ getOverlapFiles(chunk, targetOverlapTokens) {
167
+ const files = [...chunk.files].reverse(); // Start from end
168
+ const overlapFiles = [];
169
+ let tokenCount = 0;
170
+
171
+ for (const file of files) {
172
+ if (tokenCount >= targetOverlapTokens) {
173
+ break;
174
+ }
175
+ overlapFiles.unshift(file);
176
+ tokenCount += file.tokens;
177
+ }
178
+
179
+ return overlapFiles;
180
+ }
181
+
182
+ /**
183
+ * Generate chunk metadata
184
+ * v2.3.3: Language distribution, file types, etc.
185
+ */
186
+ generateChunkMetadata(chunk, index, allChunks) {
187
+ let metadata = '\n';
188
+ metadata += 'CHUNK METADATA\n';
189
+ metadata += '='.repeat(60) + '\n';
190
+
191
+ // Language distribution
192
+ const languages = {};
193
+ for (const file of chunk.files) {
194
+ const ext = path.extname(file.relativePath) || 'no-ext';
195
+ languages[ext] = (languages[ext] || 0) + 1;
196
+ }
197
+
198
+ metadata += 'Languages:\n';
199
+ for (const [ext, count] of Object.entries(languages).sort((a, b) => b[1] - a[1])) {
200
+ metadata += ` ${ext}: ${count} files\n`;
201
+ }
202
+
203
+ // Directories covered
204
+ const dirs = new Set(chunk.files.map(f => path.dirname(f.relativePath)));
205
+ metadata += `\nDirectories: ${dirs.size}\n`;
206
+
207
+ // Cross-references (v2.3.3)
208
+ if (this.chunking.crossReferences && index > 0) {
209
+ const prevChunk = allChunks[index - 1];
210
+ const sharedDirs = this.getSharedDirectories(chunk, prevChunk);
211
+ if (sharedDirs.length > 0) {
212
+ metadata += `\nShared with previous chunk: ${sharedDirs.join(', ')}\n`;
213
+ }
214
+ }
215
+
216
+ return metadata;
217
+ }
218
+
219
+ /**
220
+ * Get shared directories between chunks
221
+ * v2.3.3
222
+ */
223
+ getSharedDirectories(chunk1, chunk2) {
224
+ const dirs1 = new Set(chunk1.files.map(f => path.dirname(f.relativePath)));
225
+ const dirs2 = new Set(chunk2.files.map(f => path.dirname(f.relativePath)));
226
+
227
+ return [...dirs1].filter(dir => dirs2.has(dir));
228
+ }
229
+
230
+ /**
231
+ * Create chunks based on selected strategy
232
+ */
233
+ createChunks() {
234
+ const strategy = this.chunking.strategy;
235
+
236
+ switch (strategy) {
237
+ case 'smart':
238
+ return this.createSmartChunks();
239
+ case 'size':
240
+ return this.createSizeBasedChunks();
241
+ case 'file':
242
+ return this.createFileBasedChunks();
243
+ case 'directory':
244
+ return this.createDirectoryBasedChunks();
245
+ case 'dependency':
246
+ return this.createDependencyBasedChunks();
247
+ default:
248
+ return this.createSizeBasedChunks();
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Smart chunking - AI-based semantic grouping
254
+ * Groups related files together based on directory and imports
255
+ */
256
+ createSmartChunks() {
257
+ const chunks = [];
258
+ const maxTokens = this.chunking.maxTokensPerChunk;
259
+
260
+ // Sort files by directory and then by token count
261
+ const sortedFiles = [...this.analysisResults].sort((a, b) => {
262
+ const dirA = path.dirname(a.relativePath);
263
+ const dirB = path.dirname(b.relativePath);
264
+ if (dirA !== dirB) {
265
+ return dirA.localeCompare(dirB);
266
+ }
267
+ return b.tokens - a.tokens;
268
+ });
269
+
270
+ let currentChunk = { files: [], tokens: 0, directories: new Set() };
271
+
272
+ for (const file of sortedFiles) {
273
+ const fileDir = path.dirname(file.relativePath);
274
+ const wouldExceedLimit = currentChunk.tokens + file.tokens > maxTokens;
275
+
276
+ // Start new chunk if:
277
+ // 1. Would exceed token limit
278
+ // 2. Different directory and chunk already has content
279
+ if (wouldExceedLimit ||
280
+ (currentChunk.files.length > 0 &&
281
+ !currentChunk.directories.has(fileDir) &&
282
+ currentChunk.tokens > maxTokens * 0.7)) {
283
+
284
+ if (currentChunk.files.length > 0) {
285
+ chunks.push(currentChunk);
286
+ }
287
+ currentChunk = { files: [], tokens: 0, directories: new Set() };
288
+ }
289
+
290
+ currentChunk.files.push(file);
291
+ currentChunk.tokens += file.tokens;
292
+ currentChunk.directories.add(fileDir);
293
+ }
294
+
295
+ // Add last chunk
296
+ if (currentChunk.files.length > 0) {
297
+ chunks.push(currentChunk);
298
+ }
299
+
300
+ return chunks;
301
+ }
302
+
303
+ /**
304
+ * Size-based chunking - Fixed token size chunks
305
+ */
306
+ createSizeBasedChunks() {
307
+ const chunks = [];
308
+ const maxTokens = this.chunking.maxTokensPerChunk;
309
+
310
+ // Sort files by token count (largest first)
311
+ const sortedFiles = [...this.analysisResults].sort((a, b) => b.tokens - a.tokens);
312
+
313
+ let currentChunk = { files: [], tokens: 0 };
314
+
315
+ for (const file of sortedFiles) {
316
+ if (currentChunk.tokens + file.tokens > maxTokens && currentChunk.files.length > 0) {
317
+ chunks.push(currentChunk);
318
+ currentChunk = { files: [], tokens: 0 };
319
+ }
320
+
321
+ currentChunk.files.push(file);
322
+ currentChunk.tokens += file.tokens;
323
+ }
324
+
325
+ if (currentChunk.files.length > 0) {
326
+ chunks.push(currentChunk);
327
+ }
328
+
329
+ return chunks;
330
+ }
331
+
332
+ /**
333
+ * File-based chunking - One file per chunk (with directory context)
334
+ */
335
+ createFileBasedChunks() {
336
+ const chunks = [];
337
+
338
+ for (const file of this.analysisResults) {
339
+ chunks.push({
340
+ files: [file],
341
+ tokens: file.tokens,
342
+ metadata: {
343
+ directory: path.dirname(file.relativePath)
344
+ }
345
+ });
346
+ }
347
+
348
+ return chunks;
349
+ }
350
+
351
+ /**
352
+ * Directory-based chunking - One directory per chunk
353
+ */
354
+ createDirectoryBasedChunks() {
355
+ const chunks = [];
356
+ const filesByDirectory = new Map();
357
+
358
+ // Group files by directory
359
+ for (const file of this.analysisResults) {
360
+ const dir = path.dirname(file.relativePath);
361
+ if (!filesByDirectory.has(dir)) {
362
+ filesByDirectory.set(dir, []);
363
+ }
364
+ filesByDirectory.get(dir).push(file);
365
+ }
366
+
367
+ // Create chunks from directories
368
+ for (const [dir, files] of filesByDirectory) {
369
+ const tokens = files.reduce((sum, f) => sum + f.tokens, 0);
370
+ chunks.push({
371
+ files,
372
+ tokens,
373
+ metadata: { directory: dir }
374
+ });
375
+ }
376
+
377
+ return chunks;
378
+ }
379
+
380
+ /**
381
+ * Dependency-based chunking - Group by import/dependency graph
382
+ * Placeholder for future implementation with AST analysis
383
+ */
384
+ createDependencyBasedChunks() {
385
+ // For now, fall back to smart chunking
386
+ // In Phase 2, we'll implement proper dependency analysis
387
+ return this.createSmartChunks();
388
+ }
389
+
390
+ /**
391
+ * Generate chunk header
392
+ */
393
+ generateChunkHeader(index, total, chunk) {
394
+ let header = '='.repeat(60) + '\n';
395
+ header += `CHUNK ${index} of ${total}\n`;
396
+ header += '='.repeat(60);
397
+ return header;
398
+ }
399
+
400
+ /**
401
+ * Generate summary for a chunk
402
+ */
403
+ generateChunkSummary(chunk) {
404
+ let summary = `Files in this chunk: ${chunk.files.length}\n`;
405
+ summary += `Estimated tokens: ${this.formatTokenCount(chunk.tokens)}`;
406
+
407
+ if (chunk.metadata?.directory) {
408
+ summary += `\nDirectory: ${chunk.metadata.directory}`;
409
+ }
410
+
411
+ return summary;
412
+ }
413
+
414
+ /**
415
+ * Generate directory tree for chunk
416
+ */
417
+ generateChunkTree(chunk) {
418
+ let tree = 'Directory structure:\n';
419
+
420
+ // Build tree structure from chunk files
421
+ const fileTree = this.buildFileTreeFromFiles(chunk.files);
422
+ tree += this.formatTreeNode(fileTree, '', true);
423
+
424
+ return tree;
425
+ }
426
+
427
+ /**
428
+ * Build file tree from specific files
429
+ */
430
+ buildFileTreeFromFiles(files) {
431
+ const root = {
432
+ name: path.basename(this.projectRoot),
433
+ type: 'directory',
434
+ children: {}
435
+ };
436
+
437
+ const sortedFiles = [...files].sort((a, b) =>
438
+ a.relativePath.localeCompare(b.relativePath)
439
+ );
440
+
441
+ for (const fileInfo of sortedFiles) {
442
+ const parts = fileInfo.relativePath.split('/');
443
+ let current = root.children;
444
+
445
+ for (let i = 0; i < parts.length; i++) {
446
+ const part = parts[i];
447
+ const isLastPart = i === parts.length - 1;
448
+
449
+ if (isLastPart) {
450
+ current[part] = {
451
+ name: part,
452
+ type: 'file',
453
+ fileInfo: fileInfo
454
+ };
455
+ } else {
456
+ if (!current[part]) {
457
+ current[part] = {
458
+ name: part,
459
+ type: 'directory',
460
+ children: {}
461
+ };
462
+ }
463
+ current = current[part].children;
464
+ }
465
+ }
466
+ }
467
+
468
+ return root;
469
+ }
470
+
471
+ /**
472
+ * Generate file contents for chunk
473
+ */
474
+ generateChunkFileContents(chunk) {
475
+ let content = '';
476
+
477
+ // Sort files by token count (largest first)
478
+ const sortedFiles = [...chunk.files].sort((a, b) => b.tokens - a.tokens);
479
+
480
+ for (const fileInfo of sortedFiles) {
481
+ if (fileInfo.error) continue;
482
+
483
+ content += '\n';
484
+ content += '='.repeat(48) + '\n';
485
+ content += `FILE: ${fileInfo.relativePath}\n`;
486
+ content += '='.repeat(48) + '\n';
487
+
488
+ try {
489
+ const fileContent = fs.readFileSync(fileInfo.path, 'utf8');
490
+
491
+ if (this.methodFilterEnabled && this.isCodeFile(fileInfo.path)) {
492
+ const filteredContent = this.generateFilteredFileContent(fileContent, fileInfo.path);
493
+ content += filteredContent;
494
+ } else {
495
+ content += fileContent;
496
+ }
497
+
498
+ if (!content.endsWith('\n')) {
499
+ content += '\n';
500
+ }
501
+ } catch (error) {
502
+ content += `Error reading file: ${error.message}\n`;
503
+ }
504
+ }
505
+
506
+ return content;
507
+ }
508
+
509
+ /**
510
+ * Generate navigation info between chunks
511
+ */
512
+ generateNavigationInfo(currentIndex, totalChunks) {
513
+ let nav = '\n';
514
+ nav += '='.repeat(60) + '\n';
515
+ nav += 'NAVIGATION\n';
516
+ nav += '='.repeat(60) + '\n';
517
+
518
+ if (currentIndex > 0) {
519
+ nav += `Previous: chunk-${currentIndex}.txt\n`;
520
+ }
521
+ if (currentIndex < totalChunks - 1) {
522
+ nav += `Next: chunk-${currentIndex + 2}.txt\n`;
523
+ }
524
+
525
+ nav += `\nTotal Chunks: ${totalChunks}\n`;
526
+ nav += `Current Position: ${currentIndex + 1} / ${totalChunks}`;
527
+
528
+ return nav;
529
+ }
530
+
531
+ generateSummary() {
532
+ const projectName = path.basename(this.projectRoot);
533
+ let summary = `Directory: ${projectName}\n`;
534
+ summary += `Files analyzed: ${this.stats.totalFiles}\n`;
535
+
536
+ if (this.methodFilterEnabled) {
537
+ const mode = this.methodFilter.hasIncludeFile ? 'INCLUDE' : 'EXCLUDE';
538
+ summary += `Method filtering: ${mode} mode active\n`;
539
+ }
540
+
541
+ if (this.stats.totalTokens > 0) {
542
+ const tokenStr = this.formatTokenCount(this.stats.totalTokens);
543
+ summary += `\nEstimated tokens: ${tokenStr}`;
544
+ }
545
+
546
+ return summary;
547
+ }
548
+
549
+ formatTokenCount(tokens) {
550
+ return TokenUtils.format(tokens);
551
+ }
552
+
553
+ generateTree() {
554
+ let tree = 'Directory structure:\n';
555
+
556
+ // Build tree structure from analysis results
557
+ const fileTree = this.buildFileTree();
558
+ tree += this.formatTreeNode(fileTree, '', true);
559
+
560
+ return tree;
561
+ }
562
+
563
+ buildFileTree() {
564
+ const root = {
565
+ name: path.basename(this.projectRoot),
566
+ type: 'directory',
567
+ children: {}
568
+ };
569
+
570
+ // Sort files by path for consistent tree structure
571
+ const sortedFiles = [...this.analysisResults].sort((a, b) =>
572
+ a.relativePath.localeCompare(b.relativePath)
573
+ );
574
+
575
+ for (const fileInfo of sortedFiles) {
576
+ const parts = fileInfo.relativePath.split('/');
577
+ let current = root.children;
578
+
579
+ for (let i = 0; i < parts.length; i++) {
580
+ const part = parts[i];
581
+ const isLastPart = i === parts.length - 1;
582
+
583
+ if (isLastPart) {
584
+ // It's a file
585
+ current[part] = {
586
+ name: part,
587
+ type: 'file',
588
+ fileInfo: fileInfo
589
+ };
590
+ } else {
591
+ // It's a directory
592
+ if (!current[part]) {
593
+ current[part] = {
594
+ name: part,
595
+ type: 'directory',
596
+ children: {}
597
+ };
598
+ }
599
+ current = current[part].children;
600
+ }
601
+ }
602
+ }
603
+
604
+ return root;
605
+ }
606
+
607
+ formatTreeNode(node, prefix = '', isLast = true) {
608
+ let result = '';
609
+
610
+ if (node.name) {
611
+ const connector = isLast ? '└── ' : '├── ';
612
+ const displayName = node.type === 'directory' ? `${node.name}/` : node.name;
613
+ result += `${prefix}${connector}${displayName}\n`;
614
+ }
615
+
616
+ if (node.type === 'directory' && node.children) {
617
+ const newPrefix = node.name ? (prefix + (isLast ? ' ' : '│ ')) : '';
618
+ const childKeys = Object.keys(node.children).sort();
619
+
620
+ childKeys.forEach((key, index) => {
621
+ const child = node.children[key];
622
+ const childIsLast = index === childKeys.length - 1;
623
+ result += this.formatTreeNode(child, newPrefix, childIsLast);
624
+ });
625
+ }
626
+
627
+ return result;
628
+ }
629
+
630
+ generateFileContents() {
631
+ let content = '';
632
+
633
+ // Sort files by token count (largest first)
634
+ const sortedFiles = [...this.analysisResults].sort((a, b) =>
635
+ b.tokens - a.tokens
636
+ );
637
+
638
+ for (const fileInfo of sortedFiles) {
639
+ if (fileInfo.error) continue;
640
+
641
+ content += '\n';
642
+ content += '='.repeat(48) + '\n';
643
+ content += `FILE: ${fileInfo.relativePath}\n`;
644
+ content += '='.repeat(48) + '\n';
645
+
646
+ try {
647
+ const fileContent = fs.readFileSync(fileInfo.path, 'utf8');
648
+
649
+ // Apply method-level filtering if enabled and file is a code file
650
+ if (this.methodFilterEnabled && this.isCodeFile(fileInfo.path)) {
651
+ const filteredContent = this.generateFilteredFileContent(fileContent, fileInfo.path);
652
+ content += filteredContent;
653
+ } else {
654
+ // Include full file content
655
+ content += fileContent;
656
+ }
657
+
658
+ if (!content.endsWith('\n')) {
659
+ content += '\n';
660
+ }
661
+ } catch (error) {
662
+ content += `Error reading file: ${error.message}\n`;
663
+ }
664
+ }
665
+
666
+ return content;
667
+ }
668
+
669
+ isCodeFile(filePath) {
670
+ return FileUtils.isCode(filePath);
671
+ }
672
+
673
+ generateFilteredFileContent(content, filePath) {
674
+ // Extract all methods from the file
675
+ const methods = this.methodAnalyzer.extractMethods(content, filePath);
676
+ const fileName = path.basename(filePath, path.extname(filePath));
677
+
678
+ // Filter methods
679
+ const filteredMethods = methods.filter(method =>
680
+ this.methodFilter.shouldIncludeMethod(method.name, fileName)
681
+ );
682
+
683
+ if (filteredMethods.length === 0) {
684
+ // No methods pass the filter, return a note
685
+ return `// No methods matched the filter criteria for this file\n`;
686
+ }
687
+
688
+ // Build filtered content with only included methods
689
+ let filteredContent = `// File contains ${methods.length} methods, showing ${filteredMethods.length} filtered methods\n\n`;
690
+
691
+ const lines = content.split('\n');
692
+
693
+ for (const method of filteredMethods) {
694
+ // Extract method content using a simple approach
695
+ // Find the method start and try to get its content
696
+ const methodContent = this.extractMethodBlock(lines, method.line - 1);
697
+
698
+ filteredContent += `// Method: ${method.name} (line ${method.line})\n`;
699
+ filteredContent += methodContent + '\n\n';
700
+ }
701
+
702
+ return filteredContent;
703
+ }
704
+
705
+ extractMethodBlock(lines, startLine) {
706
+ // Simple method extraction: get the method and its body
707
+ let braceCount = 0;
708
+ let inMethod = false;
709
+ let methodLines = [];
710
+
711
+ for (let i = startLine; i < lines.length; i++) {
712
+ const line = lines[i];
713
+ methodLines.push(line);
714
+
715
+ // Count braces to find method end
716
+ for (const char of line) {
717
+ if (char === '{') {
718
+ braceCount++;
719
+ inMethod = true;
720
+ } else if (char === '}') {
721
+ braceCount--;
722
+ if (inMethod && braceCount === 0) {
723
+ return methodLines.join('\n');
724
+ }
725
+ }
726
+ }
727
+
728
+ // Safety: limit to 100 lines per method
729
+ if (methodLines.length > 100) {
730
+ methodLines.push('// ... (method too long, truncated)');
731
+ break;
732
+ }
733
+ }
734
+
735
+ return methodLines.join('\n');
736
+ }
737
+
738
+ saveToFile(outputPath) {
739
+ const digest = this.generateDigest();
740
+ fs.writeFileSync(outputPath, digest, 'utf8');
741
+ return digest.length;
742
+ }
743
+ }
744
+
745
+
746
+ export default GitIngestFormatter;