ctxman 3.0.0 → 3.1.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 (223) 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 +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -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 +683 -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 +675 -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 +113 -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 +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -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/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,683 @@
1
+ /**
2
+ * Performance Dashboard - Text-based ASCII charts for terminal output (FEAT-001)
3
+ *
4
+ * A lightweight, text-based performance dashboard that displays metrics
5
+ * with ASCII visualization without requiring Ink/React dependencies.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { TokenAnalyzer } from '../../index.js';
11
+
12
+ /**
13
+ * PerformanceDashboard class
14
+ * Generates text-based performance metrics with ASCII charts
15
+ */
16
+ export class PerformanceDashboard {
17
+ constructor(options = {}) {
18
+ this.projectRoot = options.projectRoot || process.cwd();
19
+ this.storagePath = options.storagePath || path.join(this.projectRoot, '.ctxman', 'metrics');
20
+ this.metrics = [];
21
+ this.currentMetrics = null;
22
+ this.startTime = null;
23
+ }
24
+
25
+ /**
26
+ * Start tracking a new analysis session
27
+ */
28
+ startAnalysis() {
29
+ this.startTime = Date.now();
30
+ this.currentMetrics = {
31
+ id: this.generateId(),
32
+ startedAt: new Date().toISOString(),
33
+ phases: {},
34
+ result: null,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Record a phase during analysis
40
+ * @param {string} name - Phase name (e.g., 'discovery', 'analysis', 'output')
41
+ * @param {number} duration - Duration in milliseconds
42
+ * @param {object} metadata - Additional metadata
43
+ */
44
+ recordPhase(name, duration, metadata = {}) {
45
+ if (!this.currentMetrics) {
46
+ this.currentMetrics = {
47
+ id: this.generateId(),
48
+ startedAt: new Date().toISOString(),
49
+ phases: {},
50
+ };
51
+ }
52
+ this.currentMetrics.phases[name] = {
53
+ duration,
54
+ ...metadata,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * End the current analysis session
60
+ * @param {object} result - Analysis result with totalFiles, totalTokens, cacheHits, cacheMisses
61
+ */
62
+ endAnalysis(result) {
63
+ if (!this.currentMetrics) return;
64
+
65
+ const endTime = Date.now();
66
+ this.currentMetrics.completedAt = new Date().toISOString();
67
+ this.currentMetrics.totalDuration = endTime - this.startTime;
68
+ this.currentMetrics.result = {
69
+ totalFiles: result.totalFiles || 0,
70
+ totalTokens: result.totalTokens || 0,
71
+ cacheHits: result.cacheHits || 0,
72
+ cacheMisses: result.cacheMisses || 0,
73
+ totalBytes: result.totalBytes || 0,
74
+ totalLines: result.totalLines || 0,
75
+ };
76
+
77
+ this.metrics.push(this.currentMetrics);
78
+ this.persist();
79
+ }
80
+
81
+ /**
82
+ * Generate unique ID for metrics entry
83
+ */
84
+ generateId() {
85
+ return `perf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
86
+ }
87
+
88
+ /**
89
+ * Persist metrics to storage
90
+ */
91
+ persist() {
92
+ try {
93
+ // Ensure directory exists
94
+ if (!fs.existsSync(this.storagePath)) {
95
+ fs.mkdirSync(this.storagePath, { recursive: true });
96
+ }
97
+
98
+ const metricsFile = path.join(this.storagePath, 'metrics.json');
99
+ const data = fs.existsSync(metricsFile)
100
+ ? JSON.parse(fs.readFileSync(metricsFile, 'utf8'))
101
+ : [];
102
+
103
+ data.push(...this.metrics);
104
+ fs.writeFileSync(metricsFile, JSON.stringify(data, null, 2));
105
+
106
+ // Clear in-memory metrics after persisting
107
+ this.metrics = [];
108
+ } catch (_error) {
109
+ // Silently fail - metrics are best-effort
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Load historical metrics from storage
115
+ */
116
+ loadMetrics() {
117
+ try {
118
+ const metricsFile = path.join(this.storagePath, 'metrics.json');
119
+ if (fs.existsSync(metricsFile)) {
120
+ return JSON.parse(fs.readFileSync(metricsFile, 'utf8'));
121
+ }
122
+ } catch (_error) {
123
+ // Return empty array if loading fails
124
+ }
125
+ return [];
126
+ }
127
+
128
+ /**
129
+ * Get aggregated metrics for a time period
130
+ * @param {string} period - Period string ('7d', '30d', 'all')
131
+ */
132
+ getAggregatedMetrics(period = '7d') {
133
+ const historicalMetrics = this.loadMetrics();
134
+ const allMetrics = [...historicalMetrics, ...this.metrics];
135
+
136
+ if (allMetrics.length === 0) {
137
+ return this.getEmptyMetrics();
138
+ }
139
+
140
+ const cutoff = this.getCutoffDate(period);
141
+ const filtered = allMetrics.filter((m) => new Date(m.startedAt) >= cutoff);
142
+
143
+ if (filtered.length === 0) {
144
+ return this.getEmptyMetrics();
145
+ }
146
+
147
+ return {
148
+ totalAnalyses: filtered.length,
149
+ avgDuration: this.average(filtered.map((m) => m.totalDuration || 0)),
150
+ avgFiles: this.average(filtered.map((m) => m.result?.totalFiles || 0)),
151
+ avgTokens: this.average(filtered.map((m) => m.result?.totalTokens || 0)),
152
+ cacheHitRate: this.calculateCacheHitRate(filtered),
153
+ totalFiles: filtered.reduce((sum, m) => sum + (m.result?.totalFiles || 0), 0),
154
+ totalTokens: filtered.reduce((sum, m) => sum + (m.result?.totalTokens || 0), 0),
155
+ phaseBreakdown: this.aggregatePhases(filtered),
156
+ trend: this.calculateTrend(filtered),
157
+ period,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Get empty metrics structure
163
+ */
164
+ getEmptyMetrics() {
165
+ return {
166
+ totalAnalyses: 0,
167
+ avgDuration: 0,
168
+ avgFiles: 0,
169
+ avgTokens: 0,
170
+ cacheHitRate: 0,
171
+ totalFiles: 0,
172
+ totalTokens: 0,
173
+ phaseBreakdown: {},
174
+ trend: [],
175
+ period: '7d',
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Get cutoff date for period
181
+ * @param {string} period - Period string
182
+ */
183
+ getCutoffDate(period) {
184
+ const now = new Date();
185
+ switch (period) {
186
+ case '7d':
187
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
188
+ case '30d':
189
+ return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
190
+ case 'all':
191
+ default:
192
+ return new Date(0); // Beginning of time
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Calculate average of numbers
198
+ * @param {number[]} values
199
+ */
200
+ average(values) {
201
+ if (values.length === 0) return 0;
202
+ const sum = values.reduce((a, b) => a + b, 0);
203
+ return Math.round(sum / values.length);
204
+ }
205
+
206
+ /**
207
+ * Calculate cache hit rate
208
+ * @param {object[]} metrics
209
+ */
210
+ calculateCacheHitRate(metrics) {
211
+ let totalHits = 0;
212
+ let totalRequests = 0;
213
+
214
+ for (const m of metrics) {
215
+ if (m.result) {
216
+ totalHits += m.result.cacheHits || 0;
217
+ totalRequests += (m.result.cacheHits || 0) + (m.result.cacheMisses || 0);
218
+ }
219
+ }
220
+
221
+ if (totalRequests === 0) return 0;
222
+ return Math.round((totalHits / totalRequests) * 100);
223
+ }
224
+
225
+ /**
226
+ * Aggregate phase durations across metrics
227
+ * @param {object[]} metrics
228
+ */
229
+ aggregatePhases(metrics) {
230
+ const phases = {};
231
+
232
+ for (const m of metrics) {
233
+ if (m.phases) {
234
+ for (const [name, data] of Object.entries(m.phases)) {
235
+ if (!phases[name]) {
236
+ phases[name] = { totalDuration: 0, count: 0 };
237
+ }
238
+ phases[name].totalDuration += data.duration || 0;
239
+ phases[name].count++;
240
+ }
241
+ }
242
+ }
243
+
244
+ // Calculate averages
245
+ for (const name of Object.keys(phases)) {
246
+ phases[name].avgDuration = Math.round(phases[name].totalDuration / phases[name].count);
247
+ }
248
+
249
+ return phases;
250
+ }
251
+
252
+ /**
253
+ * Calculate trend data for charts
254
+ * @param {object[]} metrics
255
+ */
256
+ calculateTrend(metrics) {
257
+ // Sort by date
258
+ const sorted = [...metrics].sort((a, b) => new Date(a.startedAt) - new Date(b.startedAt));
259
+
260
+ // Group by day
261
+ const byDay = {};
262
+ for (const m of sorted) {
263
+ const day = m.startedAt.split('T')[0];
264
+ if (!byDay[day]) {
265
+ byDay[day] = { count: 0, totalDuration: 0, totalTokens: 0 };
266
+ }
267
+ byDay[day].count++;
268
+ byDay[day].totalDuration += m.totalDuration || 0;
269
+ byDay[day].totalTokens += m.result?.totalTokens || 0;
270
+ }
271
+
272
+ // Convert to array
273
+ return Object.entries(byDay)
274
+ .map(([date, data]) => ({
275
+ date,
276
+ count: data.count,
277
+ avgDuration: Math.round(data.totalDuration / data.count),
278
+ totalTokens: data.totalTokens,
279
+ }))
280
+ .slice(-30); // Last 30 days
281
+ }
282
+
283
+ /**
284
+ * Identify performance bottlenecks
285
+ * @param {object} metrics - Aggregated metrics
286
+ */
287
+ identifyBottlenecks(metrics) {
288
+ const bottlenecks = [];
289
+ const { phaseBreakdown, avgDuration, avgFiles } = metrics;
290
+
291
+ // Check phase durations
292
+ if (phaseBreakdown) {
293
+ for (const [name, data] of Object.entries(phaseBreakdown)) {
294
+ const percentage = avgDuration > 0 ? Math.round((data.avgDuration / avgDuration) * 100) : 0;
295
+
296
+ if (percentage > 40 && data.avgDuration > 100) {
297
+ bottlenecks.push({
298
+ type: 'phase',
299
+ name,
300
+ avgDuration: data.avgDuration,
301
+ percentage,
302
+ severity: percentage > 70 ? 'high' : 'medium',
303
+ });
304
+ }
305
+ }
306
+ }
307
+
308
+ // Check for large file counts
309
+ if (avgFiles > 1000) {
310
+ bottlenecks.push({
311
+ type: 'file_count',
312
+ name: 'Large project size',
313
+ avgFiles,
314
+ severity: 'medium',
315
+ suggestion: 'Consider using .contextignore to exclude unnecessary files',
316
+ });
317
+ }
318
+
319
+ // Check cache effectiveness
320
+ if (metrics.cacheHitRate < 50 && metrics.totalAnalyses > 2) {
321
+ bottlenecks.push({
322
+ type: 'cache',
323
+ name: 'Low cache hit rate',
324
+ rate: metrics.cacheHitRate,
325
+ severity: 'medium',
326
+ suggestion: 'Cache warming may improve performance',
327
+ });
328
+ }
329
+
330
+ // Sort by severity
331
+ const severityOrder = { high: 0, medium: 1, low: 2 };
332
+ return bottlenecks.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
333
+ }
334
+
335
+ /**
336
+ * Generate optimization recommendations
337
+ * @param {object} metrics - Aggregated metrics
338
+ * @param {object[]} bottlenecks - Identified bottlenecks
339
+ */
340
+ generateRecommendations(metrics, bottlenecks) {
341
+ const recommendations = [];
342
+
343
+ // Based on bottlenecks
344
+ for (const b of bottlenecks) {
345
+ if (b.suggestion) {
346
+ recommendations.push({
347
+ type: 'performance',
348
+ priority: b.severity === 'high' ? 'high' : 'medium',
349
+ message: b.suggestion,
350
+ details: b,
351
+ });
352
+ }
353
+ }
354
+
355
+ // Based on token counts
356
+ if (metrics.avgTokens > 50000) {
357
+ recommendations.push({
358
+ type: 'context',
359
+ priority: 'medium',
360
+ message: 'Consider chunking for large context outputs',
361
+ details: { avgTokens: metrics.avgTokens },
362
+ });
363
+ }
364
+
365
+ // Based on file distribution
366
+ if (metrics.avgFiles > 500) {
367
+ recommendations.push({
368
+ type: 'filtering',
369
+ priority: 'low',
370
+ message: 'Review .contextignore for unnecessary file types',
371
+ details: { avgFiles: metrics.avgFiles },
372
+ });
373
+ }
374
+
375
+ // Cache recommendation
376
+ if (metrics.cacheHitRate < 30 && metrics.totalAnalyses > 1) {
377
+ recommendations.push({
378
+ type: 'cache',
379
+ priority: 'medium',
380
+ message: 'Enable caching for frequently analyzed files',
381
+ details: { currentRate: metrics.cacheHitRate },
382
+ });
383
+ }
384
+
385
+ // Remove duplicates by message
386
+ const seen = new Set();
387
+ return recommendations.filter((r) => {
388
+ if (seen.has(r.message)) return false;
389
+ seen.add(r.message);
390
+ return true;
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Run analysis and collect current metrics
396
+ * @param {object} options - Analysis options
397
+ */
398
+ async runAnalysis(options = {}) {
399
+ this.startAnalysis();
400
+
401
+ // Run discovery phase
402
+ const discoveryStart = Date.now();
403
+ const analyzer = new TokenAnalyzer(this.projectRoot, {
404
+ ...options,
405
+ simple: true,
406
+ verbose: false,
407
+ dashboard: true,
408
+ });
409
+
410
+ const stats = analyzer.run();
411
+ this.recordPhase('discovery', Date.now() - discoveryStart, {
412
+ fileCount: stats.totalFiles,
413
+ });
414
+
415
+ // Record analysis phase (approximate)
416
+ const analysisDuration =
417
+ stats.largestFiles?.length > 0
418
+ ? Math.round((stats.totalTokens / 10000) * 100) // Estimate based on token count
419
+ : 100;
420
+ this.recordPhase('analysis', analysisDuration, {
421
+ totalTokens: stats.totalTokens,
422
+ });
423
+
424
+ // End analysis
425
+ this.endAnalysis({
426
+ totalFiles: stats.totalFiles,
427
+ totalTokens: stats.totalTokens,
428
+ totalBytes: stats.totalBytes,
429
+ totalLines: stats.totalLines,
430
+ cacheHits: 0,
431
+ cacheMisses: 0,
432
+ });
433
+
434
+ return stats;
435
+ }
436
+
437
+ /**
438
+ * Generate ASCII bar chart
439
+ * @param {number} value - Current value
440
+ * @param {number} max - Maximum value
441
+ * @param {number} width - Chart width in characters
442
+ */
443
+ generateBarChart(value, max, width = 20) {
444
+ if (max === 0) return '░'.repeat(width);
445
+
446
+ const ratio = Math.min(value / max, 1); // Clamp to max 1
447
+ const filled = Math.round(ratio * width);
448
+ const empty = width - filled;
449
+
450
+ return '▓'.repeat(filled) + '░'.repeat(empty);
451
+ }
452
+
453
+ /**
454
+ * Generate ASCII sparkline
455
+ * @param {number[]} values - Array of values
456
+ * @param {number} width - Sparkline width
457
+ */
458
+ generateSparkline(values, width = 40) {
459
+ if (!values || values.length === 0) {
460
+ return '░'.repeat(width);
461
+ }
462
+
463
+ const chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
464
+ const max = Math.max(...values);
465
+ const min = Math.min(...values);
466
+ const range = max - min || 1;
467
+
468
+ // Normalize and map to characters
469
+ const normalized = values.map((v) => {
470
+ const normalized = (v - min) / range;
471
+ const index = Math.min(Math.floor(normalized * chars.length), chars.length - 1);
472
+ return chars[index];
473
+ });
474
+
475
+ // Pad or trim to width
476
+ if (normalized.length < width) {
477
+ return normalized.join('') + '░'.repeat(width - normalized.length);
478
+ } else if (normalized.length > width) {
479
+ return normalized.slice(-width).join('');
480
+ }
481
+ return normalized.join('');
482
+ }
483
+
484
+ /**
485
+ * Generate ASCII trend chart
486
+ * @param {object[]} trend - Trend data points
487
+ * @param {string} field - Field to chart ('avgDuration', 'totalTokens', 'count')
488
+ * @param {number} width - Chart width
489
+ * @param {number} height - Chart height
490
+ */
491
+ generateTrendChart(trend, field = 'avgDuration', width = 60, height = 6) {
492
+ if (!trend || trend.length === 0) {
493
+ return '│' + '\n'.repeat(height - 1);
494
+ }
495
+
496
+ const values = trend.map((t) => t[field] || 0);
497
+ const max = Math.max(...values);
498
+ const min = Math.min(...values);
499
+ const range = max - min || 1;
500
+
501
+ // Create grid
502
+ const grid = [];
503
+ for (let y = 0; y < height; y++) {
504
+ grid[y] = [];
505
+ for (let x = 0; x < width; x++) {
506
+ grid[y][x] = ' ';
507
+ }
508
+ }
509
+
510
+ // Plot values
511
+ const step = Math.max(1, Math.floor(values.length / width));
512
+ for (let i = 0; i < values.length && i * step < width; i++) {
513
+ const x = Math.floor((i / (values.length - 1 || 1)) * (width - 1));
514
+ const normalized = (values[i] - min) / range;
515
+ const y = height - 1 - Math.floor(normalized * (height - 1));
516
+
517
+ grid[y][x] = '█';
518
+
519
+ // Fill below
520
+ for (let fillY = y + 1; fillY < height; fillY++) {
521
+ grid[fillY][x] = '░';
522
+ }
523
+ }
524
+
525
+ // Add axes
526
+ const lines = [];
527
+ for (let y = 0; y < height; y++) {
528
+ lines.push('│' + grid[y].join(''));
529
+ }
530
+ lines.push('└' + '─'.repeat(width));
531
+
532
+ return lines.join('\n');
533
+ }
534
+
535
+ /**
536
+ * Render the full dashboard
537
+ * @param {object} options - Render options
538
+ */
539
+ render(options = {}) {
540
+ const period = options.period || '7d';
541
+ const metrics = this.getAggregatedMetrics(period);
542
+ const bottlenecks = this.identifyBottlenecks(metrics);
543
+ const recommendations = this.generateRecommendations(metrics, bottlenecks);
544
+
545
+ const lines = [];
546
+
547
+ // Header
548
+ lines.push('');
549
+ lines.push('═'.repeat(70));
550
+ lines.push(' CTXMAN PERFORMANCE DASHBOARD');
551
+ lines.push('═'.repeat(70));
552
+ lines.push('');
553
+
554
+ // Overview metrics
555
+ lines.push('┌' + '─'.repeat(30) + '┬' + '─'.repeat(30) + '┐');
556
+ lines.push('│ Overview │ Analysis Stats │');
557
+ lines.push('├' + '─'.repeat(30) + '┼' + '─'.repeat(30) + '┤');
558
+
559
+ // Total analyses
560
+ const totalAnalysesStr = metrics.totalAnalyses.toLocaleString().padStart(10);
561
+ lines.push(
562
+ `│ Total Analyses:${totalAnalysesStr} │ Avg Duration: ${(metrics.avgDuration || 0).toLocaleString()} ms │`
563
+ );
564
+
565
+ // Average files
566
+ const avgFilesStr = metrics.avgFiles.toLocaleString().padStart(10);
567
+ lines.push(
568
+ `│ Avg Files: ${avgFilesStr} │ Avg Tokens: ${(metrics.avgTokens || 0).toLocaleString().padStart(10)} │`
569
+ );
570
+
571
+ // Cache hit rate
572
+ const cacheBar = this.generateBarChart(metrics.cacheHitRate, 100, 10);
573
+ lines.push(
574
+ `│ Cache Hit Rate: ${cacheBar} ${String(metrics.cacheHitRate).padStart(3)}% │ │`
575
+ );
576
+
577
+ lines.push('└' + '─'.repeat(30) + '┴' + '─'.repeat(30) + '┘');
578
+ lines.push('');
579
+
580
+ // Performance trend
581
+ if (metrics.trend && metrics.trend.length > 0) {
582
+ lines.push('┌' + '─'.repeat(66) + '┐');
583
+ lines.push(
584
+ '│ Performance Trend (Last ' +
585
+ String(metrics.trend.length).padStart(2) +
586
+ ' days)' +
587
+ ' '.repeat(38) +
588
+ '│'
589
+ );
590
+ lines.push('├' + '─'.repeat(66) + '┤');
591
+
592
+ // Duration sparkline
593
+ const durationValues = metrics.trend.map((t) => t.avgDuration);
594
+ const durationSparkline = this.generateSparkline(durationValues, 40);
595
+ lines.push(`│ Duration (ms) ${durationSparkline} │`);
596
+
597
+ // Token sparkline
598
+ const tokenValues = metrics.trend.map((t) => Math.round(t.totalTokens / 1000));
599
+ const tokenSparkline = this.generateSparkline(tokenValues, 40);
600
+ lines.push(`│ Tokens (K) ${tokenSparkline} │`);
601
+
602
+ // Analysis count sparkline
603
+ const countValues = metrics.trend.map((t) => t.count);
604
+ const countSparkline = this.generateSparkline(countValues, 40);
605
+ lines.push(`│ Analyses ${countSparkline} │`);
606
+
607
+ lines.push('└' + '─'.repeat(66) + '┘');
608
+ lines.push('');
609
+ }
610
+
611
+ // Phase breakdown
612
+ if (Object.keys(metrics.phaseBreakdown).length > 0) {
613
+ lines.push('┌' + '─'.repeat(66) + '┐');
614
+ lines.push('│ Phase Breakdown │');
615
+ lines.push('├' + '─'.repeat(66) + '┤');
616
+
617
+ const phases = Object.entries(metrics.phaseBreakdown).sort(
618
+ (a, b) => b[1].avgDuration - a[1].avgDuration
619
+ );
620
+
621
+ for (const [name, data] of phases) {
622
+ const bar = this.generateBarChart(data.avgDuration, metrics.avgDuration || 1, 20);
623
+ const pct =
624
+ metrics.avgDuration > 0 ? Math.round((data.avgDuration / metrics.avgDuration) * 100) : 0;
625
+ lines.push(
626
+ `│ ${(name + ':').padEnd(15)} ${bar} ${String(data.avgDuration).padStart(6)} ms (${String(pct).padStart(3)}%) │`
627
+ );
628
+ }
629
+
630
+ lines.push('└' + '─'.repeat(66) + '┘');
631
+ lines.push('');
632
+ }
633
+
634
+ // Bottlenecks
635
+ if (bottlenecks.length > 0) {
636
+ lines.push('┌' + '─'.repeat(66) + '┐');
637
+ lines.push('│ Top Bottlenecks │');
638
+ lines.push('├' + '─'.repeat(66) + '┤');
639
+
640
+ for (let i = 0; i < Math.min(bottlenecks.length, 5); i++) {
641
+ const b = bottlenecks[i];
642
+ const severity = b.severity === 'high' ? '⚠️ ' : ' ';
643
+ const name = b.name.padEnd(40);
644
+ const info = b.avgDuration ? `${b.avgDuration} ms avg` : b.rate ? `${b.rate}%` : '';
645
+ lines.push(`│ ${severity}${i + 1}. ${name} ${info.padStart(15)} │`);
646
+ }
647
+
648
+ lines.push('└' + '─'.repeat(66) + '┘');
649
+ lines.push('');
650
+ }
651
+
652
+ // Recommendations
653
+ if (recommendations.length > 0) {
654
+ lines.push('┌' + '─'.repeat(66) + '┐');
655
+ lines.push('│ Optimization Recommendations │');
656
+ lines.push('├' + '─'.repeat(66) + '┤');
657
+
658
+ for (const r of recommendations.slice(0, 5)) {
659
+ const priority = r.priority === 'high' ? '🔴' : r.priority === 'medium' ? '🟡' : '🟢';
660
+ lines.push(`│ ${priority} ${r.message.slice(0, 60).padEnd(60)} │`);
661
+ }
662
+
663
+ lines.push('└' + '─'.repeat(66) + '┘');
664
+ lines.push('');
665
+ }
666
+
667
+ // Legend
668
+ lines.push('Legend: ▓ active ░ inactive █ high ▇ medium ▆ low');
669
+ lines.push('');
670
+
671
+ return lines.join('\n');
672
+ }
673
+
674
+ /**
675
+ * Display dashboard to console
676
+ * @param {object} options - Display options
677
+ */
678
+ display(options = {}) {
679
+ console.log(this.render(options));
680
+ }
681
+ }
682
+
683
+ export default PerformanceDashboard;