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.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +683 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +675 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +113 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/sources/FileDataSource.js +6 -6
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +243 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +255 -0
- package/lib/wizards/project-detector.js +571 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- 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
|
+
}
|