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,687 @@
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
+ let data = [];
100
+ try {
101
+ data = JSON.parse(fs.readFileSync(metricsFile, 'utf8'));
102
+ } catch (error) {
103
+ // No metrics yet; an unreadable file stops the save, as before
104
+ if (error.code !== 'ENOENT') throw error;
105
+ }
106
+
107
+ data.push(...this.metrics);
108
+ fs.writeFileSync(metricsFile, JSON.stringify(data, null, 2));
109
+
110
+ // Clear in-memory metrics after persisting
111
+ this.metrics = [];
112
+ } catch (_error) {
113
+ // Silently fail - metrics are best-effort
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Load historical metrics from storage
119
+ */
120
+ loadMetrics() {
121
+ try {
122
+ const metricsFile = path.join(this.storagePath, 'metrics.json');
123
+ if (fs.existsSync(metricsFile)) {
124
+ return JSON.parse(fs.readFileSync(metricsFile, 'utf8'));
125
+ }
126
+ } catch (_error) {
127
+ // Return empty array if loading fails
128
+ }
129
+ return [];
130
+ }
131
+
132
+ /**
133
+ * Get aggregated metrics for a time period
134
+ * @param {string} period - Period string ('7d', '30d', 'all')
135
+ */
136
+ getAggregatedMetrics(period = '7d') {
137
+ const historicalMetrics = this.loadMetrics();
138
+ const allMetrics = [...historicalMetrics, ...this.metrics];
139
+
140
+ if (allMetrics.length === 0) {
141
+ return this.getEmptyMetrics();
142
+ }
143
+
144
+ const cutoff = this.getCutoffDate(period);
145
+ const filtered = allMetrics.filter((m) => new Date(m.startedAt) >= cutoff);
146
+
147
+ if (filtered.length === 0) {
148
+ return this.getEmptyMetrics();
149
+ }
150
+
151
+ return {
152
+ totalAnalyses: filtered.length,
153
+ avgDuration: this.average(filtered.map((m) => m.totalDuration || 0)),
154
+ avgFiles: this.average(filtered.map((m) => m.result?.totalFiles || 0)),
155
+ avgTokens: this.average(filtered.map((m) => m.result?.totalTokens || 0)),
156
+ cacheHitRate: this.calculateCacheHitRate(filtered),
157
+ totalFiles: filtered.reduce((sum, m) => sum + (m.result?.totalFiles || 0), 0),
158
+ totalTokens: filtered.reduce((sum, m) => sum + (m.result?.totalTokens || 0), 0),
159
+ phaseBreakdown: this.aggregatePhases(filtered),
160
+ trend: this.calculateTrend(filtered),
161
+ period,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Get empty metrics structure
167
+ */
168
+ getEmptyMetrics() {
169
+ return {
170
+ totalAnalyses: 0,
171
+ avgDuration: 0,
172
+ avgFiles: 0,
173
+ avgTokens: 0,
174
+ cacheHitRate: 0,
175
+ totalFiles: 0,
176
+ totalTokens: 0,
177
+ phaseBreakdown: {},
178
+ trend: [],
179
+ period: '7d',
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Get cutoff date for period
185
+ * @param {string} period - Period string
186
+ */
187
+ getCutoffDate(period) {
188
+ const now = new Date();
189
+ switch (period) {
190
+ case '7d':
191
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
192
+ case '30d':
193
+ return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
194
+ case 'all':
195
+ default:
196
+ return new Date(0); // Beginning of time
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Calculate average of numbers
202
+ * @param {number[]} values
203
+ */
204
+ average(values) {
205
+ if (values.length === 0) return 0;
206
+ const sum = values.reduce((a, b) => a + b, 0);
207
+ return Math.round(sum / values.length);
208
+ }
209
+
210
+ /**
211
+ * Calculate cache hit rate
212
+ * @param {object[]} metrics
213
+ */
214
+ calculateCacheHitRate(metrics) {
215
+ let totalHits = 0;
216
+ let totalRequests = 0;
217
+
218
+ for (const m of metrics) {
219
+ if (m.result) {
220
+ totalHits += m.result.cacheHits || 0;
221
+ totalRequests += (m.result.cacheHits || 0) + (m.result.cacheMisses || 0);
222
+ }
223
+ }
224
+
225
+ if (totalRequests === 0) return 0;
226
+ return Math.round((totalHits / totalRequests) * 100);
227
+ }
228
+
229
+ /**
230
+ * Aggregate phase durations across metrics
231
+ * @param {object[]} metrics
232
+ */
233
+ aggregatePhases(metrics) {
234
+ const phases = {};
235
+
236
+ for (const m of metrics) {
237
+ if (m.phases) {
238
+ for (const [name, data] of Object.entries(m.phases)) {
239
+ if (!phases[name]) {
240
+ phases[name] = { totalDuration: 0, count: 0 };
241
+ }
242
+ phases[name].totalDuration += data.duration || 0;
243
+ phases[name].count++;
244
+ }
245
+ }
246
+ }
247
+
248
+ // Calculate averages
249
+ for (const name of Object.keys(phases)) {
250
+ phases[name].avgDuration = Math.round(phases[name].totalDuration / phases[name].count);
251
+ }
252
+
253
+ return phases;
254
+ }
255
+
256
+ /**
257
+ * Calculate trend data for charts
258
+ * @param {object[]} metrics
259
+ */
260
+ calculateTrend(metrics) {
261
+ // Sort by date
262
+ const sorted = [...metrics].sort((a, b) => new Date(a.startedAt) - new Date(b.startedAt));
263
+
264
+ // Group by day
265
+ const byDay = {};
266
+ for (const m of sorted) {
267
+ const day = m.startedAt.split('T')[0];
268
+ if (!byDay[day]) {
269
+ byDay[day] = { count: 0, totalDuration: 0, totalTokens: 0 };
270
+ }
271
+ byDay[day].count++;
272
+ byDay[day].totalDuration += m.totalDuration || 0;
273
+ byDay[day].totalTokens += m.result?.totalTokens || 0;
274
+ }
275
+
276
+ // Convert to array
277
+ return Object.entries(byDay)
278
+ .map(([date, data]) => ({
279
+ date,
280
+ count: data.count,
281
+ avgDuration: Math.round(data.totalDuration / data.count),
282
+ totalTokens: data.totalTokens,
283
+ }))
284
+ .slice(-30); // Last 30 days
285
+ }
286
+
287
+ /**
288
+ * Identify performance bottlenecks
289
+ * @param {object} metrics - Aggregated metrics
290
+ */
291
+ identifyBottlenecks(metrics) {
292
+ const bottlenecks = [];
293
+ const { phaseBreakdown, avgDuration, avgFiles } = metrics;
294
+
295
+ // Check phase durations
296
+ if (phaseBreakdown) {
297
+ for (const [name, data] of Object.entries(phaseBreakdown)) {
298
+ const percentage = avgDuration > 0 ? Math.round((data.avgDuration / avgDuration) * 100) : 0;
299
+
300
+ if (percentage > 40 && data.avgDuration > 100) {
301
+ bottlenecks.push({
302
+ type: 'phase',
303
+ name,
304
+ avgDuration: data.avgDuration,
305
+ percentage,
306
+ severity: percentage > 70 ? 'high' : 'medium',
307
+ });
308
+ }
309
+ }
310
+ }
311
+
312
+ // Check for large file counts
313
+ if (avgFiles > 1000) {
314
+ bottlenecks.push({
315
+ type: 'file_count',
316
+ name: 'Large project size',
317
+ avgFiles,
318
+ severity: 'medium',
319
+ suggestion: 'Consider using .contextignore to exclude unnecessary files',
320
+ });
321
+ }
322
+
323
+ // Check cache effectiveness
324
+ if (metrics.cacheHitRate < 50 && metrics.totalAnalyses > 2) {
325
+ bottlenecks.push({
326
+ type: 'cache',
327
+ name: 'Low cache hit rate',
328
+ rate: metrics.cacheHitRate,
329
+ severity: 'medium',
330
+ suggestion: 'Cache warming may improve performance',
331
+ });
332
+ }
333
+
334
+ // Sort by severity
335
+ const severityOrder = { high: 0, medium: 1, low: 2 };
336
+ return bottlenecks.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
337
+ }
338
+
339
+ /**
340
+ * Generate optimization recommendations
341
+ * @param {object} metrics - Aggregated metrics
342
+ * @param {object[]} bottlenecks - Identified bottlenecks
343
+ */
344
+ generateRecommendations(metrics, bottlenecks) {
345
+ const recommendations = [];
346
+
347
+ // Based on bottlenecks
348
+ for (const b of bottlenecks) {
349
+ if (b.suggestion) {
350
+ recommendations.push({
351
+ type: 'performance',
352
+ priority: b.severity === 'high' ? 'high' : 'medium',
353
+ message: b.suggestion,
354
+ details: b,
355
+ });
356
+ }
357
+ }
358
+
359
+ // Based on token counts
360
+ if (metrics.avgTokens > 50000) {
361
+ recommendations.push({
362
+ type: 'context',
363
+ priority: 'medium',
364
+ message: 'Consider chunking for large context outputs',
365
+ details: { avgTokens: metrics.avgTokens },
366
+ });
367
+ }
368
+
369
+ // Based on file distribution
370
+ if (metrics.avgFiles > 500) {
371
+ recommendations.push({
372
+ type: 'filtering',
373
+ priority: 'low',
374
+ message: 'Review .contextignore for unnecessary file types',
375
+ details: { avgFiles: metrics.avgFiles },
376
+ });
377
+ }
378
+
379
+ // Cache recommendation
380
+ if (metrics.cacheHitRate < 30 && metrics.totalAnalyses > 1) {
381
+ recommendations.push({
382
+ type: 'cache',
383
+ priority: 'medium',
384
+ message: 'Enable caching for frequently analyzed files',
385
+ details: { currentRate: metrics.cacheHitRate },
386
+ });
387
+ }
388
+
389
+ // Remove duplicates by message
390
+ const seen = new Set();
391
+ return recommendations.filter((r) => {
392
+ if (seen.has(r.message)) return false;
393
+ seen.add(r.message);
394
+ return true;
395
+ });
396
+ }
397
+
398
+ /**
399
+ * Run analysis and collect current metrics
400
+ * @param {object} options - Analysis options
401
+ */
402
+ async runAnalysis(options = {}) {
403
+ this.startAnalysis();
404
+
405
+ // Run discovery phase
406
+ const discoveryStart = Date.now();
407
+ const analyzer = new TokenAnalyzer(this.projectRoot, {
408
+ ...options,
409
+ simple: true,
410
+ verbose: false,
411
+ dashboard: true,
412
+ });
413
+
414
+ const stats = analyzer.run();
415
+ this.recordPhase('discovery', Date.now() - discoveryStart, {
416
+ fileCount: stats.totalFiles,
417
+ });
418
+
419
+ // Record analysis phase (approximate)
420
+ const analysisDuration =
421
+ stats.largestFiles?.length > 0
422
+ ? Math.round((stats.totalTokens / 10000) * 100) // Estimate based on token count
423
+ : 100;
424
+ this.recordPhase('analysis', analysisDuration, {
425
+ totalTokens: stats.totalTokens,
426
+ });
427
+
428
+ // End analysis
429
+ this.endAnalysis({
430
+ totalFiles: stats.totalFiles,
431
+ totalTokens: stats.totalTokens,
432
+ totalBytes: stats.totalBytes,
433
+ totalLines: stats.totalLines,
434
+ cacheHits: 0,
435
+ cacheMisses: 0,
436
+ });
437
+
438
+ return stats;
439
+ }
440
+
441
+ /**
442
+ * Generate ASCII bar chart
443
+ * @param {number} value - Current value
444
+ * @param {number} max - Maximum value
445
+ * @param {number} width - Chart width in characters
446
+ */
447
+ generateBarChart(value, max, width = 20) {
448
+ if (max === 0) return '░'.repeat(width);
449
+
450
+ const ratio = Math.min(value / max, 1); // Clamp to max 1
451
+ const filled = Math.round(ratio * width);
452
+ const empty = width - filled;
453
+
454
+ return '▓'.repeat(filled) + '░'.repeat(empty);
455
+ }
456
+
457
+ /**
458
+ * Generate ASCII sparkline
459
+ * @param {number[]} values - Array of values
460
+ * @param {number} width - Sparkline width
461
+ */
462
+ generateSparkline(values, width = 40) {
463
+ if (!values || values.length === 0) {
464
+ return '░'.repeat(width);
465
+ }
466
+
467
+ const chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
468
+ const max = Math.max(...values);
469
+ const min = Math.min(...values);
470
+ const range = max - min || 1;
471
+
472
+ // Normalize and map to characters
473
+ const normalized = values.map((v) => {
474
+ const normalized = (v - min) / range;
475
+ const index = Math.min(Math.floor(normalized * chars.length), chars.length - 1);
476
+ return chars[index];
477
+ });
478
+
479
+ // Pad or trim to width
480
+ if (normalized.length < width) {
481
+ return normalized.join('') + '░'.repeat(width - normalized.length);
482
+ } else if (normalized.length > width) {
483
+ return normalized.slice(-width).join('');
484
+ }
485
+ return normalized.join('');
486
+ }
487
+
488
+ /**
489
+ * Generate ASCII trend chart
490
+ * @param {object[]} trend - Trend data points
491
+ * @param {string} field - Field to chart ('avgDuration', 'totalTokens', 'count')
492
+ * @param {number} width - Chart width
493
+ * @param {number} height - Chart height
494
+ */
495
+ generateTrendChart(trend, field = 'avgDuration', width = 60, height = 6) {
496
+ if (!trend || trend.length === 0) {
497
+ return '│' + '\n'.repeat(height - 1);
498
+ }
499
+
500
+ const values = trend.map((t) => t[field] || 0);
501
+ const max = Math.max(...values);
502
+ const min = Math.min(...values);
503
+ const range = max - min || 1;
504
+
505
+ // Create grid
506
+ const grid = [];
507
+ for (let y = 0; y < height; y++) {
508
+ grid[y] = [];
509
+ for (let x = 0; x < width; x++) {
510
+ grid[y][x] = ' ';
511
+ }
512
+ }
513
+
514
+ // Plot values
515
+ const step = Math.max(1, Math.floor(values.length / width));
516
+ for (let i = 0; i < values.length && i * step < width; i++) {
517
+ const x = Math.floor((i / (values.length - 1 || 1)) * (width - 1));
518
+ const normalized = (values[i] - min) / range;
519
+ const y = height - 1 - Math.floor(normalized * (height - 1));
520
+
521
+ grid[y][x] = '█';
522
+
523
+ // Fill below
524
+ for (let fillY = y + 1; fillY < height; fillY++) {
525
+ grid[fillY][x] = '░';
526
+ }
527
+ }
528
+
529
+ // Add axes
530
+ const lines = [];
531
+ for (let y = 0; y < height; y++) {
532
+ lines.push('│' + grid[y].join(''));
533
+ }
534
+ lines.push('└' + '─'.repeat(width));
535
+
536
+ return lines.join('\n');
537
+ }
538
+
539
+ /**
540
+ * Render the full dashboard
541
+ * @param {object} options - Render options
542
+ */
543
+ render(options = {}) {
544
+ const period = options.period || '7d';
545
+ const metrics = this.getAggregatedMetrics(period);
546
+ const bottlenecks = this.identifyBottlenecks(metrics);
547
+ const recommendations = this.generateRecommendations(metrics, bottlenecks);
548
+
549
+ const lines = [];
550
+
551
+ // Header
552
+ lines.push('');
553
+ lines.push('═'.repeat(70));
554
+ lines.push(' CTXMAN PERFORMANCE DASHBOARD');
555
+ lines.push('═'.repeat(70));
556
+ lines.push('');
557
+
558
+ // Overview metrics
559
+ lines.push('┌' + '─'.repeat(30) + '┬' + '─'.repeat(30) + '┐');
560
+ lines.push('│ Overview │ Analysis Stats │');
561
+ lines.push('├' + '─'.repeat(30) + '┼' + '─'.repeat(30) + '┤');
562
+
563
+ // Total analyses
564
+ const totalAnalysesStr = metrics.totalAnalyses.toLocaleString().padStart(10);
565
+ lines.push(
566
+ `│ Total Analyses:${totalAnalysesStr} │ Avg Duration: ${(metrics.avgDuration || 0).toLocaleString()} ms │`
567
+ );
568
+
569
+ // Average files
570
+ const avgFilesStr = metrics.avgFiles.toLocaleString().padStart(10);
571
+ lines.push(
572
+ `│ Avg Files: ${avgFilesStr} │ Avg Tokens: ${(metrics.avgTokens || 0).toLocaleString().padStart(10)} │`
573
+ );
574
+
575
+ // Cache hit rate
576
+ const cacheBar = this.generateBarChart(metrics.cacheHitRate, 100, 10);
577
+ lines.push(
578
+ `│ Cache Hit Rate: ${cacheBar} ${String(metrics.cacheHitRate).padStart(3)}% │ │`
579
+ );
580
+
581
+ lines.push('└' + '─'.repeat(30) + '┴' + '─'.repeat(30) + '┘');
582
+ lines.push('');
583
+
584
+ // Performance trend
585
+ if (metrics.trend && metrics.trend.length > 0) {
586
+ lines.push('┌' + '─'.repeat(66) + '┐');
587
+ lines.push(
588
+ '│ Performance Trend (Last ' +
589
+ String(metrics.trend.length).padStart(2) +
590
+ ' days)' +
591
+ ' '.repeat(38) +
592
+ '│'
593
+ );
594
+ lines.push('├' + '─'.repeat(66) + '┤');
595
+
596
+ // Duration sparkline
597
+ const durationValues = metrics.trend.map((t) => t.avgDuration);
598
+ const durationSparkline = this.generateSparkline(durationValues, 40);
599
+ lines.push(`│ Duration (ms) ${durationSparkline} │`);
600
+
601
+ // Token sparkline
602
+ const tokenValues = metrics.trend.map((t) => Math.round(t.totalTokens / 1000));
603
+ const tokenSparkline = this.generateSparkline(tokenValues, 40);
604
+ lines.push(`│ Tokens (K) ${tokenSparkline} │`);
605
+
606
+ // Analysis count sparkline
607
+ const countValues = metrics.trend.map((t) => t.count);
608
+ const countSparkline = this.generateSparkline(countValues, 40);
609
+ lines.push(`│ Analyses ${countSparkline} │`);
610
+
611
+ lines.push('└' + '─'.repeat(66) + '┘');
612
+ lines.push('');
613
+ }
614
+
615
+ // Phase breakdown
616
+ if (Object.keys(metrics.phaseBreakdown).length > 0) {
617
+ lines.push('┌' + '─'.repeat(66) + '┐');
618
+ lines.push('│ Phase Breakdown │');
619
+ lines.push('├' + '─'.repeat(66) + '┤');
620
+
621
+ const phases = Object.entries(metrics.phaseBreakdown).sort(
622
+ (a, b) => b[1].avgDuration - a[1].avgDuration
623
+ );
624
+
625
+ for (const [name, data] of phases) {
626
+ const bar = this.generateBarChart(data.avgDuration, metrics.avgDuration || 1, 20);
627
+ const pct =
628
+ metrics.avgDuration > 0 ? Math.round((data.avgDuration / metrics.avgDuration) * 100) : 0;
629
+ lines.push(
630
+ `│ ${(name + ':').padEnd(15)} ${bar} ${String(data.avgDuration).padStart(6)} ms (${String(pct).padStart(3)}%) │`
631
+ );
632
+ }
633
+
634
+ lines.push('└' + '─'.repeat(66) + '┘');
635
+ lines.push('');
636
+ }
637
+
638
+ // Bottlenecks
639
+ if (bottlenecks.length > 0) {
640
+ lines.push('┌' + '─'.repeat(66) + '┐');
641
+ lines.push('│ Top Bottlenecks │');
642
+ lines.push('├' + '─'.repeat(66) + '┤');
643
+
644
+ for (let i = 0; i < Math.min(bottlenecks.length, 5); i++) {
645
+ const b = bottlenecks[i];
646
+ const severity = b.severity === 'high' ? '⚠️ ' : ' ';
647
+ const name = b.name.padEnd(40);
648
+ const info = b.avgDuration ? `${b.avgDuration} ms avg` : b.rate ? `${b.rate}%` : '';
649
+ lines.push(`│ ${severity}${i + 1}. ${name} ${info.padStart(15)} │`);
650
+ }
651
+
652
+ lines.push('└' + '─'.repeat(66) + '┘');
653
+ lines.push('');
654
+ }
655
+
656
+ // Recommendations
657
+ if (recommendations.length > 0) {
658
+ lines.push('┌' + '─'.repeat(66) + '┐');
659
+ lines.push('│ Optimization Recommendations │');
660
+ lines.push('├' + '─'.repeat(66) + '┤');
661
+
662
+ for (const r of recommendations.slice(0, 5)) {
663
+ const priority = r.priority === 'high' ? '🔴' : r.priority === 'medium' ? '🟡' : '🟢';
664
+ lines.push(`│ ${priority} ${r.message.slice(0, 60).padEnd(60)} │`);
665
+ }
666
+
667
+ lines.push('└' + '─'.repeat(66) + '┘');
668
+ lines.push('');
669
+ }
670
+
671
+ // Legend
672
+ lines.push('Legend: ▓ active ░ inactive █ high ▇ medium ▆ low');
673
+ lines.push('');
674
+
675
+ return lines.join('\n');
676
+ }
677
+
678
+ /**
679
+ * Display dashboard to console
680
+ * @param {object} options - Display options
681
+ */
682
+ display(options = {}) {
683
+ console.log(this.render(options));
684
+ }
685
+ }
686
+
687
+ export default PerformanceDashboard;