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,532 @@
1
+ /**
2
+ * Context Versioning (FEAT-012)
3
+ * Track and restore context versions over time
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import crypto from 'crypto';
9
+ import { execSync } from 'child_process';
10
+
11
+ const VERSION_DIR = '.ctxman/versions';
12
+ const VERSION_INDEX = 'index.json';
13
+
14
+ /**
15
+ * VersionStorage - Handles version storage operations
16
+ */
17
+ export class VersionStorage {
18
+ constructor(projectRoot) {
19
+ this.projectRoot = projectRoot;
20
+ this.versionDir = path.join(projectRoot, VERSION_DIR);
21
+ this.indexFile = path.join(this.versionDir, VERSION_INDEX);
22
+ }
23
+
24
+ /**
25
+ * Initialize version directory and index
26
+ */
27
+ async init() {
28
+ await fs.promises.mkdir(this.versionDir, { recursive: true });
29
+
30
+ if (!fs.existsSync(this.indexFile)) {
31
+ await this.saveIndex({ versions: [] });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Generate a unique version ID
37
+ * @returns {string} Version ID in format ctx-v001
38
+ */
39
+ generateVersionId(index) {
40
+ const count = index.versions.length + 1;
41
+ return `ctx-v${String(count).padStart(3, '0')}`;
42
+ }
43
+
44
+ /**
45
+ * Get current git commit hash
46
+ * @returns {string|null} Git commit hash or null
47
+ */
48
+ getGitCommit() {
49
+ try {
50
+ return execSync('git rev-parse --short HEAD', {
51
+ cwd: this.projectRoot,
52
+ encoding: 'utf-8',
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ }).trim();
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Save a context version
62
+ * @param {object} context - Context data from analyzer
63
+ * @param {object} config - Configuration used for this context
64
+ * @param {string} message - Optional message for the version
65
+ * @returns {object} Saved version info
66
+ */
67
+ async save(context, config = {}, message = '') {
68
+ await this.init();
69
+
70
+ const index = await this.loadIndex();
71
+ const id = this.generateVersionId(index);
72
+ const timestamp = new Date().toISOString();
73
+ const gitCommit = this.getGitCommit();
74
+
75
+ // Build version object
76
+ const version = {
77
+ id,
78
+ timestamp,
79
+ gitCommit,
80
+ message,
81
+ config: {
82
+ exclude: config.exclude || [],
83
+ include: config.include || [],
84
+ targetModel: config.targetModel || null,
85
+ },
86
+ summary: {
87
+ totalFiles: context.totalFiles || 0,
88
+ totalTokens: context.totalTokens || 0,
89
+ totalBytes: context.totalBytes || 0,
90
+ totalLines: context.totalLines || 0,
91
+ },
92
+ files: (context.largestFiles || []).map((f) => ({
93
+ path: f.relativePath || f.path,
94
+ tokens: f.tokens || 0,
95
+ lines: f.lines || 0,
96
+ extension: f.extension || '',
97
+ included: f.included !== false,
98
+ hash: this.hashFile(f),
99
+ })),
100
+ byExtension: context.byExtension || {},
101
+ byDirectory: context.byDirectory || {},
102
+ };
103
+
104
+ // Save version file
105
+ const versionFile = path.join(this.versionDir, `${id}.json`);
106
+ await fs.promises.writeFile(versionFile, JSON.stringify(version, null, 2));
107
+
108
+ // Update index
109
+ index.versions.push({
110
+ id,
111
+ timestamp,
112
+ gitCommit,
113
+ message,
114
+ totalFiles: version.summary.totalFiles,
115
+ totalTokens: version.summary.totalTokens,
116
+ targetModel: version.config.targetModel,
117
+ });
118
+
119
+ await this.saveIndex(index);
120
+
121
+ return { id, version };
122
+ }
123
+
124
+ /**
125
+ * Generate a hash for a file entry
126
+ * @param {object} file - File info
127
+ * @returns {string} Hash string
128
+ */
129
+ hashFile(file) {
130
+ const content = `${file.path}:${file.tokens}:${file.lines}`;
131
+ return crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
132
+ }
133
+
134
+ /**
135
+ * Load the version index
136
+ * @returns {object} Index object
137
+ */
138
+ async loadIndex() {
139
+ try {
140
+ const content = await fs.promises.readFile(this.indexFile, 'utf-8');
141
+ return JSON.parse(content);
142
+ } catch {
143
+ return { versions: [] };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Save the version index
149
+ * @param {object} index - Index object
150
+ */
151
+ async saveIndex(index) {
152
+ await fs.promises.writeFile(this.indexFile, JSON.stringify(index, null, 2));
153
+ }
154
+
155
+ /**
156
+ * List all version IDs
157
+ * @param {object} options - List options
158
+ * @returns {object[]} Array of version info
159
+ */
160
+ async list(options = {}) {
161
+ const index = await this.loadIndex();
162
+ let versions = [...index.versions].reverse(); // Most recent first
163
+
164
+ if (options.limit) {
165
+ versions = versions.slice(0, options.limit);
166
+ }
167
+
168
+ return versions;
169
+ }
170
+
171
+ /**
172
+ * Load a version by ID
173
+ * @param {string} id - Version ID
174
+ * @returns {object|null} Version data or null
175
+ */
176
+ async load(id) {
177
+ const versionFile = path.join(this.versionDir, `${id}.json`);
178
+ try {
179
+ const content = await fs.promises.readFile(versionFile, 'utf-8');
180
+ return JSON.parse(content);
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Delete a version
188
+ * @param {string} id - Version ID
189
+ * @returns {boolean} True if deleted
190
+ */
191
+ async delete(id) {
192
+ const versionFile = path.join(this.versionDir, `${id}.json`);
193
+ try {
194
+ await fs.promises.unlink(versionFile);
195
+
196
+ // Update index
197
+ const index = await this.loadIndex();
198
+ index.versions = index.versions.filter((v) => v.id !== id);
199
+ await this.saveIndex(index);
200
+
201
+ return true;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Get version count
209
+ * @returns {number} Number of versions
210
+ */
211
+ async count() {
212
+ const index = await this.loadIndex();
213
+ return index.versions.length;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * VersionDiffEngine - Compare versions
219
+ */
220
+ export class VersionDiffEngine {
221
+ /**
222
+ * Compare two versions
223
+ * @param {object} version1 - Earlier version
224
+ * @param {object} version2 - Later version
225
+ * @returns {object} Comparison result
226
+ */
227
+ compare(version1, version2) {
228
+ const files1 = new Map((version1.files || []).map((f) => [f.path, f]));
229
+ const files2 = new Map((version2.files || []).map((f) => [f.path, f]));
230
+
231
+ const added = [];
232
+ const removed = [];
233
+ const changed = [];
234
+ const unchanged = [];
235
+
236
+ // Find added and changed files
237
+ for (const [filePath, file2] of files2) {
238
+ if (!files1.has(filePath)) {
239
+ added.push({
240
+ path: filePath,
241
+ tokens: file2.tokens,
242
+ });
243
+ } else {
244
+ const file1 = files1.get(filePath);
245
+ if (file1.hash !== file2.hash || file1.tokens !== file2.tokens) {
246
+ changed.push({
247
+ path: filePath,
248
+ oldTokens: file1.tokens,
249
+ newTokens: file2.tokens,
250
+ tokenDiff: file2.tokens - file1.tokens,
251
+ });
252
+ } else {
253
+ unchanged.push(filePath);
254
+ }
255
+ }
256
+ }
257
+
258
+ // Find removed files
259
+ for (const [filePath, file1] of files1) {
260
+ if (!files2.has(filePath)) {
261
+ removed.push({
262
+ path: filePath,
263
+ tokens: file1.tokens,
264
+ });
265
+ }
266
+ }
267
+
268
+ // Calculate token diff
269
+ const tokenDiff = (version2.summary?.totalTokens || 0) - (version1.summary?.totalTokens || 0);
270
+ const percentChange =
271
+ (version1.summary?.totalTokens || 0) > 0
272
+ ? (tokenDiff / version1.summary.totalTokens) * 100
273
+ : 0;
274
+
275
+ return {
276
+ version1: {
277
+ id: version1.id,
278
+ timestamp: version1.timestamp,
279
+ gitCommit: version1.gitCommit,
280
+ summary: version1.summary,
281
+ },
282
+ version2: {
283
+ id: version2.id,
284
+ timestamp: version2.timestamp,
285
+ gitCommit: version2.gitCommit,
286
+ summary: version2.summary,
287
+ },
288
+ tokenDiff,
289
+ percentChange,
290
+ added,
291
+ removed,
292
+ changed,
293
+ unchanged,
294
+ summary: {
295
+ filesAdded: added.length,
296
+ filesRemoved: removed.length,
297
+ filesChanged: changed.length,
298
+ filesUnchanged: unchanged.length,
299
+ },
300
+ };
301
+ }
302
+ }
303
+
304
+ /**
305
+ * ContextVersioning - Main class for version management
306
+ */
307
+ export default class ContextVersioning {
308
+ constructor(projectRoot) {
309
+ this.projectRoot = projectRoot;
310
+ this.storage = new VersionStorage(projectRoot);
311
+ this.diffEngine = new VersionDiffEngine();
312
+ }
313
+
314
+ /**
315
+ * Create a new version (automatically called with --cli)
316
+ * @param {object} context - Context data from analyzer
317
+ * @param {object} config - Configuration used
318
+ * @param {string} message - Optional message
319
+ * @returns {object} Created version info
320
+ */
321
+ async createVersion(context, config = {}, message = '') {
322
+ return await this.storage.save(context, config, message);
323
+ }
324
+
325
+ /**
326
+ * List all versions with formatted output
327
+ * @returns {string} Formatted version list
328
+ */
329
+ async listVersions() {
330
+ const versions = await this.storage.list();
331
+
332
+ if (versions.length === 0) {
333
+ return '\n📜 No context versions found.\n\n Create one with: ctxman --cli\n\n';
334
+ }
335
+
336
+ let output = '\n📜 Context Versions:\n';
337
+ output += '═'.repeat(60) + '\n\n';
338
+
339
+ for (const v of versions) {
340
+ const date = new Date(v.timestamp);
341
+ const dateStr = date.toISOString().split('T')[0];
342
+ const timeStr = date.toTimeString().split(' ')[0].substring(0, 5);
343
+ const tokens = (v.totalTokens || 0).toLocaleString();
344
+ const files = v.totalFiles || 0;
345
+ const model = v.targetModel || 'N/A';
346
+
347
+ output += ` Version: ${v.id}\n`;
348
+ output += ` Created: ${dateStr} ${timeStr}\n`;
349
+ output += ` Files: ${files.toLocaleString()} | Tokens: ${tokens}\n`;
350
+ output += ` Model: ${model}\n`;
351
+ if (v.gitCommit) {
352
+ output += ` Git: ${v.gitCommit}\n`;
353
+ }
354
+ if (v.message) {
355
+ output += ` Message: ${v.message}\n`;
356
+ }
357
+ output += '\n';
358
+ }
359
+
360
+ return output;
361
+ }
362
+
363
+ /**
364
+ * Get versions for programmatic use
365
+ * @returns {object[]} Array of version info
366
+ */
367
+ async getVersions() {
368
+ return await this.storage.list();
369
+ }
370
+
371
+ /**
372
+ * Restore a version (return the context data)
373
+ * @param {string} id - Version ID
374
+ * @returns {object|null} Version data or null
375
+ */
376
+ async restoreVersion(id) {
377
+ const version = await this.storage.load(id);
378
+
379
+ if (!version) {
380
+ return null;
381
+ }
382
+
383
+ return {
384
+ ...version,
385
+ restoredFrom: id,
386
+ restoredAt: new Date().toISOString(),
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Compare two versions
392
+ * @param {string} id1 - First version ID
393
+ * @param {string} id2 - Second version ID
394
+ * @returns {object|null} Comparison result
395
+ */
396
+ async compareVersions(id1, id2) {
397
+ const v1 = await this.storage.load(id1);
398
+ const v2 = await this.storage.load(id2);
399
+
400
+ if (!v1 || !v2) {
401
+ return null;
402
+ }
403
+
404
+ return this.diffEngine.compare(v1, v2);
405
+ }
406
+
407
+ /**
408
+ * Format version comparison for display
409
+ * @param {object} comparison - Comparison result
410
+ * @returns {string} Formatted output
411
+ */
412
+ formatComparison(comparison) {
413
+ if (!comparison) {
414
+ return '\n⚠️ Versions not found for comparison.\n\n';
415
+ }
416
+
417
+ let output = '\n📊 Version Comparison\n';
418
+ output += '═'.repeat(60) + '\n\n';
419
+
420
+ output += ` From: ${comparison.version1.id}`;
421
+ if (comparison.version1.gitCommit) {
422
+ output += ` (${comparison.version1.gitCommit})`;
423
+ }
424
+ output += '\n';
425
+
426
+ output += ` To: ${comparison.version2.id}`;
427
+ if (comparison.version2.gitCommit) {
428
+ output += ` (${comparison.version2.gitCommit})`;
429
+ }
430
+ output += '\n\n';
431
+
432
+ // Token changes
433
+ const tokenDiff = comparison.tokenDiff;
434
+ const percentChange = comparison.percentChange;
435
+ const sign = tokenDiff >= 0 ? '+' : '';
436
+
437
+ output += ` Token change: ${sign}${tokenDiff.toLocaleString()} (${sign}${percentChange.toFixed(1)}%)\n\n`;
438
+
439
+ // File changes
440
+ output += ` Files added: ${comparison.summary.filesAdded}\n`;
441
+ output += ` Files removed: ${comparison.summary.filesRemoved}\n`;
442
+ output += ` Files changed: ${comparison.summary.filesChanged}\n\n`;
443
+
444
+ // Added files
445
+ if (comparison.added.length > 0) {
446
+ output += ' 📁 Files Added:\n';
447
+ for (const f of comparison.added.slice(0, 10)) {
448
+ output += ` + ${f.path} (+${f.tokens.toLocaleString()} tokens)\n`;
449
+ }
450
+ if (comparison.added.length > 10) {
451
+ output += ` ... and ${comparison.added.length - 10} more\n`;
452
+ }
453
+ output += '\n';
454
+ }
455
+
456
+ // Removed files
457
+ if (comparison.removed.length > 0) {
458
+ output += ' 🗑️ Files Removed:\n';
459
+ for (const f of comparison.removed.slice(0, 10)) {
460
+ output += ` - ${f.path} (-${f.tokens.toLocaleString()} tokens)\n`;
461
+ }
462
+ if (comparison.removed.length > 10) {
463
+ output += ` ... and ${comparison.removed.length - 10} more\n`;
464
+ }
465
+ output += '\n';
466
+ }
467
+
468
+ // Changed files
469
+ if (comparison.changed.length > 0) {
470
+ output += ' ✏️ Files Changed:\n';
471
+ const sorted = [...comparison.changed].sort(
472
+ (a, b) => Math.abs(b.tokenDiff) - Math.abs(a.tokenDiff)
473
+ );
474
+ for (const f of sorted.slice(0, 10)) {
475
+ const changeSign = f.tokenDiff >= 0 ? '+' : '';
476
+ output += ` ~ ${f.path} (${changeSign}${f.tokenDiff.toLocaleString()} tokens)\n`;
477
+ }
478
+ if (comparison.changed.length > 10) {
479
+ output += ` ... and ${comparison.changed.length - 10} more\n`;
480
+ }
481
+ output += '\n';
482
+ }
483
+
484
+ return output;
485
+ }
486
+
487
+ /**
488
+ * Delete a version
489
+ * @param {string} id - Version ID
490
+ * @returns {boolean} True if deleted
491
+ */
492
+ async deleteVersion(id) {
493
+ return await this.storage.delete(id);
494
+ }
495
+
496
+ /**
497
+ * Get the latest version
498
+ * @returns {object|null} Latest version info or null
499
+ */
500
+ async getLatestVersion() {
501
+ const versions = await this.storage.list({ limit: 1 });
502
+ if (versions.length === 0) {
503
+ return null;
504
+ }
505
+ return await this.storage.load(versions[0].id);
506
+ }
507
+
508
+ /**
509
+ * Prune old versions, keeping only the most recent N
510
+ * @param {number} keepCount - Number of versions to keep
511
+ * @returns {string[]} Array of deleted version IDs
512
+ */
513
+ async pruneVersions(keepCount = 10) {
514
+ const versions = await this.storage.list();
515
+
516
+ if (versions.length <= keepCount) {
517
+ return [];
518
+ }
519
+
520
+ // Get versions to delete (oldest ones)
521
+ const toDelete = versions.slice(keepCount);
522
+ const deleted = [];
523
+
524
+ for (const v of toDelete) {
525
+ if (await this.storage.delete(v.id)) {
526
+ deleted.push(v.id);
527
+ }
528
+ }
529
+
530
+ return deleted;
531
+ }
532
+ }