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
package/lib/core/Analyzer.js
CHANGED
|
@@ -17,7 +17,7 @@ export class Analyzer {
|
|
|
17
17
|
methodLevel: false,
|
|
18
18
|
parallel: false,
|
|
19
19
|
maxWorkers: 4,
|
|
20
|
-
...options
|
|
20
|
+
...options,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
this.methodAnalyzer = new MethodAnalyzer();
|
|
@@ -30,9 +30,10 @@ export class Analyzer {
|
|
|
30
30
|
totalTokens: 0,
|
|
31
31
|
totalSize: 0,
|
|
32
32
|
totalMethods: 0,
|
|
33
|
+
totalLines: 0,
|
|
33
34
|
byLanguage: {},
|
|
34
35
|
largestFiles: [],
|
|
35
|
-
analysisTime: 0
|
|
36
|
+
analysisTime: 0,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -49,7 +50,7 @@ export class Analyzer {
|
|
|
49
50
|
|
|
50
51
|
return {
|
|
51
52
|
files: results,
|
|
52
|
-
stats: this.getStats()
|
|
53
|
+
stats: this.getStats(),
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -83,19 +84,23 @@ export class Analyzer {
|
|
|
83
84
|
name: fileInfo.name,
|
|
84
85
|
extension: fileInfo.extension,
|
|
85
86
|
size: fileInfo.size,
|
|
87
|
+
modified: fileInfo.modified,
|
|
86
88
|
tokens: tokens,
|
|
87
89
|
lines: content.split('\n').length,
|
|
88
|
-
language: this.detectLanguage(fileInfo.extension)
|
|
90
|
+
language: this.detectLanguage(fileInfo.extension),
|
|
89
91
|
};
|
|
90
92
|
|
|
91
93
|
if (this.options.methodLevel) {
|
|
92
94
|
const methods = this.methodAnalyzer.extractMethods(content, fileInfo.path);
|
|
93
|
-
|
|
95
|
+
const sources = this.methodAnalyzer.extractMethodSources(content, methods, fileInfo.path);
|
|
96
|
+
analysis.methods = methods.map((method, i) => ({
|
|
97
|
+
...method,
|
|
98
|
+
tokens: TokenUtils.calculate(sources[i], fileInfo.path),
|
|
99
|
+
}));
|
|
94
100
|
analysis.methodCount = methods.length;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
return analysis;
|
|
98
|
-
|
|
99
104
|
} catch (error) {
|
|
100
105
|
logger.error(`Error analyzing file ${fileInfo.relativePath}: ${error.message}`);
|
|
101
106
|
return null;
|
|
@@ -106,12 +111,13 @@ export class Analyzer {
|
|
|
106
111
|
this.stats.totalFiles++;
|
|
107
112
|
this.stats.totalTokens += analysis.tokens;
|
|
108
113
|
this.stats.totalSize += analysis.size;
|
|
114
|
+
this.stats.totalLines += analysis.lines || 0;
|
|
109
115
|
|
|
110
116
|
if (!this.stats.byLanguage[analysis.language]) {
|
|
111
117
|
this.stats.byLanguage[analysis.language] = {
|
|
112
118
|
files: 0,
|
|
113
119
|
tokens: 0,
|
|
114
|
-
size: 0
|
|
120
|
+
size: 0,
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -122,7 +128,7 @@ export class Analyzer {
|
|
|
122
128
|
this.stats.largestFiles.push({
|
|
123
129
|
path: analysis.relativePath,
|
|
124
130
|
tokens: analysis.tokens,
|
|
125
|
-
size: analysis.size
|
|
131
|
+
size: analysis.size,
|
|
126
132
|
});
|
|
127
133
|
|
|
128
134
|
this.stats.largestFiles.sort((a, b) => b.tokens - a.tokens);
|
|
@@ -152,7 +158,7 @@ export class Analyzer {
|
|
|
152
158
|
'.cpp': 'C++',
|
|
153
159
|
'.h': 'C/C++',
|
|
154
160
|
'.hpp': 'C++',
|
|
155
|
-
'.scala': 'Scala'
|
|
161
|
+
'.scala': 'Scala',
|
|
156
162
|
};
|
|
157
163
|
|
|
158
164
|
return languageMap[extension] || 'Other';
|
|
@@ -171,7 +177,7 @@ export class Analyzer {
|
|
|
171
177
|
.map(([language, stats]) => ({
|
|
172
178
|
language,
|
|
173
179
|
...stats,
|
|
174
|
-
percentage: (stats.tokens / this.stats.totalTokens) * 100
|
|
180
|
+
percentage: (stats.tokens / this.stats.totalTokens) * 100,
|
|
175
181
|
}))
|
|
176
182
|
.sort((a, b) => b.tokens - a.tokens);
|
|
177
183
|
}
|
|
@@ -24,7 +24,7 @@ export class ContextBuilder {
|
|
|
24
24
|
includeTests: true,
|
|
25
25
|
includeDocumentation: true,
|
|
26
26
|
priorityStrategy: 'balanced', // balanced, changed-first, core-first
|
|
27
|
-
...options
|
|
27
|
+
...options,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -49,7 +49,7 @@ export class ContextBuilder {
|
|
|
49
49
|
metadata: this.buildMetadata(stats),
|
|
50
50
|
files: this.buildFileList(filteredFiles),
|
|
51
51
|
methods: this.options.methodLevel ? this.buildMethodList(filteredFiles) : null,
|
|
52
|
-
statistics: stats
|
|
52
|
+
statistics: stats,
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
// Optimize for target LLM if specified
|
|
@@ -76,8 +76,8 @@ export class ContextBuilder {
|
|
|
76
76
|
configuration: {
|
|
77
77
|
includeTests: this.options.includeTests,
|
|
78
78
|
includeDocumentation: this.options.includeDocumentation,
|
|
79
|
-
methodLevel: this.options.methodLevel
|
|
80
|
-
}
|
|
79
|
+
methodLevel: this.options.methodLevel,
|
|
80
|
+
},
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -103,7 +103,7 @@ export class ContextBuilder {
|
|
|
103
103
|
path: file.relativePath,
|
|
104
104
|
tokens: file.tokens,
|
|
105
105
|
size: file.size,
|
|
106
|
-
language: file.language
|
|
106
|
+
language: file.language,
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -120,11 +120,11 @@ export class ContextBuilder {
|
|
|
120
120
|
|
|
121
121
|
for (const file of files) {
|
|
122
122
|
if (file.methods && file.methods.length > 0) {
|
|
123
|
-
methods[file.relativePath] = file.methods.map(method => ({
|
|
123
|
+
methods[file.relativePath] = file.methods.map((method) => ({
|
|
124
124
|
name: method.name,
|
|
125
125
|
line: method.line,
|
|
126
126
|
tokens: method.tokens || 0,
|
|
127
|
-
type: method.type || 'function'
|
|
127
|
+
type: method.type || 'function',
|
|
128
128
|
}));
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -149,26 +149,15 @@ export class ContextBuilder {
|
|
|
149
149
|
return files;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Select files until token budget reached
|
|
152
|
+
// Pack by priority. A file that does not fit is skipped whole (nothing truncates file
|
|
153
|
+
// content), and smaller files after it can still use the remaining budget.
|
|
156
154
|
const selected = [];
|
|
157
155
|
let runningTotal = 0;
|
|
158
156
|
|
|
159
|
-
for (const file of
|
|
157
|
+
for (const file of this.prioritizeFiles(files)) {
|
|
160
158
|
if (runningTotal + file.tokens <= targetTokens) {
|
|
161
159
|
selected.push(file);
|
|
162
160
|
runningTotal += file.tokens;
|
|
163
|
-
} else {
|
|
164
|
-
// Check if we can include part of the file
|
|
165
|
-
const remainingBudget = targetTokens - runningTotal;
|
|
166
|
-
if (remainingBudget > 1000) { // Minimum 1k tokens
|
|
167
|
-
logger.info(`Partial file inclusion: ${file.relativePath} (${remainingBudget} tokens)`);
|
|
168
|
-
// Note: Actual implementation would truncate file content
|
|
169
|
-
selected.push(file);
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
161
|
}
|
|
173
162
|
}
|
|
174
163
|
|
|
@@ -183,11 +172,13 @@ export class ContextBuilder {
|
|
|
183
172
|
*/
|
|
184
173
|
prioritizeFiles(files) {
|
|
185
174
|
const strategy = this.options.priorityStrategy;
|
|
175
|
+
// Sort a copy: the caller's analysis result keeps its order
|
|
176
|
+
const sorted = [...files];
|
|
186
177
|
|
|
187
178
|
switch (strategy) {
|
|
188
179
|
case 'changed-first':
|
|
189
|
-
//
|
|
190
|
-
return
|
|
180
|
+
// Most recently modified files first (mtime from the scanner)
|
|
181
|
+
return sorted.sort((a, b) => {
|
|
191
182
|
const aModified = a.modified || 0;
|
|
192
183
|
const bModified = b.modified || 0;
|
|
193
184
|
return bModified - aModified;
|
|
@@ -195,7 +186,7 @@ export class ContextBuilder {
|
|
|
195
186
|
|
|
196
187
|
case 'core-first':
|
|
197
188
|
// Prioritize core application files (heuristic-based)
|
|
198
|
-
return
|
|
189
|
+
return sorted.sort((a, b) => {
|
|
199
190
|
const aCoreScore = this.calculateCoreScore(a);
|
|
200
191
|
const bCoreScore = this.calculateCoreScore(b);
|
|
201
192
|
return bCoreScore - aCoreScore;
|
|
@@ -204,7 +195,7 @@ export class ContextBuilder {
|
|
|
204
195
|
case 'balanced':
|
|
205
196
|
default:
|
|
206
197
|
// Balance between size and importance
|
|
207
|
-
return
|
|
198
|
+
return sorted.sort((a, b) => {
|
|
208
199
|
const aScore = this.calculateBalancedScore(a);
|
|
209
200
|
const bScore = this.calculateBalancedScore(b);
|
|
210
201
|
return bScore - aScore;
|
|
@@ -277,7 +268,7 @@ export class ContextBuilder {
|
|
|
277
268
|
name: profile.name,
|
|
278
269
|
vendor: profile.vendor,
|
|
279
270
|
contextWindow: profile.contextWindow,
|
|
280
|
-
recommendedInput: profile.maxRecommendedInput
|
|
271
|
+
recommendedInput: profile.maxRecommendedInput,
|
|
281
272
|
};
|
|
282
273
|
|
|
283
274
|
return optimized;
|
|
@@ -292,7 +283,7 @@ export class ContextBuilder {
|
|
|
292
283
|
const compact = {};
|
|
293
284
|
|
|
294
285
|
for (const [dir, files] of Object.entries(context.files)) {
|
|
295
|
-
const fileNames = files.map(f => f.name);
|
|
286
|
+
const fileNames = files.map((f) => f.name);
|
|
296
287
|
compact[dir] = fileNames;
|
|
297
288
|
}
|
|
298
289
|
|
|
@@ -306,7 +297,7 @@ export class ContextBuilder {
|
|
|
306
297
|
*/
|
|
307
298
|
getSummary(context) {
|
|
308
299
|
const meta = context.metadata;
|
|
309
|
-
const
|
|
300
|
+
const _stats = context.statistics;
|
|
310
301
|
|
|
311
302
|
let summary = `Context Summary:\n`;
|
|
312
303
|
summary += ` Files: ${meta.totalFiles}\n`;
|
package/lib/core/Reporter.js
CHANGED
|
@@ -24,7 +24,7 @@ export class Reporter {
|
|
|
24
24
|
outputPath: null,
|
|
25
25
|
clipboard: false,
|
|
26
26
|
verbose: true,
|
|
27
|
-
...options
|
|
27
|
+
...options,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
this.formatRegistry = new FormatRegistry();
|
|
@@ -79,7 +79,7 @@ export class Reporter {
|
|
|
79
79
|
.slice(0, 5);
|
|
80
80
|
|
|
81
81
|
for (const [lang, langStats] of languages) {
|
|
82
|
-
const percentage = (langStats.tokens / stats.totalTokens * 100).toFixed(1);
|
|
82
|
+
const percentage = ((langStats.tokens / stats.totalTokens) * 100).toFixed(1);
|
|
83
83
|
const bar = '▓'.repeat(Math.round(percentage / 2));
|
|
84
84
|
console.log(` ${lang.padEnd(15)} ${percentage.padStart(5)}% ${bar}`);
|
|
85
85
|
}
|
|
@@ -91,8 +91,10 @@ export class Reporter {
|
|
|
91
91
|
console.log('🏆 LARGEST FILES (by tokens):');
|
|
92
92
|
for (let i = 0; i < Math.min(5, stats.largestFiles.length); i++) {
|
|
93
93
|
const file = stats.largestFiles[i];
|
|
94
|
-
const percentage = (file.tokens / stats.totalTokens * 100).toFixed(1);
|
|
95
|
-
console.log(
|
|
94
|
+
const percentage = ((file.tokens / stats.totalTokens) * 100).toFixed(1);
|
|
95
|
+
console.log(
|
|
96
|
+
` ${i + 1}. ${file.path.padEnd(40)} ${file.tokens.toLocaleString().padStart(8)} tokens (${percentage}%)`
|
|
97
|
+
);
|
|
96
98
|
}
|
|
97
99
|
console.log();
|
|
98
100
|
}
|
|
@@ -114,7 +116,7 @@ export class Reporter {
|
|
|
114
116
|
console.log(` Analysis Time: ${stats.analysisTime}ms`);
|
|
115
117
|
if (stats.cacheHits !== undefined) {
|
|
116
118
|
const totalCache = stats.cacheHits + stats.cacheMisses;
|
|
117
|
-
const hitRate = totalCache > 0 ? (stats.cacheHits / totalCache * 100).toFixed(1) : 0;
|
|
119
|
+
const hitRate = totalCache > 0 ? ((stats.cacheHits / totalCache) * 100).toFixed(1) : 0;
|
|
118
120
|
console.log(` Cache Hit Rate: ${hitRate}% (${stats.cacheHits}/${totalCache})`);
|
|
119
121
|
}
|
|
120
122
|
console.log();
|
|
@@ -129,7 +131,10 @@ export class Reporter {
|
|
|
129
131
|
async exportToFile(context, outputPath) {
|
|
130
132
|
try {
|
|
131
133
|
const format = this.detectFormatFromPath(outputPath);
|
|
132
|
-
const content =
|
|
134
|
+
const content =
|
|
135
|
+
format === 'summary'
|
|
136
|
+
? this.generateSummary(context)
|
|
137
|
+
: this.formatRegistry.encode(format, context);
|
|
133
138
|
|
|
134
139
|
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
135
140
|
|
|
@@ -139,7 +144,6 @@ export class Reporter {
|
|
|
139
144
|
console.log(`💾 Report saved: ${outputPath}`);
|
|
140
145
|
console.log(` Format: ${format.toUpperCase()}`);
|
|
141
146
|
console.log(` Size: ${size} KB`);
|
|
142
|
-
|
|
143
147
|
} catch (error) {
|
|
144
148
|
logger.error(`Failed to export to file: ${error.message}`);
|
|
145
149
|
throw error;
|
|
@@ -169,7 +173,6 @@ export class Reporter {
|
|
|
169
173
|
const fallbackPath = path.join(process.cwd(), `context-${Date.now()}.${format}`);
|
|
170
174
|
await this.exportToFile(context, fallbackPath);
|
|
171
175
|
}
|
|
172
|
-
|
|
173
176
|
} catch (error) {
|
|
174
177
|
logger.error(`Failed to export to clipboard: ${error.message}`);
|
|
175
178
|
throw error;
|
|
@@ -192,7 +195,8 @@ export class Reporter {
|
|
|
192
195
|
'.md': 'markdown',
|
|
193
196
|
'.csv': 'csv',
|
|
194
197
|
'.xml': 'xml',
|
|
195
|
-
|
|
198
|
+
// A GitIngest digest needs file contents, which a Context does not carry
|
|
199
|
+
'.txt': 'summary',
|
|
196
200
|
};
|
|
197
201
|
|
|
198
202
|
return formatMap[ext] || this.options.format || 'toon';
|
|
@@ -216,7 +220,7 @@ export class Reporter {
|
|
|
216
220
|
*/
|
|
217
221
|
generateSummary(context) {
|
|
218
222
|
const meta = context.metadata;
|
|
219
|
-
const
|
|
223
|
+
const _stats = context.statistics;
|
|
220
224
|
|
|
221
225
|
let summary = 'Ctxman Analysis Summary\n';
|
|
222
226
|
summary += '='.repeat(50) + '\n\n';
|
|
@@ -250,10 +254,7 @@ export class Reporter {
|
|
|
250
254
|
const results = [];
|
|
251
255
|
|
|
252
256
|
for (const format of formats) {
|
|
253
|
-
const outputPath = path.join(
|
|
254
|
-
process.cwd(),
|
|
255
|
-
`context-${Date.now()}.${format}`
|
|
256
|
-
);
|
|
257
|
+
const outputPath = path.join(process.cwd(), `context-${Date.now()}.${format}`);
|
|
257
258
|
|
|
258
259
|
try {
|
|
259
260
|
await this.exportToFile(context, outputPath);
|
package/lib/core/Scanner.js
CHANGED
|
@@ -8,17 +8,29 @@ import path from 'path';
|
|
|
8
8
|
import GitIgnoreParser from '../parsers/gitignore-parser.js';
|
|
9
9
|
import FileUtils from '../utils/file-utils.js';
|
|
10
10
|
import { getLogger } from '../utils/logger.js';
|
|
11
|
+
import { loadPolicy } from '../policy/policy.js';
|
|
12
|
+
import { loadIgnoreFileRules, resolveIgnoreFileNames } from '../policy/ignore-files.js';
|
|
11
13
|
|
|
12
14
|
const logger = getLogger('Scanner');
|
|
13
15
|
|
|
14
16
|
export class Scanner {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} rootPath - Project root
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {import('../policy/policy.js').Policy|false|null} [options.policy] - Export policy
|
|
21
|
+
* whose deny rules drop files; loaded from the project (.ctxman/policy.json and
|
|
22
|
+
* CTXMAN_POLICY) when not given, and turned off with false
|
|
23
|
+
* @param {string|string[]} [options.respectIgnoreFiles] - AI-tool ignore files to honour
|
|
24
|
+
* ("all", ".cursorignore,.aiexclude" or an array), in addition to the policy's list
|
|
25
|
+
* @param {object} [options.env=process.env] - Environment for loading the policy
|
|
26
|
+
*/
|
|
15
27
|
constructor(rootPath, options = {}) {
|
|
16
28
|
this.rootPath = rootPath;
|
|
17
29
|
this.options = {
|
|
18
30
|
respectGitignore: true,
|
|
19
31
|
followSymlinks: false,
|
|
20
32
|
maxDepth: Infinity,
|
|
21
|
-
...options
|
|
33
|
+
...options,
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
// Initialize GitIgnoreParser with correct file paths
|
|
@@ -31,14 +43,23 @@ export class Scanner {
|
|
|
31
43
|
filesScanned: 0,
|
|
32
44
|
directoriesTraversed: 0,
|
|
33
45
|
filesIgnored: 0,
|
|
34
|
-
|
|
46
|
+
filesDenied: 0,
|
|
47
|
+
errors: 0,
|
|
35
48
|
};
|
|
49
|
+
// Loaded on first use, so constructing a Scanner reads no policy files
|
|
50
|
+
this._policy = undefined;
|
|
51
|
+
this._ignoreFileRules = undefined;
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
scan() {
|
|
39
55
|
logger.info(`Starting scan: ${this.rootPath}`);
|
|
40
56
|
const startTime = Date.now();
|
|
41
57
|
|
|
58
|
+
// Load here, outside scanDirectory's error handling: an invalid policy must fail the
|
|
59
|
+
// scan instead of yielding an empty file list
|
|
60
|
+
this.getPolicy();
|
|
61
|
+
this.getIgnoreFileRules();
|
|
62
|
+
|
|
42
63
|
const files = this.scanDirectory(this.rootPath, 0);
|
|
43
64
|
|
|
44
65
|
const elapsed = Date.now() - startTime;
|
|
@@ -62,25 +83,32 @@ export class Scanner {
|
|
|
62
83
|
const fullPath = path.join(dirPath, entry.name);
|
|
63
84
|
const relativePath = path.relative(this.rootPath, fullPath);
|
|
64
85
|
|
|
65
|
-
if (this.shouldIgnore(relativePath)) {
|
|
86
|
+
if (this.shouldIgnore(relativePath, fullPath)) {
|
|
66
87
|
this.stats.filesIgnored++;
|
|
67
88
|
continue;
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
if (entry.isDirectory()) {
|
|
92
|
+
if (FileUtils.isSkippedDirectory(entry.name)) continue;
|
|
71
93
|
const subFiles = this.scanDirectory(fullPath, depth + 1);
|
|
72
94
|
files.push(...subFiles);
|
|
73
95
|
} else if (entry.isFile()) {
|
|
74
96
|
// Check if it's a text file (not binary)
|
|
75
97
|
try {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.
|
|
98
|
+
if (this.getPolicy()?.isAuditLog(relativePath)) {
|
|
99
|
+
this.stats.filesIgnored++;
|
|
100
|
+
} else if (FileUtils.isText(fullPath)) {
|
|
101
|
+
if (this.getPolicy()?.isDenied(relativePath)) {
|
|
102
|
+
this.stats.filesDenied++;
|
|
103
|
+
} else {
|
|
104
|
+
const fileInfo = this.getFileInfo(fullPath, relativePath);
|
|
105
|
+
files.push(fileInfo);
|
|
106
|
+
this.stats.filesScanned++;
|
|
107
|
+
}
|
|
80
108
|
} else {
|
|
81
109
|
this.stats.filesIgnored++;
|
|
82
110
|
}
|
|
83
|
-
} catch (
|
|
111
|
+
} catch (_error) {
|
|
84
112
|
// Skip files that can't be processed
|
|
85
113
|
this.stats.filesIgnored++;
|
|
86
114
|
}
|
|
@@ -104,12 +132,52 @@ export class Scanner {
|
|
|
104
132
|
extension: path.extname(fullPath),
|
|
105
133
|
size: stats.size,
|
|
106
134
|
modified: stats.mtime,
|
|
107
|
-
created: stats.birthtime
|
|
135
|
+
created: stats.birthtime,
|
|
108
136
|
};
|
|
109
137
|
}
|
|
110
138
|
|
|
111
|
-
|
|
112
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Whether the export policy denies a project-relative path
|
|
141
|
+
* @param {string} relativePath
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
isDenied(relativePath) {
|
|
145
|
+
return Boolean(this.getPolicy()?.isDenied(relativePath));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
shouldIgnore(relativePath, fullPath = path.join(this.rootPath, relativePath)) {
|
|
149
|
+
if (this.gitIgnore.isIgnored(fullPath, relativePath)) return true;
|
|
150
|
+
return Boolean(this.getIgnoreFileRules()?.isIgnored(relativePath));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Effective export policy, or null when none applies
|
|
155
|
+
* @returns {import('../policy/policy.js').Policy|null}
|
|
156
|
+
*/
|
|
157
|
+
getPolicy() {
|
|
158
|
+
if (this._policy === undefined) {
|
|
159
|
+
const { policy, env } = this.options;
|
|
160
|
+
const loaded = policy === undefined ? loadPolicy(this.rootPath, { env }) : policy;
|
|
161
|
+
this._policy = loaded?.active ? loaded : null;
|
|
162
|
+
}
|
|
163
|
+
return this._policy;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* AI-tool ignore files from options.respectIgnoreFiles and the policy's ignoreFiles
|
|
168
|
+
* @returns {import('../policy/ignore-files.js').IgnoreFileRules|null}
|
|
169
|
+
*/
|
|
170
|
+
getIgnoreFileRules() {
|
|
171
|
+
if (this._ignoreFileRules === undefined) {
|
|
172
|
+
const names = [
|
|
173
|
+
...new Set([
|
|
174
|
+
...resolveIgnoreFileNames(this.options.respectIgnoreFiles),
|
|
175
|
+
...(this.getPolicy()?.ignoreFiles ?? []),
|
|
176
|
+
]),
|
|
177
|
+
];
|
|
178
|
+
this._ignoreFileRules = names.length > 0 ? loadIgnoreFileRules(this.rootPath, names) : null;
|
|
179
|
+
}
|
|
180
|
+
return this._ignoreFileRules;
|
|
113
181
|
}
|
|
114
182
|
|
|
115
183
|
getStats() {
|
|
@@ -121,7 +189,8 @@ export class Scanner {
|
|
|
121
189
|
filesScanned: 0,
|
|
122
190
|
directoriesTraversed: 0,
|
|
123
191
|
filesIgnored: 0,
|
|
124
|
-
|
|
192
|
+
filesDenied: 0,
|
|
193
|
+
errors: 0,
|
|
125
194
|
};
|
|
126
195
|
}
|
|
127
196
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token count for a target model
|
|
3
|
+
* Counts a project (or a piped text) with the target model's tokenizer where one is
|
|
4
|
+
* available offline (tiktoken for OpenAI models), and, on request, exactly through
|
|
5
|
+
* Anthropic's count_tokens API for Claude models.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import Scanner from '../core/Scanner.js';
|
|
10
|
+
import TokenUtils from '../utils/token-utils.js';
|
|
11
|
+
import { LLMDetector } from '../utils/llm-detector.js';
|
|
12
|
+
import { ClaudeTokenCounter } from '../utils/claude-token-counter.js';
|
|
13
|
+
import { redactSecrets } from '../utils/secret-redactor.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read the documents to count
|
|
17
|
+
* @param {string} root - Project root (ignored when `text` is given)
|
|
18
|
+
* @param {string} [text] - A single piped text instead of the project files
|
|
19
|
+
* @returns {{path: string, content: string}[]}
|
|
20
|
+
*/
|
|
21
|
+
function readDocuments(root, text) {
|
|
22
|
+
if (text !== undefined) return [{ path: '<stdin>', content: text }];
|
|
23
|
+
return new Scanner(root).scan().flatMap((file) => {
|
|
24
|
+
try {
|
|
25
|
+
return [{ path: file.relativePath, content: fs.readFileSync(file.path, 'utf8') }];
|
|
26
|
+
} catch (_error) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Count tokens for a target model
|
|
34
|
+
* @param {string} root - Project root
|
|
35
|
+
* @param {object} [options]
|
|
36
|
+
* @param {string} [options.model] - Target model id or alias (default: DEFAULT_TARGET_MODEL)
|
|
37
|
+
* @param {string} [options.text] - Count this text instead of the project
|
|
38
|
+
* @param {boolean} [options.api=false] - Count exactly with the Anthropic API (Claude only)
|
|
39
|
+
* @param {object} [options.client] - Anthropic client stand-in (tests)
|
|
40
|
+
* @param {(done: number, total: number) => void} [options.onProgress]
|
|
41
|
+
* @returns {Promise<object>} Count result
|
|
42
|
+
*/
|
|
43
|
+
export async function countForModel(root, options = {}) {
|
|
44
|
+
const requested = options.model || LLMDetector.getDefaultModelId();
|
|
45
|
+
const profile = LLMDetector.getProfile(requested);
|
|
46
|
+
const modelId = profile.id || requested;
|
|
47
|
+
const encoding = TokenUtils.encodingFor(modelId);
|
|
48
|
+
const documents = readDocuments(root, options.text);
|
|
49
|
+
|
|
50
|
+
const files = documents
|
|
51
|
+
.map((doc) => ({
|
|
52
|
+
path: doc.path,
|
|
53
|
+
tokens: TokenUtils.calculate(doc.content, doc.path, encoding),
|
|
54
|
+
}))
|
|
55
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
56
|
+
const localTokens = files.reduce((sum, file) => sum + file.tokens, 0);
|
|
57
|
+
|
|
58
|
+
const result = {
|
|
59
|
+
model: modelId,
|
|
60
|
+
modelName: profile.name,
|
|
61
|
+
vendor: profile.vendor,
|
|
62
|
+
files: files.length,
|
|
63
|
+
encoding: TokenUtils.isExact() ? encoding : 'estimate',
|
|
64
|
+
localTokens,
|
|
65
|
+
tokens: localTokens,
|
|
66
|
+
method: 'local',
|
|
67
|
+
// tiktoken matches OpenAI models whose encoding is published; others are approximations
|
|
68
|
+
exact: TokenUtils.isExact() && profile.tokenizer?.exact === true,
|
|
69
|
+
contextWindow: profile.contextWindow,
|
|
70
|
+
largestFiles: files,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (options.api) {
|
|
74
|
+
if (profile.vendor !== 'Anthropic') {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`--api counts with Anthropic's API, so it needs a Claude model (got ${requested})`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const counter = await ClaudeTokenCounter.create({ model: modelId, client: options.client });
|
|
80
|
+
// Secrets never leave the machine, even for counting
|
|
81
|
+
const redacted = documents.map((doc) => ({
|
|
82
|
+
path: doc.path,
|
|
83
|
+
content: redactSecrets(doc.content).text,
|
|
84
|
+
}));
|
|
85
|
+
const { total, requests } = await counter.countDocuments(redacted, {
|
|
86
|
+
onProgress: options.onProgress,
|
|
87
|
+
});
|
|
88
|
+
result.tokens = total;
|
|
89
|
+
result.method = 'anthropic-api';
|
|
90
|
+
result.exact = true;
|
|
91
|
+
result.requests = requests;
|
|
92
|
+
result.ratio = localTokens > 0 ? total / localTokens : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
result.fits = result.tokens <= result.contextWindow;
|
|
96
|
+
result.percentOfWindow = (result.tokens / result.contextWindow) * 100;
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default countForModel;
|