@sitrozyi/repomix-semantic-compressor 0.1.2
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 +105 -0
- package/bin/cli.mjs +16 -0
- package/bin/mcp-server.mjs +8 -0
- package/package.json +65 -0
- package/src/ast.mjs +489 -0
- package/src/core.mjs +502 -0
- package/src/extractor.mjs +89 -0
- package/src/imports.mjs +228 -0
- package/src/mcp.mjs +223 -0
- package/src/optimizers.mjs +870 -0
- package/src/worker.mjs +28 -0
package/src/core.mjs
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { Worker } from 'node:worker_threads';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { getEncoding } from 'js-tiktoken';
|
|
8
|
+
import {
|
|
9
|
+
optimizeHTML,
|
|
10
|
+
summarizeCSS,
|
|
11
|
+
processJSON,
|
|
12
|
+
optimizeSQL,
|
|
13
|
+
optimizeYAML,
|
|
14
|
+
optimizeDockerfile,
|
|
15
|
+
optimizeMarkdown,
|
|
16
|
+
skeletonizePython,
|
|
17
|
+
skeletonizeGo
|
|
18
|
+
} from './optimizers.mjs';
|
|
19
|
+
import { skeletonizeWithAST } from './ast.mjs';
|
|
20
|
+
import { extractFiles } from './extractor.mjs';
|
|
21
|
+
import { extractImports, resolveLocalImportPath, summarizeExports } from './imports.mjs';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
optimizeHTML,
|
|
25
|
+
summarizeCSS,
|
|
26
|
+
processJSON,
|
|
27
|
+
optimizeSQL,
|
|
28
|
+
optimizeYAML,
|
|
29
|
+
optimizeDockerfile,
|
|
30
|
+
optimizeMarkdown,
|
|
31
|
+
skeletonizePython,
|
|
32
|
+
skeletonizeGo
|
|
33
|
+
} from './optimizers.mjs';
|
|
34
|
+
export { skeletonizeWithAST, extractProtocolsFromAST } from './ast.mjs';
|
|
35
|
+
export { extractFiles } from './extractor.mjs';
|
|
36
|
+
export { extractImports, resolveLocalImportPath, summarizeExports } from './imports.mjs';
|
|
37
|
+
|
|
38
|
+
// --- CLI Configuration Specifications ---
|
|
39
|
+
const CLI_OPTIONS = {
|
|
40
|
+
input: { type: 'string', short: 'i' },
|
|
41
|
+
output: { type: 'string', short: 'o', default: 'repomix-optimized.md' },
|
|
42
|
+
'max-preserve-lines': { type: 'string', short: 'm', default: '8' },
|
|
43
|
+
focus: { type: 'string', short: 'f' },
|
|
44
|
+
'exact-tokens': { type: 'boolean', short: 'e', default: false },
|
|
45
|
+
'auto-pack': { type: 'boolean', default: true },
|
|
46
|
+
'no-auto-pack': { type: 'boolean', default: false },
|
|
47
|
+
help: { type: 'boolean', short: 'h' }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// --- Terminal Formatting Utilities ---
|
|
51
|
+
const styles = {
|
|
52
|
+
green: (t) => `\x1b[32m${t}\x1b[0m`,
|
|
53
|
+
cyan: (t) => `\x1b[36m${t}\x1b[0m`,
|
|
54
|
+
bold: (t) => `\x1b[1m${t}\x1b[0m`,
|
|
55
|
+
gray: (t) => `\x1b[90m${t}\x1b[0m`,
|
|
56
|
+
yellow: (t) => `\x1b[33m${t}\x1b[0m`,
|
|
57
|
+
magenta: (t) => `\x1b[35m${t}\x1b[0m`
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts raw byte size to a human-readable string (kB, MB, etc.).
|
|
62
|
+
* @param {number} bytes
|
|
63
|
+
* @returns {string} Formatted size string
|
|
64
|
+
*/
|
|
65
|
+
function formatBytes(bytes) {
|
|
66
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
67
|
+
const k = 1024;
|
|
68
|
+
const units = ['B', 'kB', 'MB', 'GB'];
|
|
69
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
70
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i]}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Formats LLM token count with thousands separator.
|
|
75
|
+
* @param {number} tokens
|
|
76
|
+
* @returns {string} Formatted token count
|
|
77
|
+
*/
|
|
78
|
+
export function formatTokens(tokens) {
|
|
79
|
+
return `${(tokens || 0).toLocaleString()}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let tokenizerInstance = null;
|
|
83
|
+
/**
|
|
84
|
+
* Counts LLM tokens. Uses fast byte-length approximation by default (~0ms),
|
|
85
|
+
* or exact cl100k_base BPE tokenization when exact === true.
|
|
86
|
+
* @param {string} text
|
|
87
|
+
* @param {boolean} [exact=false]
|
|
88
|
+
* @returns {number} Token count
|
|
89
|
+
*/
|
|
90
|
+
export function countTokens(text, exact = false) {
|
|
91
|
+
if (!text || typeof text !== 'string') return 0;
|
|
92
|
+
if (!exact) {
|
|
93
|
+
return Math.round(Buffer.byteLength(text, 'utf-8') / 3.8);
|
|
94
|
+
}
|
|
95
|
+
if (!tokenizerInstance) {
|
|
96
|
+
tokenizerInstance = getEncoding('cl100k_base');
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
return tokenizerInstance.encode(text).length;
|
|
100
|
+
} catch {
|
|
101
|
+
return Math.round(Buffer.byteLength(text, 'utf-8') / 3.8);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolves CLI arguments and determines input/output paths safely.
|
|
106
|
+
* @returns {{ inputFile: string, outputFile: string, maxPreserveLines: number }}
|
|
107
|
+
*/
|
|
108
|
+
export function resolveConfig() {
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = parseArgs({ options: CLI_OPTIONS, allowPositionals: true }).values;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`error: ${err.message}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (parsed.help) {
|
|
118
|
+
const bold = '\x1b[1m';
|
|
119
|
+
const cyan = '\x1b[36m';
|
|
120
|
+
const underline = '\x1b[4m';
|
|
121
|
+
const dim = '\x1b[2m';
|
|
122
|
+
const reset = '\x1b[0m';
|
|
123
|
+
|
|
124
|
+
console.log(`
|
|
125
|
+
${bold}Repomix Semantic Compressor (AST-Powered)${reset}
|
|
126
|
+
A semantic post-processor for Repomix (${cyan}${underline}https://repomix.com${reset})
|
|
127
|
+
|
|
128
|
+
${bold}Usage:${reset}
|
|
129
|
+
npx repomix-compress [options]
|
|
130
|
+
node bin/cli.mjs [options]
|
|
131
|
+
|
|
132
|
+
${bold}Options:${reset}
|
|
133
|
+
-i, --input <path> Input Repomix file (auto-detects .xml or .json)
|
|
134
|
+
-o, --output <path> Output optimized file (default: repomix-optimized.md)
|
|
135
|
+
-f, --focus <pattern> Retain full implementation for matched path/module
|
|
136
|
+
-m, --max-preserve-lines <num> Max lines to preserve full function body (default: 8)
|
|
137
|
+
-e, --exact-tokens Use exact BPE tokenizer (slower, default: false)
|
|
138
|
+
--no-auto-pack Disable automatic repomix execution if artifact is missing
|
|
139
|
+
-h, --help Show CLI help and exit
|
|
140
|
+
|
|
141
|
+
${bold}Examples:${reset}
|
|
142
|
+
${dim}$${reset} npx repomix-compress
|
|
143
|
+
${dim}$${reset} npx repomix-compress -f src/auth -o auth-context.md
|
|
144
|
+
${dim}$${reset} npx repomix-compress -i repomix-output.xml -o context.md
|
|
145
|
+
`);
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const autoPack = parsed['no-auto-pack'] ? false : (parsed['auto-pack'] ?? true);
|
|
150
|
+
let inputFile = parsed.input;
|
|
151
|
+
if (!inputFile) {
|
|
152
|
+
inputFile = findDefaultInputFile(autoPack);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const maxPreserveLines = Number.parseInt(parsed['max-preserve-lines'], 10);
|
|
156
|
+
if (Number.isNaN(maxPreserveLines) || maxPreserveLines < 0) {
|
|
157
|
+
console.error(`error: --max-preserve-lines must be a non-negative integer.`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
inputFile,
|
|
163
|
+
outputFile: parsed.output,
|
|
164
|
+
maxPreserveLines,
|
|
165
|
+
focus: parsed.focus || null,
|
|
166
|
+
exactTokens: Boolean(parsed['exact-tokens']),
|
|
167
|
+
autoPack
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Finds default repomix output file or automatically runs repomix by default.
|
|
173
|
+
* @param {boolean} [autoPack=true]
|
|
174
|
+
* @param {string} [baseDir='.']
|
|
175
|
+
* @returns {string} Path to input file
|
|
176
|
+
*/
|
|
177
|
+
export function findDefaultInputFile(autoPack = true, baseDir = '.', silent = false) {
|
|
178
|
+
const xmlPath = path.normalize(path.join(baseDir, 'repomix-output.xml')).replace(/\\/g, '/');
|
|
179
|
+
const jsonPath = path.normalize(path.join(baseDir, 'repomix-output.json')).replace(/\\/g, '/');
|
|
180
|
+
|
|
181
|
+
if (fs.existsSync(xmlPath)) return xmlPath;
|
|
182
|
+
if (fs.existsSync(jsonPath)) return jsonPath;
|
|
183
|
+
|
|
184
|
+
if (!autoPack) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
'Repomix output file not found (repomix-output.xml or repomix-output.json).\n' +
|
|
187
|
+
'Please run "npx repomix" first, or omit --no-auto-pack to pack automatically.'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const packStart = performance.now();
|
|
192
|
+
// Route logs to stderr to avoid corrupting MCP JSON-RPC stdio protocol
|
|
193
|
+
if (!silent) {
|
|
194
|
+
process.stderr.write(`${styles.cyan('ℹ')} repomix-output not found. Running "npx repomix" automatically... `);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
execSync('npx repomix', {
|
|
199
|
+
stdio: 'pipe',
|
|
200
|
+
timeout: 30000,
|
|
201
|
+
maxBuffer: 10 * 1024 * 1024
|
|
202
|
+
});
|
|
203
|
+
const packDuration = ((performance.now() - packStart) / 1000).toFixed(1);
|
|
204
|
+
if (!silent) {
|
|
205
|
+
process.stderr.write(`${styles.gray(`(done in ${packDuration}s)\n`)}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (fs.existsSync(xmlPath)) return xmlPath;
|
|
209
|
+
if (fs.existsSync(jsonPath)) return jsonPath;
|
|
210
|
+
if (fs.existsSync('repomix-output.xml')) return 'repomix-output.xml';
|
|
211
|
+
if (fs.existsSync('repomix-output.json')) return 'repomix-output.json';
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (!silent) {
|
|
214
|
+
process.stderr.write('\n');
|
|
215
|
+
}
|
|
216
|
+
const errMsg = err.stderr ? err.stderr.toString().trim() : err.message;
|
|
217
|
+
throw new Error(`Auto-running "npx repomix" failed: ${errMsg}`);
|
|
218
|
+
}
|
|
219
|
+
throw new Error('Repomix completed but neither repomix-output.xml nor repomix-output.json was found.');
|
|
220
|
+
}
|
|
221
|
+
export function writeToStream(stream, chunk) {
|
|
222
|
+
if (!stream.write(chunk)) {
|
|
223
|
+
return new Promise((resolve) => stream.once('drain', resolve));
|
|
224
|
+
}
|
|
225
|
+
return Promise.resolve();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function transformFileContent(filePath, originalCode, maxPreserveLines) {
|
|
229
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
230
|
+
const baseName = path.basename(filePath).toLowerCase();
|
|
231
|
+
// Normalize Windows CRLF to standard LF
|
|
232
|
+
const normalizedCode = originalCode.replace(/\r\n/g, '\n');
|
|
233
|
+
let processedCode = '';
|
|
234
|
+
|
|
235
|
+
const isDockerfile = baseName === 'dockerfile' || baseName.startsWith('dockerfile.');
|
|
236
|
+
|
|
237
|
+
if (isDockerfile) {
|
|
238
|
+
processedCode = optimizeDockerfile(normalizedCode);
|
|
239
|
+
} else if (['.css', '.scss', '.less'].includes(ext)) {
|
|
240
|
+
processedCode = summarizeCSS(normalizedCode);
|
|
241
|
+
} else if (ext === '.html' || ext === '.htm') {
|
|
242
|
+
processedCode = optimizeHTML(normalizedCode);
|
|
243
|
+
} else if (ext === '.json') {
|
|
244
|
+
processedCode = processJSON(normalizedCode);
|
|
245
|
+
} else if (ext === '.sql') {
|
|
246
|
+
processedCode = optimizeSQL(normalizedCode);
|
|
247
|
+
} else if (ext === '.yml' || ext === '.yaml') {
|
|
248
|
+
processedCode = optimizeYAML(normalizedCode);
|
|
249
|
+
} else if (ext === '.md' || ext === '.mdx') {
|
|
250
|
+
processedCode = optimizeMarkdown(normalizedCode);
|
|
251
|
+
} else if (['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx'].includes(ext)) {
|
|
252
|
+
const isTS = ['.ts', '.mts', '.cts', '.tsx'].includes(ext);
|
|
253
|
+
const isJSX = ['.jsx', '.tsx'].includes(ext);
|
|
254
|
+
processedCode = skeletonizeWithAST(normalizedCode, isTS, maxPreserveLines, isJSX);
|
|
255
|
+
} else if (ext === '.py') {
|
|
256
|
+
processedCode = skeletonizePython(normalizedCode, maxPreserveLines);
|
|
257
|
+
} else if (ext === '.go') {
|
|
258
|
+
processedCode = skeletonizeGo(normalizedCode, maxPreserveLines);
|
|
259
|
+
} else {
|
|
260
|
+
processedCode = normalizedCode;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
ext,
|
|
265
|
+
code: processedCode.replace(/(?:\r?\n){3,}/g, '\n\n').trim()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Transforms repository files in parallel using worker threads if file count exceeds threshold.
|
|
272
|
+
* Preserves original index order deterministically.
|
|
273
|
+
* @param {{ path: string, content: string }[]} files
|
|
274
|
+
* @param {number} maxPreserveLines
|
|
275
|
+
* @returns {Promise<{ path: string, ext: string, code: string }[]>}
|
|
276
|
+
*/
|
|
277
|
+
export async function transformFilesParallel(files, maxPreserveLines = 8) {
|
|
278
|
+
if (!files || files.length === 0) return [];
|
|
279
|
+
|
|
280
|
+
// Fallback to single thread for small batches to avoid worker initialization overhead
|
|
281
|
+
if (files.length <= 20) {
|
|
282
|
+
return files.map((f) => {
|
|
283
|
+
const transformed = transformFileContent(f.path, f.content, maxPreserveLines);
|
|
284
|
+
return {
|
|
285
|
+
path: f.path,
|
|
286
|
+
ext: transformed.ext,
|
|
287
|
+
code: transformed.code
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const numCPUs = os.cpus()?.length || 4;
|
|
293
|
+
const workerCount = Math.min(4, Math.min(numCPUs, Math.ceil(files.length / 10)));
|
|
294
|
+
const chunkSize = Math.ceil(files.length / workerCount);
|
|
295
|
+
|
|
296
|
+
const chunks = [];
|
|
297
|
+
for (let i = 0; i < workerCount; i++) {
|
|
298
|
+
const start = i * chunkSize;
|
|
299
|
+
const end = Math.min(start + chunkSize, files.length);
|
|
300
|
+
if (start < end) {
|
|
301
|
+
const items = files.slice(start, end).map((file, localIdx) => ({
|
|
302
|
+
index: start + localIdx,
|
|
303
|
+
path: file.path,
|
|
304
|
+
content: file.content
|
|
305
|
+
}));
|
|
306
|
+
chunks.push(items);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const workerUrl = new URL('./worker.mjs', import.meta.url);
|
|
311
|
+
const activeWorkers = [];
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// Fall back to sequential execution on the main thread if any worker fails
|
|
315
|
+
const workerPromises = chunks.map((chunk) => {
|
|
316
|
+
return new Promise((resolve) => {
|
|
317
|
+
const worker = new Worker(workerUrl, {
|
|
318
|
+
workerData: { items: chunk, maxPreserveLines }
|
|
319
|
+
});
|
|
320
|
+
activeWorkers.push(worker);
|
|
321
|
+
worker.on('message', (results) => resolve(results));
|
|
322
|
+
worker.on('error', () => {
|
|
323
|
+
// Fallback chunk execution on main thread
|
|
324
|
+
const fallback = chunk.map((item) => {
|
|
325
|
+
const transformed = transformFileContent(item.path, item.content, maxPreserveLines);
|
|
326
|
+
return { index: item.index, path: item.path, ext: transformed.ext, code: transformed.code };
|
|
327
|
+
});
|
|
328
|
+
resolve(fallback);
|
|
329
|
+
});
|
|
330
|
+
worker.on('exit', (code) => {
|
|
331
|
+
if (code !== 0) {
|
|
332
|
+
const fallback = chunk.map((item) => {
|
|
333
|
+
const transformed = transformFileContent(item.path, item.content, maxPreserveLines);
|
|
334
|
+
return { index: item.index, path: item.path, ext: transformed.ext, code: transformed.code };
|
|
335
|
+
});
|
|
336
|
+
resolve(fallback);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const chunkResults = await Promise.all(workerPromises);
|
|
343
|
+
const allResults = new Array(files.length);
|
|
344
|
+
for (const resList of chunkResults) {
|
|
345
|
+
for (const res of resList) {
|
|
346
|
+
allResults[res.index] = {
|
|
347
|
+
path: res.path,
|
|
348
|
+
ext: res.ext,
|
|
349
|
+
code: res.code
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return allResults;
|
|
355
|
+
} finally {
|
|
356
|
+
await Promise.allSettled(activeWorkers.map((w) => w.terminate()));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const TYPE_CONTRACT_REGEX = /(?:^|[\\/])(?:types?|interfaces?|models?|schemas?|constants?|contracts?|entities?)(?:[\\/.]|\.d\.ts$)/i;
|
|
361
|
+
|
|
362
|
+
function isTypeOrContractDefinition(filePath) {
|
|
363
|
+
return TYPE_CONTRACT_REGEX.test(filePath) || filePath.endsWith('.d.ts');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Compresses an array of repository files with transitive dependency resolution.
|
|
368
|
+
* @param {{ path: string, content: string }[]} files
|
|
369
|
+
* @param {{ focus?: string|null, maxPreserveLines?: number }} options
|
|
370
|
+
* @returns {Promise<string>}
|
|
371
|
+
*/
|
|
372
|
+
export async function compressRepository(files, options = {}) {
|
|
373
|
+
const { focus = null, maxPreserveLines = 8 } = options;
|
|
374
|
+
const sections = [
|
|
375
|
+
`[SEMANTIC REPOSITORY SKELETON CONTEXT]\n Optimized for LLM reasoning & architectural analysis.\n========================================\n\n`
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
if (focus) {
|
|
379
|
+
const fileContentMap = new Map(files.map((f) => [f.path, f.content]));
|
|
380
|
+
const focusFiles = new Set();
|
|
381
|
+
const dependencyFiles = new Set();
|
|
382
|
+
const visited = new Set();
|
|
383
|
+
const traversalQueue = [];
|
|
384
|
+
|
|
385
|
+
for (const f of files) {
|
|
386
|
+
if (f.path.includes(focus)) {
|
|
387
|
+
focusFiles.add(f.path);
|
|
388
|
+
visited.add(f.path);
|
|
389
|
+
traversalQueue.push({ path: f.path, depth: 0 });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
while (traversalQueue.length > 0) {
|
|
394
|
+
const current = traversalQueue.shift();
|
|
395
|
+
const content = fileContentMap.get(current.path);
|
|
396
|
+
if (!content) continue;
|
|
397
|
+
|
|
398
|
+
const imports = extractImports(content);
|
|
399
|
+
for (const imp of imports) {
|
|
400
|
+
const resolved = resolveLocalImportPath(current.path, imp, files);
|
|
401
|
+
if (!resolved || visited.has(resolved)) continue;
|
|
402
|
+
|
|
403
|
+
const isDirect = current.depth === 0;
|
|
404
|
+
const isTypeContract = isTypeOrContractDefinition(resolved);
|
|
405
|
+
|
|
406
|
+
if (isDirect || isTypeContract) {
|
|
407
|
+
visited.add(resolved);
|
|
408
|
+
if (!focusFiles.has(resolved)) {
|
|
409
|
+
dependencyFiles.add(resolved);
|
|
410
|
+
}
|
|
411
|
+
traversalQueue.push({ path: resolved, depth: current.depth + 1 });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const dependencyFilesList = files.filter((f) => dependencyFiles.has(f.path));
|
|
417
|
+
const transformedDependencies = await transformFilesParallel(dependencyFilesList, maxPreserveLines);
|
|
418
|
+
const transformedMap = new Map();
|
|
419
|
+
for (const item of transformedDependencies) {
|
|
420
|
+
transformedMap.set(item.path, item);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const file of files) {
|
|
424
|
+
const ext = path.extname(file.path).toLowerCase();
|
|
425
|
+
const lang = ext ? ext.replace(/^\./, '') : '';
|
|
426
|
+
|
|
427
|
+
if (focusFiles.has(file.path)) {
|
|
428
|
+
sections.push(`### File: ${file.path} [FOCUS - FULL IMPLEMENTATION]\n\`\`\`\`${lang}\n${file.content.trim()}\n\`\`\`\`\n\n`);
|
|
429
|
+
} else if (dependencyFiles.has(file.path)) {
|
|
430
|
+
const transformed = transformedMap.get(file.path) || transformFileContent(file.path, file.content, maxPreserveLines);
|
|
431
|
+
sections.push(`### File: ${file.path} [DEPENDENCY - SKELETON]\n\`\`\`\`${lang}\n${transformed.code}\n\`\`\`\`\n\n`);
|
|
432
|
+
} else {
|
|
433
|
+
const summary = summarizeExports(file.content);
|
|
434
|
+
sections.push(`### File: ${file.path} [OUT OF SCOPE - SUMMARY]\n\`\`\`\`${lang}\n${summary}\n\`\`\`\`\n\n`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
const transformedList = await transformFilesParallel(files, maxPreserveLines);
|
|
439
|
+
for (const item of transformedList) {
|
|
440
|
+
const lang = item.ext ? item.ext.replace(/^\./, '') : '';
|
|
441
|
+
sections.push(`### File: ${item.path}\n\`\`\`\`${lang}\n${item.code}\n\`\`\`\`\n\n`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return sections.join('');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const ARTIFACT_IGNORE_REGEX = /repomix-(?:optimized|output)/i;
|
|
449
|
+
|
|
450
|
+
export async function main() {
|
|
451
|
+
const { inputFile, outputFile, maxPreserveLines, focus, exactTokens } = resolveConfig();
|
|
452
|
+
const startTime = performance.now();
|
|
453
|
+
|
|
454
|
+
if (!fs.existsSync(inputFile)) {
|
|
455
|
+
throw new Error(`Input artifact not found: ${inputFile}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const rawContent = fs.readFileSync(inputFile, 'utf-8');
|
|
459
|
+
const inputBytes = Buffer.byteLength(rawContent, 'utf-8');
|
|
460
|
+
const inputTokens = countTokens(rawContent, exactTokens);
|
|
461
|
+
|
|
462
|
+
const rawFiles = extractFiles(rawContent, inputFile);
|
|
463
|
+
const files = rawFiles.filter((f) => !ARTIFACT_IGNORE_REGEX.test(f.path));
|
|
464
|
+
const optimizedContent = await compressRepository(files, { focus, maxPreserveLines });
|
|
465
|
+
|
|
466
|
+
// Stream output to disk with backpressure handling to prevent memory spikes
|
|
467
|
+
const writeStream = fs.createWriteStream(outputFile, { encoding: 'utf-8' });
|
|
468
|
+
await writeToStream(writeStream, optimizedContent);
|
|
469
|
+
await new Promise((resolve) => writeStream.end(resolve));
|
|
470
|
+
|
|
471
|
+
const outputRaw = fs.readFileSync(outputFile, 'utf-8');
|
|
472
|
+
const outputBytes = Buffer.byteLength(outputRaw, 'utf-8');
|
|
473
|
+
const outputTokens = countTokens(outputRaw, exactTokens);
|
|
474
|
+
const duration = Math.round(performance.now() - startTime);
|
|
475
|
+
|
|
476
|
+
const byteReduction = (((inputBytes - outputBytes) / inputBytes) * 100).toFixed(1);
|
|
477
|
+
const tokenReduction = (((inputTokens - outputTokens) / inputTokens) * 100).toFixed(1);
|
|
478
|
+
|
|
479
|
+
const savedTokens = Math.max(0, inputTokens - outputTokens);
|
|
480
|
+
const estimatedSavings = ((savedTokens * 3.0) / 1_000_000).toFixed(4);
|
|
481
|
+
|
|
482
|
+
const tokenModeLabel = exactTokens ? 'LLM Tokens:' : 'LLM Tokens ~:';
|
|
483
|
+
const labelTokens = tokenModeLabel.padEnd(14);
|
|
484
|
+
const labelSize = 'File Size:'.padEnd(14);
|
|
485
|
+
const labelSavings = 'Est. Savings:'.padEnd(14);
|
|
486
|
+
const labelFocus = 'Focus Filter:'.padEnd(14);
|
|
487
|
+
const labelOutput = 'Output File:'.padEnd(14);
|
|
488
|
+
|
|
489
|
+
const inTokStr = `${formatTokens(inputTokens)}`.padStart(9);
|
|
490
|
+
const outTokStr = `${formatTokens(outputTokens)}`.padStart(8);
|
|
491
|
+
const inByteStr = formatBytes(inputBytes).padStart(9);
|
|
492
|
+
const outByteStr = formatBytes(outputBytes).padStart(8);
|
|
493
|
+
|
|
494
|
+
console.log(`\n${styles.green('✔')} ${styles.bold(`Optimized ${files.length} files in ${duration}ms`)}\n`);
|
|
495
|
+
console.log(` ${styles.gray(labelTokens)} ${inTokStr} ${styles.gray('→')} ${styles.cyan(outTokStr)} ${styles.green(`(-${tokenReduction}%)`)}`);
|
|
496
|
+
console.log(` ${styles.gray(labelSize)} ${inByteStr} ${styles.gray('→')} ${styles.cyan(outByteStr)} ${styles.green(`(-${byteReduction}%)`)}`);
|
|
497
|
+
console.log(` ${styles.gray(labelSavings)} ${styles.yellow(`~$${estimatedSavings} / prompt`)} ${styles.gray('(Claude 3.5 Sonnet / GPT-4o input rate)')}`);
|
|
498
|
+
if (focus) {
|
|
499
|
+
console.log(` ${styles.gray(labelFocus)} ${styles.magenta(focus)}`);
|
|
500
|
+
}
|
|
501
|
+
console.log(` ${styles.gray(labelOutput)} ${styles.bold(outputFile)}\n`);
|
|
502
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely decodes XML entities in raw extracted file contents.
|
|
5
|
+
* @param {string} str
|
|
6
|
+
* @returns {string} Decoded string
|
|
7
|
+
*/
|
|
8
|
+
function decodeXMLEntities(str) {
|
|
9
|
+
if (typeof str !== 'string') return '';
|
|
10
|
+
return str
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'|'/g, "'")
|
|
15
|
+
.replace(/&/g, '&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function extractFiles(rawContent, filePath) {
|
|
19
|
+
if (filePath.endsWith('.json') || rawContent.trim().startsWith('{')) {
|
|
20
|
+
try {
|
|
21
|
+
const data = JSON.parse(rawContent);
|
|
22
|
+
const results = [];
|
|
23
|
+
if (data.files) {
|
|
24
|
+
if (Array.isArray(data.files)) {
|
|
25
|
+
for (const f of data.files) {
|
|
26
|
+
results.push({ path: f.path || 'unknown', content: f.content || '' });
|
|
27
|
+
}
|
|
28
|
+
} else if (typeof data.files === 'object') {
|
|
29
|
+
for (const [p, val] of Object.entries(data.files)) {
|
|
30
|
+
results.push({ path: p, content: typeof val === 'string' ? val : val.content || '' });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (results.length > 0) return results;
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const parser = new XMLParser({
|
|
40
|
+
ignoreAttributes: false,
|
|
41
|
+
attributeNamePrefix: '@_',
|
|
42
|
+
stopNodes: ['*.file', 'file', 'files.file', 'root.files.file', 'root.file'],
|
|
43
|
+
processEntities: true,
|
|
44
|
+
htmlEntities: true,
|
|
45
|
+
trimValues: false
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const sanitizedXml = rawContent.replace(/<\?xml[\s\S]*?\?>/gi, '').trim();
|
|
49
|
+
const wrappedXml = `<root>${sanitizedXml}</root>`;
|
|
50
|
+
const parsed = parser.parse(wrappedXml);
|
|
51
|
+
const results = [];
|
|
52
|
+
|
|
53
|
+
const collectFiles = (node) => {
|
|
54
|
+
if (!node) return;
|
|
55
|
+
if (Array.isArray(node)) {
|
|
56
|
+
for (const item of node) collectFiles(item);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (typeof node === 'object') {
|
|
60
|
+
if (node.file) {
|
|
61
|
+
const files = Array.isArray(node.file) ? node.file : [node.file];
|
|
62
|
+
for (const f of files) {
|
|
63
|
+
if (typeof f === 'object') {
|
|
64
|
+
const filePathAttr = f['@_path'] || f.path || 'unknown';
|
|
65
|
+
let fileContent = typeof f['#text'] === 'string' ? f['#text'] : (typeof f === 'string' ? f : '');
|
|
66
|
+
results.push({ path: filePathAttr, content: decodeXMLEntities(fileContent) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const key of Object.keys(node)) {
|
|
71
|
+
if (key !== 'file' && typeof node[key] === 'object') {
|
|
72
|
+
collectFiles(node[key]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
collectFiles(parsed.root || parsed);
|
|
79
|
+
if (results.length > 0) return results;
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
const fileRegex = /<file\s+path="([^"]+)">([\s\S]*?)<\/file>/g;
|
|
83
|
+
const results = [];
|
|
84
|
+
let m;
|
|
85
|
+
while ((m = fileRegex.exec(rawContent)) !== null) {
|
|
86
|
+
results.push({ path: m[1], content: decodeXMLEntities(m[2]) });
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|