ctxman 3.0.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/custom-profiles.example.json +20 -0
- package/.ctxman/llm-profiles.json +131 -0
- package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
- 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/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
- package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
- package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
- package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
- package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
- package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
- package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
- package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
- package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
- package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
- package/.ctxman/wizard-profiles/full/.contextignore +23 -0
- package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
- package/.ctxman/wizard-profiles/full/.methodignore +5 -0
- package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
- package/.ctxman/wizard-profiles/full/profile.json +28 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
- package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
- package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
- package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
- package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
- package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
- package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
- package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
- package/README.md +1167 -0
- package/bin/cli.js +650 -0
- package/bin/cm-ask.js +95 -0
- package/bin/cm-gitingest.js +134 -0
- package/bin/cm-update.js +175 -0
- package/bin/mcp-server.js +31 -0
- package/ctxman.js +236 -0
- package/examples/README.md +298 -0
- package/examples/custom-llm-profiles.example.json +19 -0
- package/examples/wizard-profiles/code-review/.contextignore +41 -0
- package/examples/wizard-profiles/code-review/.contextinclude +30 -0
- package/examples/wizard-profiles/code-review/.methodignore +26 -0
- package/examples/wizard-profiles/code-review/.methodinclude +47 -0
- package/examples/wizard-profiles/code-review/profile.json +26 -0
- package/examples/wizard-profiles/documentation/.contextignore +21 -0
- package/examples/wizard-profiles/documentation/.contextinclude +24 -0
- package/examples/wizard-profiles/documentation/.methodignore +19 -0
- package/examples/wizard-profiles/documentation/.methodinclude +44 -0
- package/examples/wizard-profiles/documentation/profile.json +27 -0
- package/examples/wizard-profiles/full/.contextignore +23 -0
- package/examples/wizard-profiles/full/.contextinclude +38 -0
- package/examples/wizard-profiles/full/.methodignore +5 -0
- package/examples/wizard-profiles/full/.methodinclude +5 -0
- package/examples/wizard-profiles/full/profile.json +28 -0
- package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
- package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
- package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/examples/wizard-profiles/llm-explain/profile.json +27 -0
- package/examples/wizard-profiles/minimal/.contextignore +27 -0
- package/examples/wizard-profiles/minimal/.contextinclude +14 -0
- package/examples/wizard-profiles/minimal/.methodignore +22 -0
- package/examples/wizard-profiles/minimal/.methodinclude +18 -0
- package/examples/wizard-profiles/minimal/profile.json +26 -0
- package/examples/wizard-profiles/security-audit/.contextignore +36 -0
- package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
- package/examples/wizard-profiles/security-audit/.methodignore +29 -0
- package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
- package/examples/wizard-profiles/security-audit/profile.json +34 -0
- package/index.js +68 -0
- package/lib/agents/pr-failure-agent.js +201 -0
- package/lib/analyzers/go-method-analyzer.js +126 -0
- package/lib/analyzers/method-analyzer.js +457 -0
- package/lib/analyzers/token-calculator.js +577 -0
- package/lib/api/mcp/prompts.js +319 -0
- package/lib/api/mcp/resources.js +269 -0
- package/lib/api/mcp/server.js +339 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
- package/lib/api/rest/server.js +405 -0
- package/lib/cache/CacheManager.js +388 -0
- package/lib/core/Analyzer.js +180 -0
- package/lib/core/ContextBuilder.js +325 -0
- package/lib/core/Reporter.js +270 -0
- package/lib/core/Scanner.js +129 -0
- package/lib/formatters/format-registry.js +419 -0
- package/lib/formatters/gitingest-formatter.js +746 -0
- package/lib/formatters/toon-formatter-v1.3.js +731 -0
- package/lib/formatters/toon-formatter.js +291 -0
- package/lib/integrations/git/BlameTracker.js +142 -0
- package/lib/integrations/git/DiffAnalyzer.js +260 -0
- package/lib/integrations/git/GitClient.js +336 -0
- package/lib/parsers/gitignore-parser.js +127 -0
- package/lib/parsers/method-filter-parser.js +50 -0
- package/lib/plugins/ExporterPlugin.js +102 -0
- package/lib/plugins/LanguagePlugin.js +100 -0
- package/lib/plugins/PluginManager.js +302 -0
- package/lib/rag/DataSourcePlugin.js +31 -0
- package/lib/rag/EmbeddingProvider.js +75 -0
- package/lib/rag/Indexer.js +83 -0
- package/lib/rag/VectorStoreFactory.js +24 -0
- package/lib/rag/sources/FileDataSource.js +70 -0
- package/lib/rag/stores/BaseVectorStore.js +43 -0
- package/lib/rag/stores/ChromaDBStore.js +85 -0
- package/lib/rag/stores/LanceDBStore.js +109 -0
- package/lib/rag/stores/PineconeStore.js +74 -0
- package/lib/ui/dashboard.js +109 -0
- package/lib/ui/index.js +15 -0
- package/lib/ui/progress-bar.js +110 -0
- package/lib/ui/select-input.js +51 -0
- package/lib/ui/wizard.js +359 -0
- package/lib/utils/clipboard-utils.js +66 -0
- package/lib/utils/config-utils.js +94 -0
- package/lib/utils/error-handler.js +145 -0
- package/lib/utils/file-utils.js +89 -0
- package/lib/utils/format-converter.js +325 -0
- package/lib/utils/git-utils.js +337 -0
- package/lib/utils/llm-detector.js +409 -0
- package/lib/utils/logger.js +278 -0
- package/lib/utils/token-utils.js +200 -0
- package/lib/utils/tokenizer-adapter.js +454 -0
- package/lib/utils/updater.js +565 -0
- package/lib/watch/FileWatcher.js +208 -0
- package/lib/watch/IncrementalAnalyzer.js +206 -0
- package/package.json +118 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import TokenUtils from '../utils/token-utils.js';
|
|
6
|
+
import FileUtils from '../utils/file-utils.js';
|
|
7
|
+
import ClipboardUtils from '../utils/clipboard-utils.js';
|
|
8
|
+
import ConfigUtils from '../utils/config-utils.js';
|
|
9
|
+
import GitIgnoreParser from '../parsers/gitignore-parser.js';
|
|
10
|
+
import MethodAnalyzer from './method-analyzer.js';
|
|
11
|
+
import MethodFilterParser from '../parsers/method-filter-parser.js';
|
|
12
|
+
import GitIngestFormatter from '../formatters/gitingest-formatter.js';
|
|
13
|
+
import { LLMDetector } from '../utils/llm-detector.js';
|
|
14
|
+
|
|
15
|
+
class TokenCalculator {
|
|
16
|
+
constructor(projectRoot, options = {}) {
|
|
17
|
+
this.projectRoot = projectRoot;
|
|
18
|
+
this.options = { verbose: false, compactContext: true, methodLevel: false, ...options };
|
|
19
|
+
this.stats = this.initStats();
|
|
20
|
+
this.gitIgnore = this.initGitIgnore();
|
|
21
|
+
this.methodAnalyzer = new MethodAnalyzer();
|
|
22
|
+
this.methodFilter = this.options.methodLevel ? this.initMethodFilter() : null;
|
|
23
|
+
this.methodStats = { totalMethods: 0, includedMethods: 0, methodTokens: {} };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
initMethodFilter() {
|
|
27
|
+
return ConfigUtils.initMethodFilter(this.projectRoot);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
initStats() {
|
|
31
|
+
return {
|
|
32
|
+
totalFiles: 0, totalTokens: 0, totalBytes: 0, totalLines: 0,
|
|
33
|
+
ignoredFiles: 0, calculatorIgnoredFiles: 0,
|
|
34
|
+
byExtension: {}, byDirectory: {}, largestFiles: []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
initGitIgnore() {
|
|
39
|
+
return ConfigUtils.initGitIgnore(this.projectRoot);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
calculateTokens(content, filePath) {
|
|
43
|
+
const tokens = TokenUtils.calculate(content, filePath);
|
|
44
|
+
return tokens;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isTextFile(filePath) {
|
|
48
|
+
return FileUtils.isText(filePath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
analyzeFile(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
const stats = fs.statSync(filePath);
|
|
55
|
+
|
|
56
|
+
const fileInfo = {
|
|
57
|
+
path: filePath,
|
|
58
|
+
relativePath: path.relative(this.projectRoot, filePath),
|
|
59
|
+
sizeBytes: stats.size,
|
|
60
|
+
tokens: this.calculateTokens(content, filePath),
|
|
61
|
+
lines: content.split('\n').length,
|
|
62
|
+
extension: path.extname(filePath).toLowerCase() || 'no-extension'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Method-level analysis if enabled
|
|
66
|
+
if (this.options.methodLevel && this.isCodeFile(filePath)) {
|
|
67
|
+
fileInfo.methods = this.analyzeFileMethods(content, filePath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return fileInfo;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
path: filePath,
|
|
74
|
+
relativePath: path.relative(this.projectRoot, filePath),
|
|
75
|
+
sizeBytes: 0, tokens: 0, lines: 0,
|
|
76
|
+
extension: 'error', error: error.message
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isCodeFile(filePath) {
|
|
82
|
+
return FileUtils.isCode(filePath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
analyzeFileMethods(content, filePath) {
|
|
86
|
+
const methods = this.methodAnalyzer.extractMethods(content, filePath);
|
|
87
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
88
|
+
const filteredMethods = [];
|
|
89
|
+
|
|
90
|
+
this.methodStats.totalMethods += methods.length;
|
|
91
|
+
|
|
92
|
+
for (const method of methods) {
|
|
93
|
+
if (!this.methodFilter || this.methodFilter.shouldIncludeMethod(method.name, fileName)) {
|
|
94
|
+
const methodContent = this.methodAnalyzer.extractMethodContent(content, method.name);
|
|
95
|
+
const methodTokens = methodContent ? this.calculateTokens(methodContent, filePath) : 0;
|
|
96
|
+
|
|
97
|
+
const methodInfo = {
|
|
98
|
+
...method,
|
|
99
|
+
tokens: methodTokens,
|
|
100
|
+
content: methodContent
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
filteredMethods.push(methodInfo);
|
|
104
|
+
this.methodStats.includedMethods++;
|
|
105
|
+
this.methodStats.methodTokens[`${fileName}.${method.name}`] = methodTokens;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return filteredMethods;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
scanDirectory(dir) {
|
|
113
|
+
const files = [];
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
for (const item of fs.readdirSync(dir)) {
|
|
117
|
+
const fullPath = path.join(dir, item);
|
|
118
|
+
const relativePath = path.relative(this.projectRoot, fullPath);
|
|
119
|
+
|
|
120
|
+
if (this.gitIgnore.isIgnored(fullPath, relativePath)) {
|
|
121
|
+
this.countIgnoredFiles(fullPath);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const stat = fs.statSync(fullPath);
|
|
126
|
+
if (stat.isDirectory()) {
|
|
127
|
+
if (!['node_modules', '.git', '.svn', '.hg', 'coverage', 'dist', 'build'].includes(item)) {
|
|
128
|
+
files.push(...this.scanDirectory(fullPath));
|
|
129
|
+
}
|
|
130
|
+
} else if (this.isTextFile(fullPath)) {
|
|
131
|
+
files.push(fullPath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error(`Error scanning directory ${dir}:`, error.message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
countIgnoredFiles(filePath) {
|
|
142
|
+
const isCalculatorIgnored = ['calculator', 'calculator-include'].includes(
|
|
143
|
+
this.gitIgnore._lastIgnoreReason
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const stat = fs.statSync(filePath);
|
|
148
|
+
if (stat.isDirectory()) {
|
|
149
|
+
const count = this.countFilesInDirectory(filePath);
|
|
150
|
+
if (isCalculatorIgnored) {
|
|
151
|
+
this.stats.calculatorIgnoredFiles += count;
|
|
152
|
+
} else {
|
|
153
|
+
this.stats.ignoredFiles += count;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
if (isCalculatorIgnored) {
|
|
157
|
+
this.stats.calculatorIgnoredFiles++;
|
|
158
|
+
} else {
|
|
159
|
+
this.stats.ignoredFiles++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch { }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
countFilesInDirectory(dirPath) {
|
|
166
|
+
let count = 0;
|
|
167
|
+
try {
|
|
168
|
+
for (const item of fs.readdirSync(dirPath)) {
|
|
169
|
+
const fullPath = path.join(dirPath, item);
|
|
170
|
+
const stat = fs.statSync(fullPath);
|
|
171
|
+
|
|
172
|
+
if (stat.isDirectory()) {
|
|
173
|
+
count += this.countFilesInDirectory(fullPath);
|
|
174
|
+
} else if (this.isTextFile(fullPath)) {
|
|
175
|
+
count++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch { }
|
|
179
|
+
return count;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
updateStats(fileInfo) {
|
|
183
|
+
if (fileInfo.error) return;
|
|
184
|
+
|
|
185
|
+
const { extension, tokens, sizeBytes, lines } = fileInfo;
|
|
186
|
+
|
|
187
|
+
this.stats.totalFiles++;
|
|
188
|
+
this.stats.totalTokens += tokens;
|
|
189
|
+
this.stats.totalBytes += sizeBytes;
|
|
190
|
+
this.stats.totalLines += lines;
|
|
191
|
+
this.stats.largestFiles.push(fileInfo);
|
|
192
|
+
|
|
193
|
+
// Update extension stats
|
|
194
|
+
this.stats.byExtension[extension] = this.stats.byExtension[extension] ||
|
|
195
|
+
{ count: 0, tokens: 0, bytes: 0, lines: 0 };
|
|
196
|
+
const extStats = this.stats.byExtension[extension];
|
|
197
|
+
extStats.count++; extStats.tokens += tokens;
|
|
198
|
+
extStats.bytes += sizeBytes; extStats.lines += lines;
|
|
199
|
+
|
|
200
|
+
// Update directory stats
|
|
201
|
+
const topDir = fileInfo.relativePath.split('/')[0];
|
|
202
|
+
this.stats.byDirectory[topDir] = this.stats.byDirectory[topDir] ||
|
|
203
|
+
{ count: 0, tokens: 0, bytes: 0, lines: 0 };
|
|
204
|
+
const dirStats = this.stats.byDirectory[topDir];
|
|
205
|
+
dirStats.count++; dirStats.tokens += tokens;
|
|
206
|
+
dirStats.bytes += sizeBytes; dirStats.lines += lines;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
generateLLMContext(analysisResults) {
|
|
210
|
+
const context = {
|
|
211
|
+
project: {
|
|
212
|
+
root: path.basename(this.projectRoot),
|
|
213
|
+
totalFiles: this.stats.totalFiles,
|
|
214
|
+
totalTokens: this.stats.totalTokens
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (this.options.methodLevel) {
|
|
219
|
+
context.methods = this.generateMethodContext(analysisResults.files || analysisResults);
|
|
220
|
+
context.methodStats = {
|
|
221
|
+
totalMethods: this.methodStats.totalMethods,
|
|
222
|
+
includedMethods: this.methodStats.includedMethods,
|
|
223
|
+
totalMethodTokens: Object.values(this.methodStats.methodTokens).reduce((sum, tokens) => sum + tokens, 0)
|
|
224
|
+
};
|
|
225
|
+
} else {
|
|
226
|
+
context.paths = this.generateCompactPaths(analysisResults.files || analysisResults);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return context;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
generateMethodContext(analysisResults) {
|
|
233
|
+
const methodContext = {};
|
|
234
|
+
|
|
235
|
+
for (const fileInfo of analysisResults) {
|
|
236
|
+
if (fileInfo.methods && fileInfo.methods.length > 0) {
|
|
237
|
+
methodContext[fileInfo.relativePath] = fileInfo.methods.map(method => ({
|
|
238
|
+
name: method.name,
|
|
239
|
+
line: method.line,
|
|
240
|
+
tokens: method.tokens
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return methodContext;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
generateCompactPaths(analysisResults) {
|
|
249
|
+
const sortedFiles = analysisResults.sort((a, b) => b.tokens - a.tokens);
|
|
250
|
+
const pathGroups = {};
|
|
251
|
+
|
|
252
|
+
sortedFiles.forEach(file => {
|
|
253
|
+
const filePath = file.relativePath;
|
|
254
|
+
const parts = filePath.split('/');
|
|
255
|
+
|
|
256
|
+
// Group files by their directory path
|
|
257
|
+
// If file is in root, use '/' as key, otherwise use directory path with trailing slash
|
|
258
|
+
const categoryPath = parts.length > 1 ? parts.slice(0, -1).join('/') + '/' : '/';
|
|
259
|
+
|
|
260
|
+
if (!pathGroups[categoryPath]) {
|
|
261
|
+
pathGroups[categoryPath] = [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const filename = parts[parts.length - 1];
|
|
265
|
+
pathGroups[categoryPath].push(filename);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return pathGroups;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
exportContextToClipboard(context) {
|
|
272
|
+
const contextString = JSON.stringify(context, null, 2);
|
|
273
|
+
const success = ClipboardUtils.copy(contextString);
|
|
274
|
+
|
|
275
|
+
if (!success) {
|
|
276
|
+
console.log('š” Context saved to llm-context.json instead');
|
|
277
|
+
this.saveContextToFile(context);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
saveContextToFile(context) {
|
|
282
|
+
const contextPath = path.join(this.projectRoot, 'llm-context.json');
|
|
283
|
+
fs.writeFileSync(contextPath, JSON.stringify(context, null, 2));
|
|
284
|
+
console.log(`š¾ Context saved to: ${contextPath}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
promptForExport(analysisResults) {
|
|
288
|
+
console.log('\nš¤ Export Options:');
|
|
289
|
+
console.log('1) Save detailed JSON report (token-analysis-report.json)');
|
|
290
|
+
console.log('2) Generate LLM context file (llm-context.json)');
|
|
291
|
+
console.log('3) Copy LLM context to clipboard');
|
|
292
|
+
console.log('4) Generate GitIngest digest (digest.txt)');
|
|
293
|
+
console.log('5) No export (skip)');
|
|
294
|
+
|
|
295
|
+
const rl = readline.createInterface({
|
|
296
|
+
input: process.stdin,
|
|
297
|
+
output: process.stdout
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
rl.question('š¤ Which export option would you like? (1-5): ', (answer) => {
|
|
301
|
+
const trimmed = answer.trim();
|
|
302
|
+
|
|
303
|
+
// Handle empty input (just Enter pressed) - default to option 5 (skip)
|
|
304
|
+
if (trimmed === '') {
|
|
305
|
+
console.log('\nā
Analysis complete. No export selected.');
|
|
306
|
+
rl.close();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const choice = parseInt(trimmed);
|
|
311
|
+
|
|
312
|
+
// Validate input is a number between 1-5
|
|
313
|
+
if (isNaN(choice) || choice < 1 || choice > 5) {
|
|
314
|
+
console.log('\nā Invalid option. Please enter a number between 1-5.');
|
|
315
|
+
console.log(' Skipping export.');
|
|
316
|
+
rl.close();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
switch (choice) {
|
|
321
|
+
case 1:
|
|
322
|
+
console.log('\nš¾ Saving detailed JSON report...');
|
|
323
|
+
this.saveDetailedReport(analysisResults);
|
|
324
|
+
break;
|
|
325
|
+
case 2:
|
|
326
|
+
console.log('\nš¤ Generating LLM context file...');
|
|
327
|
+
const context = this.generateLLMContext(analysisResults);
|
|
328
|
+
this.saveContextToFile(context);
|
|
329
|
+
console.log('š¾ LLM context saved to: llm-context.json');
|
|
330
|
+
break;
|
|
331
|
+
case 3:
|
|
332
|
+
console.log('\nš¤ Generating LLM context for clipboard...');
|
|
333
|
+
const clipboardContext = this.generateLLMContext(analysisResults);
|
|
334
|
+
this.exportContextToClipboard(clipboardContext);
|
|
335
|
+
break;
|
|
336
|
+
case 4:
|
|
337
|
+
console.log('\nš Generating GitIngest digest...');
|
|
338
|
+
this.saveGitIngestDigest(analysisResults);
|
|
339
|
+
break;
|
|
340
|
+
case 5:
|
|
341
|
+
console.log('\nā
Analysis complete. No export selected.');
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
rl.close();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
saveGitIngestDigest(analysisResults) {
|
|
350
|
+
const formatter = new GitIngestFormatter(this.projectRoot, this.stats, analysisResults);
|
|
351
|
+
const digestPath = path.join(this.projectRoot, 'digest.txt');
|
|
352
|
+
const digestSize = formatter.saveToFile(digestPath);
|
|
353
|
+
|
|
354
|
+
console.log(`š¾ GitIngest digest saved to: digest.txt`);
|
|
355
|
+
console.log(`š Digest size: ${(digestSize / 1024).toFixed(1)} KB`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
run() {
|
|
359
|
+
this.printHeader();
|
|
360
|
+
|
|
361
|
+
const allFiles = this.scanDirectory(this.projectRoot);
|
|
362
|
+
this.printScanResults(allFiles);
|
|
363
|
+
|
|
364
|
+
// Analyze all files
|
|
365
|
+
const analysisResults = [];
|
|
366
|
+
for (const file of allFiles) {
|
|
367
|
+
const fileInfo = this.analyzeFile(file);
|
|
368
|
+
this.updateStats(fileInfo);
|
|
369
|
+
analysisResults.push(fileInfo);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.stats.largestFiles.sort((a, b) => b.tokens - a.tokens);
|
|
373
|
+
this.printReport();
|
|
374
|
+
|
|
375
|
+
// Context Fit Analysis (v2.3.7)
|
|
376
|
+
if (this.options.targetModel && !this.options.dashboard) {
|
|
377
|
+
this.printContextFitAnalysis();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Handle exports (skip for dashboard mode)
|
|
381
|
+
if (!this.options.dashboard) {
|
|
382
|
+
this.handleExports(analysisResults);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Return stats for programmatic usage (e.g., Dashboard)
|
|
386
|
+
return this.stats;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
printHeader() {
|
|
390
|
+
console.log('š Analyzing project: ' + this.projectRoot);
|
|
391
|
+
console.log('š Respecting .gitignore rules...');
|
|
392
|
+
|
|
393
|
+
const configPath = this.gitIgnore.hasIncludeFile ?
|
|
394
|
+
ConfigUtils.findConfigFile(this.projectRoot, '.contextinclude') :
|
|
395
|
+
ConfigUtils.findConfigFile(this.projectRoot, '.contextignore');
|
|
396
|
+
|
|
397
|
+
if (configPath) {
|
|
398
|
+
const mode = this.gitIgnore.hasIncludeFile ? 'INCLUDE' : 'EXCLUDE';
|
|
399
|
+
console.log(`š
Found calculator config - using ${mode} mode`);
|
|
400
|
+
console.log(' Location: ' + path.relative(this.projectRoot, configPath));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log('šÆ Token calculation: ' + (TokenUtils.hasExactCounting() ? 'ā
Exact (using tiktoken)' : 'ā ļø Estimated'));
|
|
404
|
+
|
|
405
|
+
if (this.options.methodLevel) {
|
|
406
|
+
console.log('š§ Method-level analysis: ā
Enabled');
|
|
407
|
+
if (this.methodFilter) {
|
|
408
|
+
const hasInclude = this.methodFilter.hasIncludeFile;
|
|
409
|
+
const mode = hasInclude ? 'INCLUDE' : 'EXCLUDE';
|
|
410
|
+
console.log(` Method filtering: ${mode} mode active`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.log();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
printScanResults(allFiles) {
|
|
418
|
+
console.log(`š Found ${allFiles.length} text files to analyze`);
|
|
419
|
+
console.log(`š« Ignored ${this.stats.ignoredFiles} files due to .gitignore rules`);
|
|
420
|
+
if (this.stats.calculatorIgnoredFiles > 0) {
|
|
421
|
+
const mode = this.gitIgnore.hasIncludeFile ? 'include rules' : 'ignore rules';
|
|
422
|
+
console.log(`š Filtered ${this.stats.calculatorIgnoredFiles} additional files due to calculator ${mode}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (this.options.methodLevel) {
|
|
426
|
+
console.log(`š§ Total methods found: ${this.methodStats.totalMethods}`);
|
|
427
|
+
console.log(`ā
Methods included: ${this.methodStats.includedMethods}`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
console.log();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
printReport() {
|
|
434
|
+
console.log('='.repeat(80));
|
|
435
|
+
console.log('šÆ PROJECT TOKEN ANALYSIS REPORT');
|
|
436
|
+
console.log('='.repeat(80));
|
|
437
|
+
console.log(`š Total files analyzed: ${this.stats.totalFiles.toLocaleString()}`);
|
|
438
|
+
console.log(`š¢ Total tokens: ${this.stats.totalTokens.toLocaleString()}`);
|
|
439
|
+
console.log(`š¾ Total size: ${(this.stats.totalBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
440
|
+
console.log(`š Total lines: ${this.stats.totalLines.toLocaleString()}`);
|
|
441
|
+
|
|
442
|
+
const avgTokens = this.stats.totalFiles > 0
|
|
443
|
+
? Math.round(this.stats.totalTokens / this.stats.totalFiles).toLocaleString()
|
|
444
|
+
: 'N/A';
|
|
445
|
+
console.log(`š Average tokens per file: ${avgTokens}`);
|
|
446
|
+
console.log(`š« Files ignored by .gitignore: ${this.stats.ignoredFiles.toLocaleString()}`);
|
|
447
|
+
if (this.stats.calculatorIgnoredFiles > 0) {
|
|
448
|
+
console.log(`š Files ignored by calculator rules: ${this.stats.calculatorIgnoredFiles.toLocaleString()}`);
|
|
449
|
+
}
|
|
450
|
+
console.log();
|
|
451
|
+
|
|
452
|
+
this.printExtensionStats();
|
|
453
|
+
this.printTopFiles();
|
|
454
|
+
this.printTopDirectories();
|
|
455
|
+
|
|
456
|
+
console.log('\nš” Tip: Use --save-report flag to save detailed analysis to JSON file');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
printExtensionStats() {
|
|
460
|
+
console.log('š BY FILE TYPE:');
|
|
461
|
+
console.log('-'.repeat(80));
|
|
462
|
+
console.log('Extension'.padEnd(15) + 'Files'.padStart(8) + 'Tokens'.padStart(12) + 'Size (KB)'.padStart(12) + 'Lines'.padStart(10));
|
|
463
|
+
console.log('-'.repeat(80));
|
|
464
|
+
|
|
465
|
+
Object.entries(this.stats.byExtension)
|
|
466
|
+
.sort((a, b) => b[1].tokens - a[1].tokens)
|
|
467
|
+
.forEach(([ext, data]) => {
|
|
468
|
+
console.log(
|
|
469
|
+
ext.padEnd(15) +
|
|
470
|
+
data.count.toString().padStart(8) +
|
|
471
|
+
data.tokens.toLocaleString().padStart(12) +
|
|
472
|
+
(data.bytes / 1024).toFixed(1).padStart(12) +
|
|
473
|
+
data.lines.toLocaleString().padStart(10)
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
printTopFiles() {
|
|
479
|
+
console.log('\nš TOP 5 LARGEST FILES BY TOKEN COUNT:');
|
|
480
|
+
console.log('-'.repeat(80));
|
|
481
|
+
this.stats.largestFiles.slice(0, 5).forEach((file, index) => {
|
|
482
|
+
const percentage = ((file.tokens / this.stats.totalTokens) * 100).toFixed(1);
|
|
483
|
+
const sizeKB = (file.sizeBytes / 1024).toFixed(1);
|
|
484
|
+
console.log(`${(index + 1).toString().padStart(2)}. ${file.tokens.toLocaleString().padStart(8)} tokens (${percentage}%) - ${file.relativePath}`);
|
|
485
|
+
console.log(` š¾ ${sizeKB.padStart(8)} KB, š ${file.lines.toLocaleString().padStart(6)} lines`);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
printTopDirectories() {
|
|
490
|
+
const largestDirectories = Object.entries(this.stats.byDirectory)
|
|
491
|
+
.map(([dir, stats]) => ({ directory: dir, ...stats }))
|
|
492
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
493
|
+
|
|
494
|
+
console.log('\nš TOP 5 LARGEST DIRECTORIES BY TOKEN COUNT:');
|
|
495
|
+
console.log('-'.repeat(80));
|
|
496
|
+
largestDirectories.slice(0, 5).forEach((dir, index) => {
|
|
497
|
+
const percentage = ((dir.tokens / this.stats.totalTokens) * 100).toFixed(1);
|
|
498
|
+
const avgTokensPerFile = Math.round(dir.tokens / dir.count);
|
|
499
|
+
console.log(`${(index + 1).toString().padStart(2)}. ${dir.tokens.toLocaleString().padStart(8)} tokens (${percentage}%) - ${dir.directory}/`);
|
|
500
|
+
console.log(` š ${dir.count.toString().padStart(3)} files, š ${avgTokensPerFile.toLocaleString().padStart(6)} avg tokens/file`);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
handleExports(analysisResults) {
|
|
505
|
+
const { saveReport, contextExport, contextToClipboard, gitingest } = this.options;
|
|
506
|
+
|
|
507
|
+
if (contextExport || contextToClipboard || saveReport || gitingest) {
|
|
508
|
+
if (contextExport || contextToClipboard) {
|
|
509
|
+
console.log('\nš¤ Generating LLM Context...');
|
|
510
|
+
const context = this.generateLLMContext(analysisResults);
|
|
511
|
+
|
|
512
|
+
if (contextToClipboard) {
|
|
513
|
+
this.exportContextToClipboard(context);
|
|
514
|
+
} else {
|
|
515
|
+
this.saveContextToFile(context);
|
|
516
|
+
console.log('š¾ LLM context saved to: llm-context.json');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (saveReport) {
|
|
521
|
+
this.saveDetailedReport(analysisResults);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (gitingest) {
|
|
525
|
+
console.log('\nš Generating GitIngest digest...');
|
|
526
|
+
this.saveGitIngestDigest(analysisResults);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
this.promptForExport(analysisResults);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
saveDetailedReport(analysisResults) {
|
|
534
|
+
const report = {
|
|
535
|
+
metadata: {
|
|
536
|
+
generatedAt: new Date().toISOString(),
|
|
537
|
+
projectRoot: this.projectRoot,
|
|
538
|
+
gitignoreRules: this.gitIgnore.patterns.map(p => p.original),
|
|
539
|
+
calculatorRules: this.gitIgnore.contextPatterns.map(p => p.original)
|
|
540
|
+
},
|
|
541
|
+
summary: this.stats,
|
|
542
|
+
files: analysisResults.sort((a, b) => b.tokens - a.tokens)
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const reportPath = path.join(this.projectRoot, 'token-analysis-report.json');
|
|
546
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
547
|
+
console.log('š¾ Detailed analysis saved to: token-analysis-report.json');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Print Context Fit Analysis (v2.3.7)
|
|
552
|
+
* Shows if repository fits in target LLM's context window
|
|
553
|
+
*/
|
|
554
|
+
printContextFitAnalysis() {
|
|
555
|
+
try {
|
|
556
|
+
const profile = LLMDetector.getProfile(this.options.targetModel);
|
|
557
|
+
const repoStats = {
|
|
558
|
+
totalTokens: this.stats.totalTokens,
|
|
559
|
+
totalFiles: this.stats.totalFiles
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const analysis = LLMDetector.analyzeContextFit(profile, repoStats);
|
|
563
|
+
const formatted = LLMDetector.formatAnalysis(analysis);
|
|
564
|
+
|
|
565
|
+
console.log(formatted);
|
|
566
|
+
|
|
567
|
+
} catch (error) {
|
|
568
|
+
// Silently fail if context fit analysis fails
|
|
569
|
+
// Don't break the main workflow
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
export default TokenCalculator;
|