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,409 @@
1
+ /**
2
+ * LLM Detector
3
+ * Auto-detects target LLM model and provides optimization recommendations
4
+ * v2.3.7 feature - JSON config based
5
+ */
6
+
7
+ import { readFileSync } from 'fs';
8
+ import { resolve, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import ConfigUtils from './config-utils.js';
11
+ import { getLogger } from './logger.js';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ // Lazy-loaded profiles cache
17
+ let profilesCache = null;
18
+
19
+ // Logger instance
20
+ const logger = getLogger('LLMDetector');
21
+
22
+ export class LLMDetector {
23
+ /**
24
+ * Load LLM profiles from JSON config
25
+ * Merges built-in profiles with custom user profiles
26
+ * @returns {object} All LLM profiles
27
+ */
28
+ static loadProfiles() {
29
+ // Return cached if available
30
+ if (profilesCache) {
31
+ return profilesCache;
32
+ }
33
+
34
+ try {
35
+ // Load built-in profiles
36
+ const profilesPath = resolve(__dirname, '../../.ctxman/llm-profiles.json');
37
+ const profilesData = JSON.parse(readFileSync(profilesPath, 'utf-8'));
38
+ const builtInProfiles = profilesData.profiles || {};
39
+ const defaultProfile = profilesData.default;
40
+
41
+ // Try to load custom profiles
42
+ let customProfiles = {};
43
+ try {
44
+ const customPath = resolve(__dirname, '../../.ctxman/custom-profiles.json');
45
+ const customData = JSON.parse(readFileSync(customPath, 'utf-8'));
46
+ customProfiles = customData.profiles || {};
47
+ logger.debug(`Loaded ${Object.keys(customProfiles).length} custom LLM profiles`);
48
+ } catch (error) {
49
+ // Custom profiles optional
50
+ logger.debug('No custom profiles found (optional)');
51
+ }
52
+
53
+ // Merge profiles (custom overrides built-in)
54
+ const allProfiles = {
55
+ ...builtInProfiles,
56
+ ...customProfiles
57
+ };
58
+
59
+ // Cache the result
60
+ profilesCache = {
61
+ profiles: allProfiles,
62
+ default: defaultProfile
63
+ };
64
+
65
+ logger.debug(`Loaded ${Object.keys(allProfiles).length} total LLM profiles`);
66
+ return profilesCache;
67
+
68
+ } catch (error) {
69
+ logger.error(`Failed to load LLM profiles: ${error.message}`);
70
+
71
+ // Fallback to minimal built-in if JSON load fails
72
+ const fallback = {
73
+ profiles: {
74
+ 'claude-sonnet-4.5': {
75
+ name: 'Claude Sonnet 4.5',
76
+ vendor: 'Anthropic',
77
+ contextWindow: 200000,
78
+ utilizationPercentage: 0.6,
79
+ maxRecommendedInput: 120000,
80
+ preferredFormat: 'toon',
81
+ chunkStrategy: 'smart'
82
+ },
83
+ 'gpt-4o': {
84
+ name: 'GPT-4o',
85
+ vendor: 'OpenAI',
86
+ contextWindow: 128000,
87
+ utilizationPercentage: 0.6,
88
+ maxRecommendedInput: 76800,
89
+ preferredFormat: 'json',
90
+ chunkStrategy: 'smart'
91
+ }
92
+ },
93
+ default: {
94
+ name: 'Unknown Model',
95
+ contextWindow: 100000,
96
+ utilizationPercentage: 0.6,
97
+ maxRecommendedInput: 60000,
98
+ preferredFormat: 'json',
99
+ chunkStrategy: 'smart'
100
+ }
101
+ };
102
+
103
+ profilesCache = fallback;
104
+ return fallback;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get default profile for unknown models
110
+ * @returns {object} Default profile
111
+ */
112
+ static getDefaultProfile() {
113
+ const data = this.loadProfiles();
114
+ return data.default;
115
+ }
116
+
117
+ /**
118
+ * Get all profiles
119
+ * @returns {object} All LLM profiles
120
+ */
121
+ static getAllProfiles() {
122
+ const data = this.loadProfiles();
123
+ return data.profiles;
124
+ }
125
+ /**
126
+ * Auto-detect LLM from environment and config
127
+ * Performance target: <100ms
128
+ * @returns {string} Detected model name or 'unknown'
129
+ */
130
+ static detect() {
131
+ const startTime = Date.now();
132
+
133
+ try {
134
+ // 1. Check environment variables (fastest, <1ms)
135
+ const envModel = this.detectFromEnv();
136
+ if (envModel && envModel !== 'unknown') {
137
+ logger.debug(`LLM detected from environment: ${envModel}`);
138
+ return envModel;
139
+ }
140
+
141
+ // 2. Check user config (slower but still fast, <50ms)
142
+ const configModel = this.detectFromConfig();
143
+ if (configModel && configModel !== 'unknown') {
144
+ logger.debug(`LLM detected from config: ${configModel}`);
145
+ return configModel;
146
+ }
147
+
148
+ logger.debug('No LLM detected, using defaults');
149
+ return 'unknown';
150
+
151
+ } finally {
152
+ const elapsed = Date.now() - startTime;
153
+ if (elapsed > 100) {
154
+ logger.warn(`LLM detection took ${elapsed}ms (threshold: 100ms)`);
155
+ } else {
156
+ logger.debug(`LLM detection completed in ${elapsed}ms`);
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Detect from environment variables
163
+ * Checks for API keys and explicit overrides
164
+ * @returns {string|null} Model name or null
165
+ */
166
+ static detectFromEnv() {
167
+ // Explicit override
168
+ if (process.env.CTXMAN_LLM) {
169
+ return process.env.CTXMAN_LLM;
170
+ }
171
+
172
+ // Anthropic (Claude)
173
+ if (process.env.ANTHROPIC_API_KEY) {
174
+ return 'claude-sonnet-4.5'; // Default to Sonnet
175
+ }
176
+
177
+ // OpenAI (GPT-4)
178
+ if (process.env.OPENAI_API_KEY) {
179
+ return 'gpt-4o'; // Default to GPT-4o (most capable)
180
+ }
181
+
182
+ // Google (Gemini)
183
+ if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
184
+ return 'gemini-2.0-flash'; // Default to Flash (fastest)
185
+ }
186
+
187
+ // DeepSeek
188
+ if (process.env.DEEPSEEK_API_KEY) {
189
+ return 'deepseek-chat'; // Default to Chat
190
+ }
191
+
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Detect from user configuration file
197
+ * @returns {string|null} Model name or null
198
+ */
199
+ static detectFromConfig() {
200
+ try {
201
+ const config = ConfigUtils.loadUserConfig();
202
+ if (config && config.targetModel) {
203
+ return config.targetModel;
204
+ }
205
+ } catch (error) {
206
+ logger.debug(`Config file not found or invalid: ${error.message}`);
207
+ }
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Get LLM profile by name
213
+ * Supports built-in and custom user models
214
+ * @param {string} modelName - Model identifier
215
+ * @returns {object} LLM profile
216
+ */
217
+ static getProfile(modelName) {
218
+ // Unknown model
219
+ if (!modelName || modelName === 'unknown') {
220
+ return this.getDefaultProfile();
221
+ }
222
+
223
+ // Load all profiles
224
+ const allProfiles = this.getAllProfiles();
225
+
226
+ // Check if profile exists
227
+ if (allProfiles[modelName]) {
228
+ return allProfiles[modelName];
229
+ }
230
+
231
+ // Fallback to default
232
+ logger.warn(`Unknown LLM model: ${modelName}, using default profile`);
233
+ return {
234
+ ...this.getDefaultProfile(),
235
+ name: modelName
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Recommend optimal configuration based on LLM profile and repository stats
241
+ * @param {object} profile - LLM profile
242
+ * @param {object} repoStats - Repository statistics
243
+ * @returns {object} Configuration recommendations
244
+ */
245
+ static recommendConfiguration(profile, repoStats) {
246
+ const { totalTokens, totalFiles } = repoStats;
247
+ const { maxRecommendedInput, preferredFormat, chunkStrategy, utilizationPercentage } = profile;
248
+
249
+ const fitsInContext = totalTokens <= maxRecommendedInput;
250
+ const chunksNeeded = fitsInContext ? 1 : Math.ceil(totalTokens / maxRecommendedInput);
251
+
252
+ // Chunk size should also respect 60% rule
253
+ const recommendedChunkSize = maxRecommendedInput;
254
+
255
+ return {
256
+ targetModel: profile.name,
257
+ outputFormat: preferredFormat,
258
+ chunkingEnabled: !fitsInContext,
259
+ chunkStrategy: chunkStrategy,
260
+ chunkSize: recommendedChunkSize,
261
+ chunksNeeded: chunksNeeded,
262
+ fitsInContext: fitsInContext,
263
+ utilizationPercentage: Math.round((utilizationPercentage || 0.6) * 100),
264
+ utilizationActual: (totalTokens / maxRecommendedInput) * 100
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Analyze context fit - detailed analysis for display
270
+ * @param {object} profile - LLM profile
271
+ * @param {object} repoStats - Repository statistics
272
+ * @returns {object} Context fit analysis
273
+ */
274
+ static analyzeContextFit(profile, repoStats) {
275
+ const { totalTokens, totalFiles } = repoStats;
276
+ const { name, contextWindow, maxRecommendedInput, utilizationPercentage } = profile;
277
+
278
+ // Calculate usable context (60% of total by default)
279
+ const utilization = utilizationPercentage || 0.6;
280
+ const usableContext = maxRecommendedInput; // Already calculated as 60%
281
+ const reservedForSystem = contextWindow - usableContext; // 40% reserved
282
+
283
+ const fitsInOne = totalTokens <= usableContext;
284
+ const chunksNeeded = fitsInOne ? 1 : Math.ceil(totalTokens / usableContext);
285
+ const utilizationActual = (totalTokens / usableContext) * 100; // % of usable context
286
+
287
+ return {
288
+ modelName: name,
289
+ contextWindow,
290
+ usableContext,
291
+ reservedForSystem,
292
+ utilizationPercentage: Math.round(utilization * 100),
293
+ repoTokens: totalTokens,
294
+ repoFiles: totalFiles,
295
+ fitsInOne,
296
+ chunksNeeded,
297
+ utilizationActual,
298
+ recommendation: this.getRecommendation(fitsInOne, chunksNeeded, utilizationActual)
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Get recommendation message based on context fit
304
+ * @param {boolean} fitsInOne - Whether repo fits in one context
305
+ * @param {number} chunksNeeded - Number of chunks needed
306
+ * @param {number} utilizationActual - Actual utilization percentage
307
+ * @returns {string} Recommendation message
308
+ */
309
+ static getRecommendation(fitsInOne, chunksNeeded, utilizationActual) {
310
+ if (fitsInOne) {
311
+ if (utilizationActual < 50) {
312
+ return 'Plenty of room for system prompts and responses.';
313
+ } else if (utilizationActual < 80) {
314
+ return 'Your entire codebase fits comfortably.';
315
+ } else if (utilizationActual <= 100) {
316
+ return 'Repository uses most of available context.';
317
+ } else {
318
+ // Should not happen if fitsInOne is true, but safety check
319
+ return 'Context limit exceeded. Enable chunking.';
320
+ }
321
+ } else {
322
+ if (chunksNeeded <= 3) {
323
+ return `Repository requires ${chunksNeeded} chunks. Enable chunking with --chunk flag.`;
324
+ } else if (chunksNeeded <= 10) {
325
+ return `Large repository needs ${chunksNeeded} chunks. Consider filtering with .contextinclude.`;
326
+ } else {
327
+ return `Very large repository (${chunksNeeded} chunks). Strongly recommend filtering to key files.`;
328
+ }
329
+ }
330
+ }
331
+
332
+ /**
333
+ * List all available LLM profiles
334
+ * @returns {string[]} Array of model identifiers
335
+ */
336
+ static listProfiles() {
337
+ const allProfiles = this.getAllProfiles();
338
+ return Object.keys(allProfiles);
339
+ }
340
+
341
+ /**
342
+ * Get human-readable list of models with details
343
+ * @returns {object[]} Array of model details
344
+ */
345
+ static getModelList() {
346
+ const allProfiles = this.getAllProfiles();
347
+ return Object.entries(allProfiles).map(([key, profile]) => ({
348
+ id: key,
349
+ name: profile.name,
350
+ vendor: profile.vendor,
351
+ contextWindow: profile.contextWindow,
352
+ preferredFormat: profile.preferredFormat
353
+ }));
354
+ }
355
+
356
+ /**
357
+ * Format context fit analysis for CLI display
358
+ * @param {object} analysis - Context fit analysis
359
+ * @returns {string} Formatted text
360
+ */
361
+ static formatAnalysis(analysis) {
362
+ const lines = [];
363
+
364
+ lines.push('');
365
+ lines.push('📊 Context Window Analysis:');
366
+ lines.push('════════════════════════════════════════════════════');
367
+ lines.push(` Target Model: ${analysis.modelName}`);
368
+ lines.push(` Total Context: ${analysis.contextWindow.toLocaleString()} tokens`);
369
+ lines.push(` Usable Context: ${analysis.usableContext.toLocaleString()} tokens (${analysis.utilizationPercentage}% policy)`);
370
+ lines.push(` Reserved for System: ${analysis.reservedForSystem.toLocaleString()} tokens (${100 - analysis.utilizationPercentage}%)`);
371
+ lines.push('');
372
+ lines.push(` Your Repository: ${analysis.repoTokens.toLocaleString()} tokens (${analysis.repoFiles} files)`);
373
+
374
+ if (analysis.fitsInOne) {
375
+ // Show utilization only if it fits
376
+ lines.push(` Utilization: ${analysis.utilizationActual.toFixed(1)}% of usable context`);
377
+ lines.push('');
378
+
379
+ // Determine fit quality
380
+ let fitStatus = '';
381
+ if (analysis.utilizationActual < 50) {
382
+ fitStatus = '✅ EXCELLENT FIT!';
383
+ } else if (analysis.utilizationActual < 80) {
384
+ fitStatus = '✅ GOOD FIT!';
385
+ } else {
386
+ fitStatus = '⚠️ ACCEPTABLE FIT';
387
+ }
388
+
389
+ lines.push(` ${fitStatus}`);
390
+ lines.push(` ${analysis.recommendation}`);
391
+ lines.push(' 💡 Recommendation: Use single-file export (no chunking needed)');
392
+ } else {
393
+ // Show excess instead of percentage over 100%
394
+ const excess = analysis.repoTokens - analysis.usableContext;
395
+ const excessMultiplier = (analysis.repoTokens / analysis.usableContext).toFixed(1);
396
+
397
+ lines.push(` Exceeds by: ${excess.toLocaleString()} tokens (${excessMultiplier}x usable context)`);
398
+ lines.push('');
399
+ lines.push(` ❌ TOO LARGE! Repository exceeds usable context.`);
400
+ lines.push(` 📦 Chunks Needed: ${analysis.chunksNeeded}`);
401
+ lines.push(' 💡 Recommendation: Enable chunking with --chunk flag');
402
+ lines.push(` 💡 Or: Filter files with .contextinclude to fit in ${analysis.usableContext.toLocaleString()} tokens`);
403
+ }
404
+
405
+ lines.push('');
406
+
407
+ return lines.join('\n');
408
+ }
409
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Logger System
3
+ * Centralized logging with multiple levels and file output
4
+ * v2.3.6+
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+
10
+ class Logger {
11
+ constructor(options = {}) {
12
+ this.level = options.level || process.env.LOG_LEVEL || 'info';
13
+ this.logToFile = options.logToFile !== false;
14
+ this.logDir = options.logDir || path.join(process.cwd(), '.ctxman', 'logs');
15
+ this.logFile = options.logFile || `ctxman-${this.getDateString()}.log`;
16
+ this.silent = options.silent || false;
17
+
18
+ // Log levels (higher number = more verbose)
19
+ this.levels = {
20
+ error: 0,
21
+ warn: 1,
22
+ info: 2,
23
+ debug: 3,
24
+ trace: 4
25
+ };
26
+
27
+ // Colors for terminal output
28
+ this.colors = {
29
+ error: '\x1b[31m', // Red
30
+ warn: '\x1b[33m', // Yellow
31
+ info: '\x1b[36m', // Cyan
32
+ debug: '\x1b[35m', // Magenta
33
+ trace: '\x1b[90m', // Gray
34
+ reset: '\x1b[0m'
35
+ };
36
+
37
+ // Icons for each level
38
+ this.icons = {
39
+ error: '❌',
40
+ warn: '⚠️',
41
+ info: 'ℹ️',
42
+ debug: '🔍',
43
+ trace: '🔬'
44
+ };
45
+
46
+ this.initializeLogDirectory();
47
+ }
48
+
49
+ /**
50
+ * Initialize log directory
51
+ */
52
+ initializeLogDirectory() {
53
+ if (this.logToFile) {
54
+ const fullLogDir = path.isAbsolute(this.logDir) ? this.logDir : path.join(process.cwd(), this.logDir);
55
+ if (!fs.existsSync(fullLogDir)) {
56
+ fs.mkdirSync(fullLogDir, { recursive: true });
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get current date string for log file names
63
+ */
64
+ getDateString() {
65
+ const now = new Date();
66
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
67
+ }
68
+
69
+ /**
70
+ * Get timestamp for log entries
71
+ */
72
+ getTimestamp() {
73
+ return new Date().toISOString();
74
+ }
75
+
76
+ /**
77
+ * Check if should log based on level
78
+ */
79
+ shouldLog(level) {
80
+ return this.levels[level] <= this.levels[this.level];
81
+ }
82
+
83
+ /**
84
+ * Format log message
85
+ */
86
+ formatMessage(level, message, meta = {}) {
87
+ const timestamp = this.getTimestamp();
88
+ const icon = this.icons[level];
89
+
90
+ // Console format (with colors)
91
+ const consoleMessage = `${this.colors[level]}${icon} [${level.toUpperCase()}]${this.colors.reset} ${message}`;
92
+
93
+ // File format (plain text with metadata)
94
+ const metaString = Object.keys(meta).length > 0 ? ` | ${JSON.stringify(meta)}` : '';
95
+ const fileMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}${metaString}`;
96
+
97
+ return { consoleMessage, fileMessage };
98
+ }
99
+
100
+ /**
101
+ * Write log to file
102
+ */
103
+ writeToFile(message) {
104
+ if (!this.logToFile) return;
105
+
106
+ try {
107
+ const fullLogPath = path.isAbsolute(this.logDir)
108
+ ? path.join(this.logDir, this.logFile)
109
+ : path.join(process.cwd(), this.logDir, this.logFile);
110
+
111
+ fs.appendFileSync(fullLogPath, message + '\n', 'utf8');
112
+ } catch (error) {
113
+ // Fail silently to avoid infinite loop
114
+ console.error('Failed to write to log file:', error.message);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Core logging method
120
+ */
121
+ log(level, message, meta = {}) {
122
+ if (!this.shouldLog(level)) return;
123
+
124
+ const { consoleMessage, fileMessage } = this.formatMessage(level, message, meta);
125
+
126
+ // Output to console
127
+ if (!this.silent) {
128
+ console.log(consoleMessage);
129
+ }
130
+
131
+ // Output to file
132
+ this.writeToFile(fileMessage);
133
+ }
134
+
135
+ /**
136
+ * Convenience methods
137
+ */
138
+ error(message, meta = {}) {
139
+ this.log('error', message, meta);
140
+ }
141
+
142
+ warn(message, meta = {}) {
143
+ this.log('warn', message, meta);
144
+ }
145
+
146
+ info(message, meta = {}) {
147
+ this.log('info', message, meta);
148
+ }
149
+
150
+ debug(message, meta = {}) {
151
+ this.log('debug', message, meta);
152
+ }
153
+
154
+ trace(message, meta = {}) {
155
+ this.log('trace', message, meta);
156
+ }
157
+
158
+ /**
159
+ * Log with custom icon
160
+ */
161
+ custom(icon, level, message, meta = {}) {
162
+ const originalIcon = this.icons[level];
163
+ this.icons[level] = icon;
164
+ this.log(level, message, meta);
165
+ this.icons[level] = originalIcon;
166
+ }
167
+
168
+ /**
169
+ * Group related logs
170
+ */
171
+ group(title) {
172
+ if (!this.silent) {
173
+ console.group(title);
174
+ }
175
+ this.writeToFile(`\n--- ${title} ---`);
176
+ }
177
+
178
+ groupEnd() {
179
+ if (!this.silent) {
180
+ console.groupEnd();
181
+ }
182
+ this.writeToFile('--- End Group ---\n');
183
+ }
184
+
185
+ /**
186
+ * Log with timer
187
+ */
188
+ time(label) {
189
+ console.time(label);
190
+ this.writeToFile(`[TIMER START] ${label}`);
191
+ }
192
+
193
+ timeEnd(label) {
194
+ console.timeEnd(label);
195
+ this.writeToFile(`[TIMER END] ${label}`);
196
+ }
197
+
198
+ /**
199
+ * Clear old log files (keep last N days)
200
+ */
201
+ clearOldLogs(daysToKeep = 7) {
202
+ try {
203
+ const fullLogDir = path.isAbsolute(this.logDir) ? this.logDir : path.join(process.cwd(), this.logDir);
204
+ if (!fs.existsSync(fullLogDir)) return;
205
+
206
+ const now = Date.now();
207
+ const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
208
+
209
+ const files = fs.readdirSync(fullLogDir);
210
+ files.forEach(file => {
211
+ const filePath = path.join(fullLogDir, file);
212
+ const stats = fs.statSync(filePath);
213
+ const age = now - stats.mtime.getTime();
214
+
215
+ if (age > maxAge) {
216
+ fs.unlinkSync(filePath);
217
+ this.debug(`Deleted old log file: ${file}`);
218
+ }
219
+ });
220
+ } catch (error) {
221
+ this.error('Failed to clear old logs', { error: error.message });
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Get log file path
227
+ */
228
+ getLogFilePath() {
229
+ return path.isAbsolute(this.logDir)
230
+ ? path.join(this.logDir, this.logFile)
231
+ : path.join(process.cwd(), this.logDir, this.logFile);
232
+ }
233
+
234
+ /**
235
+ * Read recent logs
236
+ */
237
+ getRecentLogs(lines = 100) {
238
+ try {
239
+ const logPath = this.getLogFilePath();
240
+ if (!fs.existsSync(logPath)) {
241
+ return [];
242
+ }
243
+
244
+ const content = fs.readFileSync(logPath, 'utf8');
245
+ const allLines = content.split('\n').filter(line => line.trim());
246
+ return allLines.slice(-lines);
247
+ } catch (error) {
248
+ this.error('Failed to read logs', { error: error.message });
249
+ return [];
250
+ }
251
+ }
252
+ }
253
+
254
+ // Create singleton instance
255
+ let defaultLogger = null;
256
+
257
+ /**
258
+ * Get or create default logger instance
259
+ */
260
+ function getLogger(options = {}) {
261
+ if (!defaultLogger) {
262
+ defaultLogger = new Logger(options);
263
+ }
264
+ return defaultLogger;
265
+ }
266
+
267
+ /**
268
+ * Create a new logger instance
269
+ */
270
+ function createLogger(options = {}) {
271
+ return new Logger(options);
272
+ }
273
+
274
+ export {
275
+ Logger,
276
+ getLogger,
277
+ createLogger
278
+ };