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