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
@@ -19,6 +19,12 @@ let profilesCache = null;
19
19
  // Logger instance
20
20
  const logger = getLogger('LLMDetector');
21
21
 
22
+ /** Model used when nothing is configured or detected (a current, widely available model) */
23
+ export const DEFAULT_TARGET_MODEL = 'claude-sonnet-5-5';
24
+
25
+ // Deprecated/retired notices already printed in this process
26
+ const warnedModels = new Set();
27
+
22
28
  export class LLMDetector {
23
29
  /**
24
30
  * Load LLM profiles from JSON config
@@ -37,6 +43,7 @@ export class LLMDetector {
37
43
  const profilesData = JSON.parse(readFileSync(profilesPath, 'utf-8'));
38
44
  const builtInProfiles = profilesData.profiles || {};
39
45
  const defaultProfile = profilesData.default;
46
+ let aliases = profilesData.aliases || {};
40
47
 
41
48
  // Try to load custom profiles
42
49
  let customProfiles = {};
@@ -44,8 +51,9 @@ export class LLMDetector {
44
51
  const customPath = resolve(__dirname, '../../.ctxman/custom-profiles.json');
45
52
  const customData = JSON.parse(readFileSync(customPath, 'utf-8'));
46
53
  customProfiles = customData.profiles || {};
54
+ aliases = { ...aliases, ...(customData.aliases || {}) };
47
55
  logger.debug(`Loaded ${Object.keys(customProfiles).length} custom LLM profiles`);
48
- } catch (error) {
56
+ } catch (_error) {
49
57
  // Custom profiles optional
50
58
  logger.debug('No custom profiles found (optional)');
51
59
  }
@@ -53,51 +61,44 @@ export class LLMDetector {
53
61
  // Merge profiles (custom overrides built-in)
54
62
  const allProfiles = {
55
63
  ...builtInProfiles,
56
- ...customProfiles
64
+ ...customProfiles,
57
65
  };
58
66
 
59
67
  // Cache the result
60
68
  profilesCache = {
61
69
  profiles: allProfiles,
62
- default: defaultProfile
70
+ default: defaultProfile,
71
+ aliases,
72
+ lastUpdated: profilesData.lastUpdated,
63
73
  };
64
74
 
65
75
  logger.debug(`Loaded ${Object.keys(allProfiles).length} total LLM profiles`);
66
76
  return profilesCache;
67
-
68
77
  } catch (error) {
69
78
  logger.error(`Failed to load LLM profiles: ${error.message}`);
70
79
 
71
80
  // Fallback to minimal built-in if JSON load fails
72
81
  const fallback = {
73
82
  profiles: {
74
- 'claude-sonnet-4.5': {
75
- name: 'Claude Sonnet 4.5',
83
+ [DEFAULT_TARGET_MODEL]: {
84
+ name: 'Claude Sonnet 5.5',
76
85
  vendor: 'Anthropic',
77
- contextWindow: 200000,
86
+ contextWindow: 1000000,
78
87
  utilizationPercentage: 0.6,
79
- maxRecommendedInput: 120000,
88
+ maxRecommendedInput: 600000,
80
89
  preferredFormat: 'toon',
81
- chunkStrategy: 'smart'
90
+ chunkStrategy: 'none',
82
91
  },
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
92
  },
93
+ aliases: {},
93
94
  default: {
94
95
  name: 'Unknown Model',
95
96
  contextWindow: 100000,
96
97
  utilizationPercentage: 0.6,
97
98
  maxRecommendedInput: 60000,
98
99
  preferredFormat: 'json',
99
- chunkStrategy: 'smart'
100
- }
100
+ chunkStrategy: 'smart',
101
+ },
101
102
  };
102
103
 
103
104
  profilesCache = fallback;
@@ -114,6 +115,14 @@ export class LLMDetector {
114
115
  return data.default;
115
116
  }
116
117
 
118
+ /**
119
+ * Id of the model used when none is configured
120
+ * @returns {string}
121
+ */
122
+ static getDefaultModelId() {
123
+ return DEFAULT_TARGET_MODEL;
124
+ }
125
+
117
126
  /**
118
127
  * Get all profiles
119
128
  * @returns {object} All LLM profiles
@@ -147,7 +156,6 @@ export class LLMDetector {
147
156
 
148
157
  logger.debug('No LLM detected, using defaults');
149
158
  return 'unknown';
150
-
151
159
  } finally {
152
160
  const elapsed = Date.now() - startTime;
153
161
  if (elapsed > 100) {
@@ -169,24 +177,21 @@ export class LLMDetector {
169
177
  return process.env.CTXMAN_LLM;
170
178
  }
171
179
 
172
- // Anthropic (Claude)
180
+ // A provider's API key selects that provider's current balanced model
173
181
  if (process.env.ANTHROPIC_API_KEY) {
174
- return 'claude-sonnet-4.5'; // Default to Sonnet
182
+ return 'claude-sonnet-5-5';
175
183
  }
176
184
 
177
- // OpenAI (GPT-4)
178
185
  if (process.env.OPENAI_API_KEY) {
179
- return 'gpt-4o'; // Default to GPT-4o (most capable)
186
+ return 'gpt-6.1-sol';
180
187
  }
181
188
 
182
- // Google (Gemini)
183
189
  if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
184
- return 'gemini-2.0-flash'; // Default to Flash (fastest)
190
+ return 'gemini-3.8-flash';
185
191
  }
186
192
 
187
- // DeepSeek
188
193
  if (process.env.DEEPSEEK_API_KEY) {
189
- return 'deepseek-chat'; // Default to Chat
194
+ return 'deepseek-flash';
190
195
  }
191
196
 
192
197
  return null;
@@ -220,22 +225,60 @@ export class LLMDetector {
220
225
  return this.getDefaultProfile();
221
226
  }
222
227
 
223
- // Load all profiles
224
- const allProfiles = this.getAllProfiles();
225
-
226
- // Check if profile exists
227
- if (allProfiles[modelName]) {
228
- return allProfiles[modelName];
228
+ const id = this.resolveModelId(modelName);
229
+ if (id) {
230
+ const profile = { id, ...this.getAllProfiles()[id] };
231
+ this.warnIfOutdated(profile);
232
+ return profile;
229
233
  }
230
234
 
231
235
  // Fallback to default
232
- logger.warn(`Unknown LLM model: ${modelName}, using default profile`);
236
+ logger.warn(`Unknown LLM model: ${modelName}, using default profile (see --list-llms)`);
233
237
  return {
234
238
  ...this.getDefaultProfile(),
235
- name: modelName
239
+ name: modelName,
236
240
  };
237
241
  }
238
242
 
243
+ /**
244
+ * Map a model name to the id of its profile: the id itself, an alias (older ctxman
245
+ * ids such as `claude-sonnet-4.5`, provider aliases) or a dated snapshot id
246
+ * (`claude-haiku-4-5-20251001`, `gpt-5-2025-08-07`)
247
+ * @param {string} modelName - Model identifier as given by the user
248
+ * @returns {string|null} Profile id, or null for unknown models
249
+ */
250
+ static resolveModelId(modelName) {
251
+ if (!modelName) return null;
252
+ const { profiles, aliases = {} } = this.loadProfiles();
253
+ const candidates = [modelName, modelName.toLowerCase()];
254
+ for (const name of candidates) {
255
+ if (profiles[name]) return name;
256
+ if (aliases[name] && profiles[aliases[name]]) return aliases[name];
257
+ }
258
+ const undated = modelName
259
+ .toLowerCase()
260
+ .replace(/-\d{4}-\d{2}-\d{2}$/, '')
261
+ .replace(/-\d{8}$/, '');
262
+ if (undated !== modelName.toLowerCase()) {
263
+ return this.resolveModelId(undated);
264
+ }
265
+ return null;
266
+ }
267
+
268
+ /**
269
+ * Warn once per process when a profile's model is deprecated or retired by its provider
270
+ * @param {object} profile - Resolved profile (with id)
271
+ */
272
+ static warnIfOutdated(profile) {
273
+ if (!['deprecated', 'retired'].includes(profile.status) || warnedModels.has(profile.id)) {
274
+ return;
275
+ }
276
+ warnedModels.add(profile.id);
277
+ const when = profile.retires ? ` (retires ${profile.retires})` : '';
278
+ const instead = profile.replacement ? `; consider ${profile.replacement}` : '';
279
+ logger.warn(`${profile.name} is ${profile.status} by ${profile.vendor}${when}${instead}`);
280
+ }
281
+
239
282
  /**
240
283
  * Recommend optimal configuration based on LLM profile and repository stats
241
284
  * @param {object} profile - LLM profile
@@ -243,7 +286,7 @@ export class LLMDetector {
243
286
  * @returns {object} Configuration recommendations
244
287
  */
245
288
  static recommendConfiguration(profile, repoStats) {
246
- const { totalTokens, totalFiles } = repoStats;
289
+ const { totalTokens } = repoStats;
247
290
  const { maxRecommendedInput, preferredFormat, chunkStrategy, utilizationPercentage } = profile;
248
291
 
249
292
  const fitsInContext = totalTokens <= maxRecommendedInput;
@@ -261,7 +304,7 @@ export class LLMDetector {
261
304
  chunksNeeded: chunksNeeded,
262
305
  fitsInContext: fitsInContext,
263
306
  utilizationPercentage: Math.round((utilizationPercentage || 0.6) * 100),
264
- utilizationActual: (totalTokens / maxRecommendedInput) * 100
307
+ utilizationActual: (totalTokens / maxRecommendedInput) * 100,
265
308
  };
266
309
  }
267
310
 
@@ -269,9 +312,10 @@ export class LLMDetector {
269
312
  * Analyze context fit - detailed analysis for display
270
313
  * @param {object} profile - LLM profile
271
314
  * @param {object} repoStats - Repository statistics
315
+ * @param {object[]} largestFiles - Optional array of largest files for suggestions
272
316
  * @returns {object} Context fit analysis
273
317
  */
274
- static analyzeContextFit(profile, repoStats) {
318
+ static analyzeContextFit(profile, repoStats, largestFiles = []) {
275
319
  const { totalTokens, totalFiles } = repoStats;
276
320
  const { name, contextWindow, maxRecommendedInput, utilizationPercentage } = profile;
277
321
 
@@ -284,6 +328,22 @@ export class LLMDetector {
284
328
  const chunksNeeded = fitsInOne ? 1 : Math.ceil(totalTokens / usableContext);
285
329
  const utilizationActual = (totalTokens / usableContext) * 100; // % of usable context
286
330
 
331
+ // Calculate percentage of total context window
332
+ const percentageOfTotal = (totalTokens / contextWindow) * 100;
333
+
334
+ // Determine warning level based on thresholds
335
+ let warningLevel = 'ok';
336
+ if (percentageOfTotal >= 100) {
337
+ warningLevel = 'overflow';
338
+ } else if (percentageOfTotal >= 90) {
339
+ warningLevel = 'critical';
340
+ } else if (percentageOfTotal >= 80) {
341
+ warningLevel = 'warning';
342
+ }
343
+
344
+ // Generate file suggestions if needed
345
+ const suggestions = this.generateFileSuggestions(largestFiles, totalTokens, usableContext);
346
+
287
347
  return {
288
348
  modelName: name,
289
349
  contextWindow,
@@ -295,10 +355,88 @@ export class LLMDetector {
295
355
  fitsInOne,
296
356
  chunksNeeded,
297
357
  utilizationActual,
298
- recommendation: this.getRecommendation(fitsInOne, chunksNeeded, utilizationActual)
358
+ percentageOfTotal,
359
+ warningLevel,
360
+ suggestions,
361
+ recommendation: this.getRecommendation(fitsInOne, chunksNeeded, utilizationActual),
299
362
  };
300
363
  }
301
364
 
365
+ /**
366
+ * Generate file exclusion suggestions based on largest files
367
+ * @param {object[]} largestFiles - Array of largest files sorted by tokens
368
+ * @param {number} totalTokens - Total repository tokens
369
+ * @param {number} usableContext - Usable context limit
370
+ * @returns {object[]} Array of file suggestions
371
+ */
372
+ static generateFileSuggestions(largestFiles, totalTokens, usableContext) {
373
+ if (!largestFiles || largestFiles.length === 0) {
374
+ return [];
375
+ }
376
+
377
+ const suggestions = [];
378
+ const excessTokens = totalTokens - usableContext;
379
+
380
+ // If we're over budget, suggest files to exclude
381
+ if (excessTokens > 0) {
382
+ let accumulatedSavings = 0;
383
+
384
+ for (const file of largestFiles) {
385
+ if (accumulatedSavings >= excessTokens) break;
386
+
387
+ suggestions.push({
388
+ path: file.relativePath || file.path,
389
+ tokens: file.tokens,
390
+ percentage: ((file.tokens / totalTokens) * 100).toFixed(1),
391
+ reason: this.getFileSuggestionReason(file),
392
+ });
393
+
394
+ accumulatedSavings += file.tokens;
395
+ }
396
+ } else {
397
+ // Even if under budget, show top files that could be excluded if needed
398
+ const topFiles = largestFiles.slice(0, 3);
399
+ for (const file of topFiles) {
400
+ suggestions.push({
401
+ path: file.relativePath || file.path,
402
+ tokens: file.tokens,
403
+ percentage: ((file.tokens / totalTokens) * 100).toFixed(1),
404
+ reason: this.getFileSuggestionReason(file),
405
+ });
406
+ }
407
+ }
408
+
409
+ return suggestions;
410
+ }
411
+
412
+ /**
413
+ * Determine reason for file suggestion
414
+ * @param {object} file - File info object
415
+ * @returns {string} Suggestion reason
416
+ */
417
+ static getFileSuggestionReason(file) {
418
+ const path = file.relativePath || file.path;
419
+ const ext = path.split('.').pop()?.toLowerCase();
420
+
421
+ if (path.includes('.test.') || path.includes('.spec.') || path.includes('__tests__')) {
422
+ return 'Test file';
423
+ }
424
+ if (path.includes('node_modules')) {
425
+ return 'Dependency';
426
+ }
427
+ if (ext === 'md' || path.includes('docs/') || path.includes('documentation/')) {
428
+ return 'Documentation';
429
+ }
430
+ if (ext === 'json' && !path.includes('package.json') && !path.includes('tsconfig.json')) {
431
+ return 'Data file';
432
+ }
433
+ if (path.includes('dist/') || path.includes('build/') || path.includes('coverage/')) {
434
+ return 'Build output';
435
+ }
436
+
437
+ return 'Large file';
438
+ }
439
+
302
440
  /**
303
441
  * Get recommendation message based on context fit
304
442
  * @param {boolean} fitsInOne - Whether repo fits in one context
@@ -340,17 +478,32 @@ export class LLMDetector {
340
478
 
341
479
  /**
342
480
  * Get human-readable list of models with details
481
+ * @param {object} [options]
482
+ * @param {boolean} [options.includeRetired=false] - Also list models the provider has retired
343
483
  * @returns {object[]} Array of model details
344
484
  */
345
- static getModelList() {
485
+ static getModelList({ includeRetired = false } = {}) {
346
486
  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
- }));
487
+ return Object.entries(allProfiles)
488
+ .filter(([, profile]) => includeRetired || profile.status !== 'retired')
489
+ .map(([key, profile]) => ({
490
+ id: key,
491
+ name: profile.name,
492
+ vendor: profile.vendor,
493
+ contextWindow: profile.contextWindow,
494
+ outputWindow: profile.outputWindow,
495
+ preferredFormat: profile.preferredFormat,
496
+ status: profile.status || 'active',
497
+ pricing: profile.pricing || null,
498
+ }));
499
+ }
500
+
501
+ /**
502
+ * Date the built-in model data was last checked against the providers' docs
503
+ * @returns {string|undefined} ISO date
504
+ */
505
+ static getDataDate() {
506
+ return this.loadProfiles().lastUpdated;
354
507
  }
355
508
 
356
509
  /**
@@ -365,43 +518,54 @@ export class LLMDetector {
365
518
  lines.push('📊 Context Window Analysis:');
366
519
  lines.push('════════════════════════════════════════════════════');
367
520
  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}%)`);
521
+ lines.push(` Available Context: ${analysis.contextWindow.toLocaleString()} tokens`);
522
+
523
+ // Show actual percentage of total context window
524
+ const percentageDisplay = analysis.percentageOfTotal.toFixed(1);
525
+ lines.push(
526
+ ` Your Repository: ${analysis.repoTokens.toLocaleString()} tokens (${percentageDisplay}%)`
527
+ );
371
528
  lines.push('');
372
- lines.push(` Your Repository: ${analysis.repoTokens.toLocaleString()} tokens (${analysis.repoFiles} files)`);
373
529
 
374
- if (analysis.fitsInOne) {
375
- // Show utilization only if it fits
376
- lines.push(` Utilization: ${analysis.utilizationActual.toFixed(1)}% of usable context`);
530
+ // Warning level display with colors (using ANSI codes)
531
+ if (analysis.warningLevel === 'overflow') {
532
+ // Red for overflow (100%+)
533
+ lines.push(" \x1b[31m⚠️ OVERFLOW: You've exceeded the context window!\x1b[0m");
534
+ lines.push(
535
+ ` Exceeds by: ${(analysis.repoTokens - analysis.contextWindow).toLocaleString()} tokens`
536
+ );
537
+ } else if (analysis.warningLevel === 'critical') {
538
+ // Red for critical (90-99%)
539
+ lines.push(" \x1b[31m⚠️ CRITICAL: You're using 90%+ of context window\x1b[0m");
540
+ } else if (analysis.warningLevel === 'warning') {
541
+ // Yellow for warning (80-89%)
542
+ lines.push(" \x1b[33m⚠️ WARNING: You're using 80%+ of context window\x1b[0m");
543
+ } else {
544
+ // Green for OK
545
+ lines.push(' \x1b[32m✅ Context usage is within safe limits\x1b[0m');
546
+ }
547
+
548
+ // Show suggestions if available
549
+ if (analysis.suggestions && analysis.suggestions.length > 0) {
377
550
  lines.push('');
551
+ lines.push(' 💡 Consider excluding these large files:');
378
552
 
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
- }
553
+ const maxSuggestions = analysis.warningLevel === 'overflow' ? 5 : 3;
554
+ const suggestionsToShow = analysis.suggestions.slice(0, maxSuggestions);
388
555
 
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);
556
+ for (const suggestion of suggestionsToShow) {
557
+ lines.push(
558
+ ` - ${suggestion.path} (${suggestion.tokens.toLocaleString()} tokens, ${suggestion.percentage}%)`
559
+ );
560
+ }
396
561
 
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`);
562
+ if (analysis.suggestions.length > maxSuggestions) {
563
+ lines.push(` ... and ${analysis.suggestions.length - maxSuggestions} more`);
564
+ }
403
565
  }
404
566
 
567
+ lines.push('');
568
+ lines.push(` ${analysis.recommendation}`);
405
569
  lines.push('');
406
570
 
407
571
  return lines.join('\n');