@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/imports.mjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { parse } from '@babel/parser';
|
|
3
|
+
import traversePkg from '@babel/traverse';
|
|
4
|
+
|
|
5
|
+
const traverse = traversePkg.default || traversePkg;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts import and require paths from source code using AST parsing with regex fallback.
|
|
9
|
+
* @param {string} code
|
|
10
|
+
* @returns {string[]}
|
|
11
|
+
*/
|
|
12
|
+
export function extractImports(code) {
|
|
13
|
+
const importedPaths = new Set();
|
|
14
|
+
try {
|
|
15
|
+
const ast = parse(code, {
|
|
16
|
+
sourceType: 'unambiguous',
|
|
17
|
+
errorRecovery: true,
|
|
18
|
+
plugins: [
|
|
19
|
+
'jsx',
|
|
20
|
+
'typescript',
|
|
21
|
+
['decorators', { decoratorsBeforeExport: true }],
|
|
22
|
+
'decoratorAutoAccessors',
|
|
23
|
+
'explicitResourceManagement',
|
|
24
|
+
'classProperties',
|
|
25
|
+
'classPrivateProperties',
|
|
26
|
+
'classPrivateMethods',
|
|
27
|
+
'classStaticBlock',
|
|
28
|
+
'dynamicImport',
|
|
29
|
+
'exportDefaultFrom',
|
|
30
|
+
'importAttributes'
|
|
31
|
+
]
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
traverse(ast, {
|
|
35
|
+
ImportDeclaration(importPath) {
|
|
36
|
+
if (importPath.node.source && importPath.node.source.value) {
|
|
37
|
+
importedPaths.add(importPath.node.source.value);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
ExportNamedDeclaration(exportPath) {
|
|
41
|
+
if (exportPath.node.source && exportPath.node.source.value) {
|
|
42
|
+
importedPaths.add(exportPath.node.source.value);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
ExportAllDeclaration(exportPath) {
|
|
46
|
+
if (exportPath.node.source && exportPath.node.source.value) {
|
|
47
|
+
importedPaths.add(exportPath.node.source.value);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
CallExpression(callPath) {
|
|
51
|
+
const callee = callPath.node.callee;
|
|
52
|
+
if (callee.type === 'Identifier' && callee.name === 'require') {
|
|
53
|
+
const arg = callPath.node.arguments[0];
|
|
54
|
+
if (arg && arg.type === 'StringLiteral') {
|
|
55
|
+
importedPaths.add(arg.value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (callee.type === 'Import') {
|
|
59
|
+
const arg = callPath.node.arguments[0];
|
|
60
|
+
if (arg && arg.type === 'StringLiteral') {
|
|
61
|
+
importedPaths.add(arg.value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return Array.from(importedPaths);
|
|
68
|
+
} catch {
|
|
69
|
+
const importRegex = /(?:import\s+(?:[^\n\r;]+?from\s+)?['"]([^'"]+)['"]|export\s+[^\n\r;]+?from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)|from\s+([.\w]+)\s+import|import\s+([.\w]+))/g;
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
72
|
+
const p = match[1] || match[2] || match[3] || match[4] || match[5];
|
|
73
|
+
if (p) importedPaths.add(p);
|
|
74
|
+
}
|
|
75
|
+
return Array.from(importedPaths);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const RESOLVABLE_EXTENSIONS = [
|
|
80
|
+
'',
|
|
81
|
+
'.ts',
|
|
82
|
+
'.tsx',
|
|
83
|
+
'.d.ts',
|
|
84
|
+
'.js',
|
|
85
|
+
'.jsx',
|
|
86
|
+
'.mjs',
|
|
87
|
+
'.cjs',
|
|
88
|
+
'.mts',
|
|
89
|
+
'.cts',
|
|
90
|
+
'.json',
|
|
91
|
+
'.py',
|
|
92
|
+
'/__init__.py',
|
|
93
|
+
'.go',
|
|
94
|
+
'/index.ts',
|
|
95
|
+
'/index.tsx',
|
|
96
|
+
'/index.d.ts',
|
|
97
|
+
'/index.js',
|
|
98
|
+
'/index.jsx',
|
|
99
|
+
'/index.mjs',
|
|
100
|
+
'/index.json'
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolves relative, aliased, subpath, and monorepo workspace package imports.
|
|
105
|
+
* @param {string} fromFilePath
|
|
106
|
+
* @param {string} importPath
|
|
107
|
+
* @param {{ path: string }[]} allFiles
|
|
108
|
+
* @returns {string|null}
|
|
109
|
+
*/
|
|
110
|
+
export function resolveLocalImportPath(fromFilePath, importPath, allFiles) {
|
|
111
|
+
const fileMap = new Map();
|
|
112
|
+
for (const f of allFiles) {
|
|
113
|
+
const normalized = path.normalize(f.path).replace(/\\/g, '/');
|
|
114
|
+
fileMap.set(normalized, f.path);
|
|
115
|
+
fileMap.set(normalized.toLowerCase(), f.path);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const normalizedFromFile = path.normalize(fromFilePath).replace(/\\/g, '/');
|
|
119
|
+
const candidateBases = [];
|
|
120
|
+
|
|
121
|
+
if (importPath.startsWith('.')) {
|
|
122
|
+
const currentDir = path.dirname(normalizedFromFile);
|
|
123
|
+
candidateBases.push(path.normalize(path.join(currentDir, importPath)).replace(/\\/g, '/'));
|
|
124
|
+
} else if (importPath.startsWith('@/') || importPath.startsWith('~/')) {
|
|
125
|
+
const subPath = importPath.slice(2);
|
|
126
|
+
candidateBases.push(path.normalize(subPath).replace(/\\/g, '/'));
|
|
127
|
+
candidateBases.push(path.normalize(path.join('src', subPath)).replace(/\\/g, '/'));
|
|
128
|
+
} else if (importPath.startsWith('#')) {
|
|
129
|
+
const subPath = importPath.slice(1);
|
|
130
|
+
candidateBases.push(path.normalize(subPath).replace(/\\/g, '/'));
|
|
131
|
+
candidateBases.push(path.normalize(path.join('src', subPath)).replace(/\\/g, '/'));
|
|
132
|
+
} else {
|
|
133
|
+
candidateBases.push(path.normalize(importPath).replace(/\\/g, '/'));
|
|
134
|
+
candidateBases.push(path.normalize(path.join('src', importPath)).replace(/\\/g, '/'));
|
|
135
|
+
candidateBases.push(path.normalize(path.join('packages', importPath)).replace(/\\/g, '/'));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const base of candidateBases) {
|
|
139
|
+
for (const ext of RESOLVABLE_EXTENSIONS) {
|
|
140
|
+
const candidate = `${base}${ext}`;
|
|
141
|
+
if (fileMap.has(candidate)) {
|
|
142
|
+
return fileMap.get(candidate);
|
|
143
|
+
}
|
|
144
|
+
const lower = candidate.toLowerCase();
|
|
145
|
+
if (fileMap.has(lower)) {
|
|
146
|
+
return fileMap.get(lower);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Summarizes top-level exports for out-of-scope non-focused files using AST analysis.
|
|
156
|
+
* @param {string} code
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
export function summarizeExports(code) {
|
|
160
|
+
const exports = new Set();
|
|
161
|
+
try {
|
|
162
|
+
const ast = parse(code, {
|
|
163
|
+
sourceType: 'unambiguous',
|
|
164
|
+
errorRecovery: true,
|
|
165
|
+
plugins: [
|
|
166
|
+
'jsx',
|
|
167
|
+
'typescript',
|
|
168
|
+
['decorators', { decoratorsBeforeExport: true }],
|
|
169
|
+
'decoratorAutoAccessors',
|
|
170
|
+
'explicitResourceManagement',
|
|
171
|
+
'classProperties',
|
|
172
|
+
'classPrivateProperties',
|
|
173
|
+
'classPrivateMethods',
|
|
174
|
+
'classStaticBlock',
|
|
175
|
+
'dynamicImport',
|
|
176
|
+
'exportDefaultFrom',
|
|
177
|
+
'importAttributes'
|
|
178
|
+
]
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
traverse(ast, {
|
|
182
|
+
ExportNamedDeclaration(exportPath) {
|
|
183
|
+
if (exportPath.node.declaration) {
|
|
184
|
+
const decl = exportPath.node.declaration;
|
|
185
|
+
if (decl.id && decl.id.name) {
|
|
186
|
+
exports.add(decl.id.name);
|
|
187
|
+
} else if (decl.declarations && Array.isArray(decl.declarations)) {
|
|
188
|
+
for (const d of decl.declarations) {
|
|
189
|
+
if (d.id && d.id.type === 'Identifier') {
|
|
190
|
+
exports.add(d.id.name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (exportPath.node.specifiers && Array.isArray(exportPath.node.specifiers)) {
|
|
196
|
+
for (const spec of exportPath.node.specifiers) {
|
|
197
|
+
if (spec.exported && spec.exported.name) {
|
|
198
|
+
exports.add(spec.exported.name);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
ExportDefaultDeclaration(exportPath) {
|
|
204
|
+
const decl = exportPath.node.declaration;
|
|
205
|
+
if (decl.id && decl.id.name) {
|
|
206
|
+
exports.add(`default (${decl.id.name})`);
|
|
207
|
+
} else {
|
|
208
|
+
exports.add('default');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (exports.size > 0) {
|
|
214
|
+
return `// Exported signatures: ${Array.from(exports).join(', ')}\n// [Non-focused implementation omitted]`;
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
const exportRegex = /export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([a-zA-Z0-9_$]+)/g;
|
|
218
|
+
let m;
|
|
219
|
+
while ((m = exportRegex.exec(code)) !== null) {
|
|
220
|
+
exports.add(m[1]);
|
|
221
|
+
}
|
|
222
|
+
if (exports.size > 0) {
|
|
223
|
+
return `// Exported signatures: ${Array.from(exports).join(', ')}\n// [Non-focused implementation omitted]`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return `// [Non-focused implementation omitted]`;
|
|
228
|
+
}
|
package/src/mcp.mjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { extractFiles, compressRepository, findDefaultInputFile } from './core.mjs';
|
|
7
|
+
|
|
8
|
+
export function sanitizeInputPath(targetPath, baseDir = process.cwd()) {
|
|
9
|
+
const resolvedBase = path.resolve(baseDir);
|
|
10
|
+
const resolvedTarget = path.resolve(baseDir, targetPath);
|
|
11
|
+
const rel = path.relative(resolvedBase, resolvedTarget);
|
|
12
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
13
|
+
throw new Error('Access denied: Path traversal detected outside root directory.');
|
|
14
|
+
}
|
|
15
|
+
return resolvedTarget;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function errorResult(message) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
21
|
+
isError: true
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveInput(args = {}) {
|
|
26
|
+
const inputFile = args.input
|
|
27
|
+
? sanitizeInputPath(args.input)
|
|
28
|
+
: findDefaultInputFile(false, '.', true);
|
|
29
|
+
if (!fs.existsSync(inputFile)) {
|
|
30
|
+
throw new Error(`Repomix file not found: ${inputFile}`);
|
|
31
|
+
}
|
|
32
|
+
const rawContent = fs.readFileSync(inputFile, 'utf-8');
|
|
33
|
+
return { inputFile, rawContent };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates and configures the Repomix Compressor MCP Server instance.
|
|
38
|
+
* @returns {Server}
|
|
39
|
+
*/
|
|
40
|
+
export function createMCPServer() {
|
|
41
|
+
const server = new Server(
|
|
42
|
+
{
|
|
43
|
+
name: 'repomix-semantic-compressor',
|
|
44
|
+
version: '1.0.0'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
capabilities: {
|
|
48
|
+
tools: {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
54
|
+
return {
|
|
55
|
+
tools: [
|
|
56
|
+
{
|
|
57
|
+
name: 'get_repo_skeleton',
|
|
58
|
+
description: 'Retrieve the compressed semantic skeleton of the repository. Optionally pass a focus path to retain full implementation for relevant modules.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
focus: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Path pattern or module name to retain full implementation (e.g. "src/auth")'
|
|
65
|
+
},
|
|
66
|
+
input: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'Path to repomix output file (auto-detected if omitted)'
|
|
69
|
+
},
|
|
70
|
+
maxPreserveLines: {
|
|
71
|
+
type: 'number',
|
|
72
|
+
description: 'Max lines to preserve full function bodies (default: 8)'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'get_file_implementation',
|
|
79
|
+
description: 'Retrieve the uncompressed source code for a specific file from the repository artifact.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
path: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'File path to retrieve'
|
|
86
|
+
},
|
|
87
|
+
input: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Path to repomix output file (auto-detected if omitted)'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
required: ['path']
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'compress_repomix_file',
|
|
97
|
+
description: 'Compress a repomix file and write the result to a specified output path.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
input: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'Input repomix file path'
|
|
104
|
+
},
|
|
105
|
+
output: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Output optimized markdown path (default: repomix-optimized.md)'
|
|
108
|
+
},
|
|
109
|
+
focus: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: 'Focus pattern for targeted full retention'
|
|
112
|
+
},
|
|
113
|
+
maxPreserveLines: {
|
|
114
|
+
type: 'number',
|
|
115
|
+
description: 'Max lines to preserve full function bodies (default: 8)'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
125
|
+
const { name, arguments: args = {} } = request.params;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (name === 'get_repo_skeleton') {
|
|
129
|
+
let inputFile, rawContent;
|
|
130
|
+
try {
|
|
131
|
+
({ inputFile, rawContent } = resolveInput(args));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return errorResult(err.message);
|
|
134
|
+
}
|
|
135
|
+
const files = extractFiles(rawContent, inputFile);
|
|
136
|
+
const skeleton = await compressRepository(files, {
|
|
137
|
+
focus: args.focus || null,
|
|
138
|
+
maxPreserveLines: args.maxPreserveLines || 8
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: skeleton }]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (name === 'get_file_implementation') {
|
|
146
|
+
let inputFile, rawContent;
|
|
147
|
+
try {
|
|
148
|
+
({ inputFile, rawContent } = resolveInput(args));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return errorResult(err.message);
|
|
151
|
+
}
|
|
152
|
+
const files = extractFiles(rawContent, inputFile);
|
|
153
|
+
const targetPath = args.path;
|
|
154
|
+
const normTarget = path.normalize(targetPath).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
155
|
+
|
|
156
|
+
// Exact matching to prevent ambiguous resolution and path traversal
|
|
157
|
+
const matched = files.find((f) => {
|
|
158
|
+
const norm = path.normalize(f.path).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
159
|
+
return norm === normTarget;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!matched) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: 'text', text: `Error: File '${targetPath}' not found in repository artifact.` }],
|
|
165
|
+
isError: true
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: `### File: ${matched.path}\n\`\`\`\`\n${matched.content}\n\`\`\`\``
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (name === 'compress_repomix_file') {
|
|
180
|
+
let inputFile, outputFile, rawContent;
|
|
181
|
+
try {
|
|
182
|
+
({ inputFile, rawContent } = resolveInput(args));
|
|
183
|
+
outputFile = sanitizeInputPath(args.output || 'repomix-optimized.md');
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return errorResult(err.message);
|
|
186
|
+
}
|
|
187
|
+
const files = extractFiles(rawContent, inputFile);
|
|
188
|
+
const result = await compressRepository(files, {
|
|
189
|
+
focus: args.focus || null,
|
|
190
|
+
maxPreserveLines: args.maxPreserveLines || 8
|
|
191
|
+
});
|
|
192
|
+
fs.writeFileSync(outputFile, result, 'utf-8');
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: 'text',
|
|
197
|
+
text: `Successfully compressed ${files.length} files to ${outputFile}`
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
205
|
+
isError: true
|
|
206
|
+
};
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: 'text', text: `Tool execution failed: ${err.message}` }],
|
|
210
|
+
isError: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return server;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function startMCPServer() {
|
|
219
|
+
const server = createMCPServer();
|
|
220
|
+
const transport = new StdioServerTransport();
|
|
221
|
+
await server.connect(transport);
|
|
222
|
+
console.error('Repomix Semantic Compressor MCP Server running on stdio');
|
|
223
|
+
}
|