context-squeezer-cli 1.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/LICENSE +21 -0
- package/README.md +72 -0
- package/bin/context-squeezer.js +2 -0
- package/dist/ignorer.js +75 -0
- package/dist/index.js +54 -0
- package/dist/packager.js +108 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erhan Namal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 🚀 Context-Squeezer
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Lightweight CLI that packs a repository into a single, LLM-optimized context file. Ideal for feeding projects to ChatGPT, Claude, or local LLMs without wasting tokens on irrelevant files.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🔥 Features
|
|
11
|
+
|
|
12
|
+
- **Smart Code Squeezing:** Produces a structured, human- and LLM-friendly summary of your codebase.
|
|
13
|
+
- **Automatic Ignore Detection:** Skips common heavy folders and files (`node_modules`, `.git`, `dist`, lockfiles, etc.).
|
|
14
|
+
- **Live Stats & Token Estimation:** Shows total files, lines, characters and an estimated LLM token count.
|
|
15
|
+
- **Custom Output:** Change the output filename or destination with a CLI flag.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🛠️ Installation
|
|
20
|
+
|
|
21
|
+
Clone and install dependencies:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/ErhanNamal/context-squeezer.git
|
|
25
|
+
cd context-squeezer
|
|
26
|
+
npm install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🚀 Usage
|
|
30
|
+
|
|
31
|
+
Squeeze the current project and write the default `ai_context.txt`:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Specify a custom output path:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm start -- --output my_project_summary.txt
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 🖥️ Terminal Dashboard (example)
|
|
44
|
+
|
|
45
|
+
When run, the CLI prints a compact analytics dashboard similar to the example below:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
=============================================
|
|
49
|
+
🚀 CONTEXT-SQUEEZER - AI Context Prepared
|
|
50
|
+
=============================================
|
|
51
|
+
|
|
52
|
+
🔍 Analyzing directory: D:\Koufc\Yazılım Çalışmaları\Karışık\context-squeezer
|
|
53
|
+
|
|
54
|
+
📊 PROJECT ANALYTICS REPORT
|
|
55
|
+
---------------------------------------------
|
|
56
|
+
📂 Total Files Compressed : 5
|
|
57
|
+
📝 Total Lines of Code : 155
|
|
58
|
+
🔤 Total Character Count : 4940
|
|
59
|
+
🪙 Estimated LLM Token Load: ~1235 tokens
|
|
60
|
+
---------------------------------------------
|
|
61
|
+
|
|
62
|
+
✅ Success! Your LLM context file is ready:
|
|
63
|
+
👉 D:\Koufc\Yazılım Çalışmaları\Karışık\context-squeezer\ai_context.txt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 🤝 Contributing
|
|
67
|
+
|
|
68
|
+
Contributions are welcome — open issues or pull requests for improvements. Add examples, edge-case handling, or CI workflows to help others use the tool.
|
|
69
|
+
|
|
70
|
+
## 📝 License
|
|
71
|
+
|
|
72
|
+
Distributed under the MIT License. See LICENSE for details.
|
package/dist/ignorer.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const DEFAULT_IGNORES = [
|
|
4
|
+
'node_modules',
|
|
5
|
+
'.git',
|
|
6
|
+
'package-lock.json',
|
|
7
|
+
'yarn.lock',
|
|
8
|
+
'pnpm-lock.yaml',
|
|
9
|
+
'dist',
|
|
10
|
+
'build',
|
|
11
|
+
'.DS_Store'
|
|
12
|
+
];
|
|
13
|
+
// Kuralları çalışma zamanında hafızada tutacak önbellek mekanizması
|
|
14
|
+
let cachedCustomRules = null;
|
|
15
|
+
/**
|
|
16
|
+
* Projenin kök dizinindeki .aiignore dosyasını sadece bir kez okuyarak belleğe alır.
|
|
17
|
+
* @param projectRoot Projenin ana çalışma dizini
|
|
18
|
+
*/
|
|
19
|
+
function loadCustomIgnoreRules(projectRoot) {
|
|
20
|
+
if (cachedCustomRules !== null) {
|
|
21
|
+
return cachedCustomRules;
|
|
22
|
+
}
|
|
23
|
+
const aiIgnorePath = path.join(projectRoot, '.aiignore');
|
|
24
|
+
if (!fs.existsSync(aiIgnorePath)) {
|
|
25
|
+
cachedCustomRules = [];
|
|
26
|
+
return cachedCustomRules;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = fs.readFileSync(aiIgnorePath, 'utf-8');
|
|
30
|
+
cachedCustomRules = content
|
|
31
|
+
.split('\n')
|
|
32
|
+
.map(line => line.trim())
|
|
33
|
+
.filter(line => line.length > 0 && !line.startsWith('#'));
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
cachedCustomRules = [];
|
|
37
|
+
}
|
|
38
|
+
return cachedCustomRules;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Belirtilen dosya yolunun engellenip engellenmeyeceğini kontrol eder.
|
|
42
|
+
* @param filePath Denetlenecek dosyanın tam yolu
|
|
43
|
+
* @param projectRoot Projenin ana çalışma dizini
|
|
44
|
+
*/
|
|
45
|
+
export function shouldIgnore(filePath, projectRoot) {
|
|
46
|
+
// Windows'taki \ işaretlerini / işaretine çevirerek yolları normalize ediyoruz
|
|
47
|
+
const relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
48
|
+
const parts = relativePath.split('/');
|
|
49
|
+
// 1. Çıktı ve test dosyalarının kendi kendini taramasını engeller
|
|
50
|
+
const file = path.basename(filePath);
|
|
51
|
+
if (file.endsWith('.txt') && (file === 'ai_context.txt' || file === 'kodlarim.txt' || file === 'test_output.txt')) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// 2. Varsayılan global engelleme listesi kontrolü
|
|
55
|
+
if (parts.some(part => DEFAULT_IGNORES.includes(part))) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
// 3. Kullanıcı tanımlı .aiignore kurallarının kontrolü
|
|
59
|
+
const customRules = loadCustomIgnoreRules(projectRoot);
|
|
60
|
+
for (const rule of customRules) {
|
|
61
|
+
// .aiignore içindeki olası Windows yollarını da normalize et
|
|
62
|
+
const normalizedRule = rule.replace(/\\/g, '/');
|
|
63
|
+
if (normalizedRule.startsWith('*.')) {
|
|
64
|
+
const ext = normalizedRule.slice(1); // Örn: .json uzantısını yakalar
|
|
65
|
+
if (filePath.endsWith(ext))
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
else if (relativePath === normalizedRule ||
|
|
69
|
+
parts.includes(normalizedRule) ||
|
|
70
|
+
relativePath.startsWith(normalizedRule + '/')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { packProject } from './packager.js';
|
|
4
|
+
function printHelp() {
|
|
5
|
+
console.log(`
|
|
6
|
+
Context-Squeezer - usage
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-h, --help Show this help message
|
|
10
|
+
-o, --output <file> Write output to <file> (default: ai_context.txt)
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
npm start -- --output my_context.txt
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
function main() {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
19
|
+
printHelp();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let outputFileName = 'ai_context.txt';
|
|
23
|
+
const oIndex = args.findIndex(a => a === '-o' || a === '--output');
|
|
24
|
+
if (oIndex !== -1 && args[oIndex + 1]) {
|
|
25
|
+
outputFileName = args[oIndex + 1];
|
|
26
|
+
}
|
|
27
|
+
const targetDir = process.cwd();
|
|
28
|
+
console.log('\n=============================================');
|
|
29
|
+
console.log('🚀 CONTEXT-SQUEEZER - AI Context Prepared');
|
|
30
|
+
console.log('=============================================\n');
|
|
31
|
+
console.log(`🔍 Analyzing directory: ${targetDir}`);
|
|
32
|
+
try {
|
|
33
|
+
const { combinedContent, stats } = packProject(targetDir);
|
|
34
|
+
const outputPath = path.isAbsolute(outputFileName)
|
|
35
|
+
? outputFileName
|
|
36
|
+
: path.join(targetDir, outputFileName);
|
|
37
|
+
fs.writeFileSync(outputPath, combinedContent, 'utf-8');
|
|
38
|
+
console.log('\n📊 PROJECT ANALYTICS REPORT');
|
|
39
|
+
console.log('---------------------------------------------');
|
|
40
|
+
console.log(`📂 Total Files Compressed : ${stats.totalFiles}`);
|
|
41
|
+
console.log(`📝 Total Lines of Code : ${stats.totalLines}`);
|
|
42
|
+
console.log(`🔤 Total Character Count : ${stats.totalChars}`);
|
|
43
|
+
console.log(`🪙 Estimated LLM Token Load: ~${stats.estimatedTokens} tokens`);
|
|
44
|
+
console.log('---------------------------------------------');
|
|
45
|
+
console.log(`\n✅ Success! Your LLM context file is ready:`);
|
|
46
|
+
console.log(`👉 ${outputPath}\n`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error('❌ An error occurred while building the context file:');
|
|
50
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
main();
|
package/dist/packager.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { shouldIgnore } from './ignorer.js';
|
|
4
|
+
// Güvenli işleme için maksimum dosya boyutu sınırı (Örn: 500 KB)
|
|
5
|
+
const MAX_FILE_SIZE_BYTES = 500 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Bir dosyanın ikili (binary) olup olmadığını ilk byte bloklarını tarayarak kontrol eder.
|
|
8
|
+
* @param filePath Denetlenecek dosyanın tam yolu
|
|
9
|
+
*/
|
|
10
|
+
function isBinaryFile(filePath) {
|
|
11
|
+
const buffer = Buffer.alloc(512);
|
|
12
|
+
let fd = null;
|
|
13
|
+
try {
|
|
14
|
+
fd = fs.openSync(filePath, 'r');
|
|
15
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
|
|
16
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
17
|
+
// Null karakteri (\0) kontrolü ikili dosyaları tespit etmek için en güvenli yöntemdir
|
|
18
|
+
if (buffer[i] === 0) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return true; // Okuma hatası durumunda güvenli tarafta kalmak için binary kabul edilir
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
if (fd !== null) {
|
|
29
|
+
fs.closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Kod içindeki hassas şifre, token ve API anahtarlarını sansürler
|
|
35
|
+
*/
|
|
36
|
+
function maskSensitiveData(content) {
|
|
37
|
+
const sensitivePatterns = [
|
|
38
|
+
/(secret[-_]?key|api[-_]?key|password|passwd|auth[-_]?token|client[-_]?secret)\s*[:=]\s*['"`][^'"`]{4,200}['"`]/gi,
|
|
39
|
+
/AIzaSy[A-Za-z0-9-_]{33}/g,
|
|
40
|
+
/sk-[a-zA-Z0-9]{48}/g
|
|
41
|
+
];
|
|
42
|
+
let maskedContent = content;
|
|
43
|
+
for (const pattern of sensitivePatterns) {
|
|
44
|
+
maskedContent = maskedContent.replace(pattern, (match) => {
|
|
45
|
+
if (match.includes('=') || match.includes(':')) {
|
|
46
|
+
const separator = match.includes('=') ? '=' : ':';
|
|
47
|
+
const parts = match.split(separator);
|
|
48
|
+
return `${parts[0]}${separator} "[REDACTED BY CONTEXT-SQUEEZER]"`;
|
|
49
|
+
}
|
|
50
|
+
return "[REDACTED BY CONTEXT-SQUEEZER]";
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return maskedContent;
|
|
54
|
+
}
|
|
55
|
+
export function packProject(dirPath) {
|
|
56
|
+
let combinedContent = '';
|
|
57
|
+
const stats = {
|
|
58
|
+
totalFiles: 0,
|
|
59
|
+
totalLines: 0,
|
|
60
|
+
totalChars: 0,
|
|
61
|
+
estimatedTokens: 0
|
|
62
|
+
};
|
|
63
|
+
function scan(currentDir) {
|
|
64
|
+
const files = fs.readdirSync(currentDir);
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const fullPath = path.join(currentDir, file);
|
|
67
|
+
const stat = fs.statSync(fullPath);
|
|
68
|
+
if (shouldIgnore(fullPath, dirPath)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (stat.isDirectory()) {
|
|
72
|
+
scan(fullPath);
|
|
73
|
+
}
|
|
74
|
+
else if (stat.isFile()) {
|
|
75
|
+
const relativePath = path.relative(dirPath, fullPath);
|
|
76
|
+
// Çıktı ve test dosyalarının kendi kendini taramasını engeller
|
|
77
|
+
if (file.endsWith('.txt') && (file === 'ai_context.txt' || file === 'kodlarim.txt' || file === 'test_output.txt')) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// PERFORMANS & GÜVENLİK: Belirlenen boyuttan büyük dosyaları atlar
|
|
81
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
82
|
+
console.warn(`⚠️ Skipped large file (over 500KB): ${relativePath}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// GÜVENLİK: İkili (Resim, PDF, Derlenmiş Dosya vb.) içerikleri filtreler
|
|
86
|
+
if (isBinaryFile(fullPath)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
91
|
+
const safeContent = maskSensitiveData(content);
|
|
92
|
+
stats.totalFiles += 1;
|
|
93
|
+
stats.totalLines += safeContent.split('\n').length;
|
|
94
|
+
stats.totalChars += safeContent.length;
|
|
95
|
+
combinedContent += `\n--- START OF FILE: ${relativePath} ---\n`;
|
|
96
|
+
combinedContent += safeContent;
|
|
97
|
+
combinedContent += `\n--- END OF FILE: ${relativePath} ---\n`;
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
// Okunamayan dosyaları es geç
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
scan(dirPath);
|
|
106
|
+
stats.estimatedTokens = Math.ceil(stats.totalChars / 4);
|
|
107
|
+
return { combinedContent, stats };
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "context-squeezer-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight CLI that packs a repository into a single, LLM-optimized context file.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"context-squeezer": "bin/context-squeezer.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "tsc && node ./dist/index.js",
|
|
19
|
+
"test": "node ./scripts/run-tests.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"cli",
|
|
23
|
+
"llm",
|
|
24
|
+
"context",
|
|
25
|
+
"tokenizer",
|
|
26
|
+
"ai",
|
|
27
|
+
"devtools"
|
|
28
|
+
],
|
|
29
|
+
"author": "Erhan Namal",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/ErhanNamal/context-squeezer.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/ErhanNamal/context-squeezer/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/ErhanNamal/context-squeezer#readme",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.11.0",
|
|
41
|
+
"typescript": "^5.3.3"
|
|
42
|
+
}
|
|
43
|
+
}
|