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
@@ -0,0 +1,281 @@
1
+ /**
2
+ * LLM Cost Estimator
3
+ * Estimates API costs for different LLM providers
4
+ * FEAT-010 feature
5
+ */
6
+
7
+ import { getLogger } from './logger.js';
8
+ import { LLMDetector } from './llm-detector.js';
9
+
10
+ const logger = getLogger('LLMCostEstimator');
11
+
12
+ /** Provider key for a profile's vendor, e.g. `OpenAI` → `openai`, `xAI` → `xai` */
13
+ function providerKey(vendor) {
14
+ return String(vendor || 'other')
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]/g, '');
17
+ }
18
+
19
+ /**
20
+ * Per-1M-token prices (USD) of the models in .ctxman/llm-profiles.json, grouped by
21
+ * provider. The profiles are the single source: they carry the context window, the
22
+ * output limit and the pricing, with the date and the provider pages they were checked
23
+ * against. Deprecated and retired models are left out unless asked for.
24
+ * @param {object} [options]
25
+ * @param {boolean} [options.includeInactive=false] - Include deprecated/retired models
26
+ * @returns {Object<string, Object<string, object>>} provider → model id → pricing
27
+ */
28
+ export function buildPricingTable({ includeInactive = false } = {}) {
29
+ const table = {};
30
+ for (const [id, profile] of Object.entries(LLMDetector.getAllProfiles())) {
31
+ if (!profile.pricing) continue;
32
+ if (!includeInactive && ['deprecated', 'retired'].includes(profile.status)) continue;
33
+ const provider = providerKey(profile.vendor);
34
+ table[provider] ??= {};
35
+ table[provider][id] = {
36
+ name: profile.name,
37
+ input: profile.pricing.input,
38
+ output: profile.pricing.output,
39
+ tiers: profile.pricing.tiers || [],
40
+ context: profile.contextWindow,
41
+ maxOutput: profile.outputWindow || null,
42
+ tier: profile.tier || null,
43
+ featured: Boolean(profile.featured),
44
+ };
45
+ }
46
+ return table;
47
+ }
48
+
49
+ /** Pricing of the current models, read from the LLM profiles */
50
+ export const LLM_PRICING = buildPricingTable();
51
+
52
+ /**
53
+ * Cost Estimator class
54
+ * Calculates and compares LLM API costs
55
+ */
56
+ export class LLMCostEstimator {
57
+ /**
58
+ * Create a cost estimator
59
+ * @param {number} tokens - Total tokens in repository
60
+ */
61
+ constructor(tokens) {
62
+ this.tokens = tokens;
63
+ logger.debug(`Initialized cost estimator with ${tokens.toLocaleString()} tokens`);
64
+ }
65
+
66
+ /**
67
+ * Calculate cost for a specific model
68
+ * @param {string} modelId - Model identifier (e.g., 'gpt-4o')
69
+ * @param {string} provider - Provider name (e.g., 'openai')
70
+ * @returns {object|null} Cost breakdown or null if model not found
71
+ */
72
+ calculateCost(modelId, provider) {
73
+ const pricing = LLM_PRICING[provider]?.[modelId];
74
+ if (!pricing) {
75
+ logger.debug(`Model not found: ${provider}/${modelId}`);
76
+ return null;
77
+ }
78
+
79
+ // Some models charge a higher rate for every token once the prompt is long
80
+ const tier = pricing.tiers.filter((t) => this.tokens > t.above).at(-1);
81
+ const rates = tier || pricing;
82
+
83
+ // Input cost: straightforward
84
+ const inputCost = (this.tokens / 1_000_000) * rates.input;
85
+
86
+ // Output cost: estimate output tokens as 2/3 of input (typical conversation ratio),
87
+ // but a response can't be longer than the model's output limit
88
+ const estimatedOutputTokens = Math.min(
89
+ Math.round(this.tokens * (2 / 3)),
90
+ pricing.maxOutput || Infinity
91
+ );
92
+ const outputCost = (estimatedOutputTokens / 1_000_000) * rates.output;
93
+
94
+ // Full conversation cost
95
+ const fullCost = inputCost + outputCost;
96
+
97
+ // Check if fits in context
98
+ const fitsInContext = this.tokens <= pricing.context;
99
+
100
+ return {
101
+ modelId,
102
+ name: pricing.name,
103
+ provider,
104
+ inputCost,
105
+ outputCost,
106
+ fullCost,
107
+ estimatedOutputTokens,
108
+ fitsInContext,
109
+ contextLimit: pricing.context,
110
+ longContextRate: rates !== pricing,
111
+ tier: pricing.tier,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Compare costs across all models
117
+ * @returns {object[]} Array of cost comparisons sorted by full cost
118
+ */
119
+ compareAll() {
120
+ const comparisons = [];
121
+
122
+ for (const [provider, models] of Object.entries(LLM_PRICING)) {
123
+ for (const modelId of Object.keys(models)) {
124
+ const cost = this.calculateCost(modelId, provider);
125
+ if (cost) {
126
+ comparisons.push(cost);
127
+ }
128
+ }
129
+ }
130
+
131
+ // Sort by full cost (ascending)
132
+ comparisons.sort((a, b) => a.fullCost - b.fullCost);
133
+
134
+ logger.debug(`Compared costs across ${comparisons.length} models`);
135
+ return comparisons;
136
+ }
137
+
138
+ /**
139
+ * Get cheapest option that fits in context
140
+ * @param {object[]} comparisons - Cost comparisons array
141
+ * @returns {object|null} Cheapest option or null
142
+ */
143
+ getCheapest(comparisons) {
144
+ const validOptions = comparisons.filter((c) => c.fitsInContext);
145
+ return validOptions.length > 0 ? validOptions[0] : null;
146
+ }
147
+
148
+ /**
149
+ * Get best value recommendation: the cheapest fitting model of the providers'
150
+ * balanced tier (Sonnet-class models), falling back to the cheapest fitting model
151
+ * @param {object[]} comparisons - Cost comparisons array
152
+ * @returns {object|null} Best value option or null
153
+ */
154
+ getBestValue(comparisons) {
155
+ const validOptions = comparisons.filter((c) => c.fitsInContext);
156
+
157
+ if (validOptions.length === 0) {
158
+ return null;
159
+ }
160
+
161
+ return validOptions.find((c) => c.tier === 'balanced') || validOptions[0];
162
+ }
163
+
164
+ /**
165
+ * Get recommendation message
166
+ * @param {object[]} comparisons - Cost comparisons array
167
+ * @returns {object} Recommendation object
168
+ */
169
+ getRecommendation(comparisons) {
170
+ const cheapest = this.getCheapest(comparisons);
171
+ const bestValue = this.getBestValue(comparisons);
172
+
173
+ if (!cheapest) {
174
+ return {
175
+ type: 'warning',
176
+ message: 'Context exceeds all model limits. Consider chunking or filtering files.',
177
+ cheapest: null,
178
+ bestValue: null,
179
+ };
180
+ }
181
+
182
+ return {
183
+ type: 'recommendation',
184
+ cheapest,
185
+ bestValue,
186
+ message:
187
+ bestValue.modelId === cheapest.modelId
188
+ ? `${cheapest.name} offers the best value`
189
+ : `${bestValue.name} offers the best balance of cost and quality`,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Format cost estimates for display
195
+ * @param {object[]} comparisons - Cost comparisons array
196
+ * @param {object} recommendation - Recommendation object
197
+ * @returns {string} Formatted output
198
+ */
199
+ formatEstimates(comparisons, recommendation) {
200
+ const lines = [];
201
+
202
+ lines.push('');
203
+ lines.push('💰 LLM Cost Estimation:');
204
+ lines.push('════════════════════════════════════════════════════');
205
+ lines.push(` Repository: ${this.tokens.toLocaleString()} tokens`);
206
+ lines.push('');
207
+ lines.push(' Cost by Provider:');
208
+ lines.push(' ┌──────────────────────────┬────────────┬────────────┐');
209
+ lines.push(' │ Model │ Input Cost │ Full Cost │');
210
+ lines.push(' ├──────────────────────────┼────────────┼────────────┤');
211
+
212
+ // One or two featured models per provider keep the table short
213
+ const featured = comparisons
214
+ .filter((c) => LLM_PRICING[c.provider]?.[c.modelId]?.featured)
215
+ .sort((a, b) => a.provider.localeCompare(b.provider) || b.fullCost - a.fullCost);
216
+
217
+ for (const cost of featured) {
218
+ const inputStr = this.formatCost(cost.inputCost).padStart(10);
219
+ const fullStr = cost.fitsInContext
220
+ ? this.formatCost(cost.fullCost).padStart(10)
221
+ : "won't fit".padStart(10);
222
+ lines.push(` │ ${cost.name.padEnd(24)} │${inputStr} │${fullStr} │`);
223
+ }
224
+
225
+ lines.push(' └──────────────────────────┴────────────┴────────────┘');
226
+ const checked = LLMDetector.getDataDate();
227
+ lines.push(
228
+ ` Full cost assumes a response 2/3 the size of the input.${checked ? ` Prices checked ${checked}.` : ''}`
229
+ );
230
+ lines.push('');
231
+
232
+ // Show recommendation
233
+ if (recommendation.type === 'warning') {
234
+ lines.push(` ⚠️ ${recommendation.message}`);
235
+ } else if (recommendation.cheapest) {
236
+ lines.push(
237
+ ` 💡 Cheapest: ${recommendation.cheapest.name} (${this.formatCost(recommendation.cheapest.inputCost)})`
238
+ );
239
+ }
240
+
241
+ if (
242
+ recommendation.bestValue &&
243
+ recommendation.bestValue.modelId !== recommendation.cheapest?.modelId
244
+ ) {
245
+ lines.push(
246
+ ` ⚡ Best Value: ${recommendation.bestValue.name} (${this.formatCost(recommendation.bestValue.inputCost)})`
247
+ );
248
+ }
249
+
250
+ lines.push('');
251
+
252
+ return lines.join('\n');
253
+ }
254
+
255
+ /**
256
+ * Format cost with proper precision
257
+ * @param {number} cost - Cost in USD
258
+ * @returns {string} Formatted cost string
259
+ */
260
+ formatCost(cost) {
261
+ if (cost < 0.01) {
262
+ return `$${cost.toFixed(4)}`;
263
+ } else if (cost < 1) {
264
+ return `$${cost.toFixed(3)}`;
265
+ } else {
266
+ return `$${cost.toFixed(2)}`;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Run full cost estimation and return formatted output
272
+ * @returns {string} Formatted cost estimation
273
+ */
274
+ estimate() {
275
+ const comparisons = this.compareAll();
276
+ const recommendation = this.getRecommendation(comparisons);
277
+ return this.formatEstimates(comparisons, recommendation);
278
+ }
279
+ }
280
+
281
+ export default LLMCostEstimator;