ruvector 0.2.19 → 0.2.20
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/bin/cli.js +139 -0
- package/bin/mcp-server.js +269 -0
- package/package.json +14 -6
- package/src/decompiler/index.js +407 -0
- package/src/decompiler/metrics.js +86 -0
- package/src/decompiler/module-splitter.js +498 -0
- package/src/decompiler/module-tree.js +142 -0
- package/src/decompiler/name-predictor.js +400 -0
- package/src/decompiler/npm-fetch.js +176 -0
- package/src/decompiler/reconstructor.js +499 -0
- package/src/decompiler/reference-tracker.js +285 -0
- package/src/decompiler/statement-parser.js +285 -0
- package/src/decompiler/style-improver.js +438 -0
- package/src/decompiler/subcategories.js +339 -0
- package/src/decompiler/validator.js +379 -0
- package/src/decompiler/witness.js +140 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* style-improver.js - Readability improvements and JSDoc generation.
|
|
3
|
+
*
|
|
4
|
+
* Transforms beautified-but-minified code into human-readable form:
|
|
5
|
+
* - Converts minification artifacts (!0, !1, void 0)
|
|
6
|
+
* - Adds blank lines between declarations
|
|
7
|
+
* - Converts optional chaining candidates
|
|
8
|
+
* - Generates JSDoc comments from context
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Apply all readability improvements to source code.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} source
|
|
17
|
+
* @param {object} [options]
|
|
18
|
+
* @param {boolean} [options.convertBooleans=true]
|
|
19
|
+
* @param {boolean} [options.convertVoid=true]
|
|
20
|
+
* @param {boolean} [options.optionalChaining=true]
|
|
21
|
+
* @param {boolean} [options.addSpacing=true]
|
|
22
|
+
* @param {boolean} [options.expandCommaExpressions=true]
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function improveReadability(source, options = {}) {
|
|
26
|
+
const {
|
|
27
|
+
convertBooleans = true,
|
|
28
|
+
convertVoid = true,
|
|
29
|
+
optionalChaining = true,
|
|
30
|
+
addSpacing = true,
|
|
31
|
+
expandCommaExpressions = true,
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
let result = source;
|
|
35
|
+
|
|
36
|
+
if (convertBooleans) {
|
|
37
|
+
result = convertMinifiedBooleans(result);
|
|
38
|
+
}
|
|
39
|
+
if (convertVoid) {
|
|
40
|
+
result = convertVoidZero(result);
|
|
41
|
+
}
|
|
42
|
+
if (optionalChaining) {
|
|
43
|
+
result = convertToOptionalChaining(result);
|
|
44
|
+
}
|
|
45
|
+
if (expandCommaExpressions) {
|
|
46
|
+
result = expandCommaExprs(result);
|
|
47
|
+
}
|
|
48
|
+
if (addSpacing) {
|
|
49
|
+
result = addBlankLines(result);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert !0 -> true, !1 -> false.
|
|
57
|
+
* Must not match inside strings or comments.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} source
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function convertMinifiedBooleans(source) {
|
|
63
|
+
// Replace !0 with true (not inside strings)
|
|
64
|
+
let result = source.replace(/(?<![a-zA-Z0-9_$"'`])!0(?![a-zA-Z0-9_$])/g, 'true');
|
|
65
|
+
// Replace !1 with false
|
|
66
|
+
result = result.replace(/(?<![a-zA-Z0-9_$"'`])!1(?![a-zA-Z0-9_$])/g, 'false');
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convert void 0 -> undefined.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} source
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function convertVoidZero(source) {
|
|
77
|
+
return source.replace(/\bvoid 0\b/g, 'undefined');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert guard chains to optional chaining.
|
|
82
|
+
* a && a.b && a.b.c -> a?.b?.c
|
|
83
|
+
* a && a.b -> a?.b
|
|
84
|
+
*
|
|
85
|
+
* Only applies to simple property access chains (no method calls).
|
|
86
|
+
*
|
|
87
|
+
* @param {string} source
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
function convertToOptionalChaining(source) {
|
|
91
|
+
// Pattern: a && a.b && a.b.c
|
|
92
|
+
// Match the longest chains first
|
|
93
|
+
let result = source;
|
|
94
|
+
|
|
95
|
+
// 3-level chain: a && a.b && a.b.c
|
|
96
|
+
result = result.replace(
|
|
97
|
+
/\b([a-zA-Z_$]\w*)(?:\s*&&\s*\1\.([a-zA-Z_$]\w*))(?:\s*&&\s*\1\.\2\.([a-zA-Z_$]\w*))/g,
|
|
98
|
+
'$1?.$2?.$3',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// 2-level chain: a && a.b
|
|
102
|
+
result = result.replace(
|
|
103
|
+
/\b([a-zA-Z_$]\w*)\s*&&\s*\1\.([a-zA-Z_$]\w*)/g,
|
|
104
|
+
'$1?.$2',
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Expand simple comma expressions into separate statements.
|
|
112
|
+
* Only expands top-level comma expressions (not inside for-loops, function args, etc.).
|
|
113
|
+
*
|
|
114
|
+
* a = 1, b = 2, c = 3 -> a = 1;\nb = 2;\nc = 3
|
|
115
|
+
*
|
|
116
|
+
* @param {string} source
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
function expandCommaExprs(source) {
|
|
120
|
+
const lines = source.split('\n');
|
|
121
|
+
const result = [];
|
|
122
|
+
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
|
|
126
|
+
// Skip lines that are clearly not comma expressions
|
|
127
|
+
if (trimmed.startsWith('for') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
|
|
128
|
+
result.push(line);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Skip function calls, array literals, object literals
|
|
133
|
+
if (trimmed.includes('(') || trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
134
|
+
result.push(line);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for assignment comma expressions: a=1,b=2,c=3
|
|
139
|
+
if (/^\s*[a-zA-Z_$]\w*\s*=.*,.*[a-zA-Z_$]\w*\s*=/.test(trimmed)) {
|
|
140
|
+
const indent = line.match(/^(\s*)/)[1];
|
|
141
|
+
// Split on comma but only between assignments
|
|
142
|
+
const parts = splitCommaExpression(trimmed);
|
|
143
|
+
if (parts.length > 1) {
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
result.push(indent + part.trim() + ';');
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
result.push(line);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Split a comma expression into parts, respecting nested parens/brackets.
|
|
159
|
+
* @param {string} expr
|
|
160
|
+
* @returns {string[]}
|
|
161
|
+
*/
|
|
162
|
+
function splitCommaExpression(expr) {
|
|
163
|
+
const parts = [];
|
|
164
|
+
let depth = 0;
|
|
165
|
+
let current = '';
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < expr.length; i++) {
|
|
168
|
+
const ch = expr[i];
|
|
169
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
170
|
+
depth++;
|
|
171
|
+
current += ch;
|
|
172
|
+
} else if (ch === ')' || ch === ']' || ch === '}') {
|
|
173
|
+
depth--;
|
|
174
|
+
current += ch;
|
|
175
|
+
} else if (ch === ',' && depth === 0) {
|
|
176
|
+
parts.push(current);
|
|
177
|
+
current = '';
|
|
178
|
+
} else {
|
|
179
|
+
current += ch;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (current.trim()) parts.push(current);
|
|
183
|
+
|
|
184
|
+
// Only return split if each part looks like an assignment
|
|
185
|
+
const allAssignments = parts.every((p) => /\s*[a-zA-Z_$]\w*\s*=/.test(p.trim()));
|
|
186
|
+
if (allAssignments && parts.length > 1) return parts;
|
|
187
|
+
return [expr];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Add blank lines between top-level declarations for readability.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} source
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
function addBlankLines(source) {
|
|
197
|
+
const lines = source.split('\n');
|
|
198
|
+
const result = [];
|
|
199
|
+
let prevWasDecl = false;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
const trimmed = lines[i].trim();
|
|
203
|
+
const isDecl =
|
|
204
|
+
/^(function|async function|class|const|let|var|export)/.test(trimmed) &&
|
|
205
|
+
!trimmed.startsWith('const {') &&
|
|
206
|
+
trimmed.length > 20;
|
|
207
|
+
|
|
208
|
+
// Add blank line before a new declaration block
|
|
209
|
+
if (isDecl && prevWasDecl && result.length > 0 && result[result.length - 1].trim() !== '') {
|
|
210
|
+
// Only add if there is not already a blank line
|
|
211
|
+
result.push('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
result.push(lines[i]);
|
|
215
|
+
|
|
216
|
+
// Track if we just left a closing brace (end of function/class)
|
|
217
|
+
if (trimmed === '}' || trimmed === '};') {
|
|
218
|
+
prevWasDecl = true;
|
|
219
|
+
} else if (trimmed === '') {
|
|
220
|
+
prevWasDecl = false;
|
|
221
|
+
} else {
|
|
222
|
+
prevWasDecl = isDecl;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result.join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Generate a JSDoc comment for a function/class declaration.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} declaration - the declaration line(s)
|
|
233
|
+
* @param {string[]} contextStrings - strings found near the declaration
|
|
234
|
+
* @param {object} [options]
|
|
235
|
+
* @param {Array<{oldName: string, newName: string}>} [options.renames] - applied renames
|
|
236
|
+
* @returns {string|null} JSDoc comment string, or null if not applicable
|
|
237
|
+
*/
|
|
238
|
+
function generateJSDoc(declaration, contextStrings, options = {}) {
|
|
239
|
+
const { renames = [] } = options;
|
|
240
|
+
|
|
241
|
+
// Determine declaration type
|
|
242
|
+
const isAsyncGen = /async\s+function\s*\*/.test(declaration);
|
|
243
|
+
const isGenerator = /function\s*\*/.test(declaration);
|
|
244
|
+
const isAsync = /async/.test(declaration);
|
|
245
|
+
const isClass = /class\s+/.test(declaration);
|
|
246
|
+
const isFunction =
|
|
247
|
+
/function[\s*]/.test(declaration) || /=>\s*{/.test(declaration) || /=>\s*[^{]/.test(declaration);
|
|
248
|
+
|
|
249
|
+
if (!isFunction && !isClass) return null;
|
|
250
|
+
|
|
251
|
+
const lines = ['/**'];
|
|
252
|
+
|
|
253
|
+
// Infer purpose from context
|
|
254
|
+
const purpose = inferPurpose(declaration, contextStrings, renames);
|
|
255
|
+
if (purpose) {
|
|
256
|
+
lines.push(` * ${purpose}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isClass) {
|
|
260
|
+
const extendsMatch = declaration.match(/extends\s+(\w+)/);
|
|
261
|
+
if (extendsMatch) {
|
|
262
|
+
lines.push(` * @extends ${extendsMatch[1]}`);
|
|
263
|
+
}
|
|
264
|
+
lines.push(' */');
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Extract parameters
|
|
269
|
+
const params = extractParams(declaration);
|
|
270
|
+
if (params.length > 0) {
|
|
271
|
+
for (const param of params) {
|
|
272
|
+
const type = inferParamType(param, contextStrings);
|
|
273
|
+
lines.push(` * @param {${type}} ${param}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Yields for generators
|
|
278
|
+
if (isAsyncGen || isGenerator) {
|
|
279
|
+
const yieldType = inferYieldType(declaration, contextStrings);
|
|
280
|
+
lines.push(` * @yields {${yieldType}}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Returns
|
|
284
|
+
if (!isAsyncGen && !isGenerator) {
|
|
285
|
+
const returnType = inferReturnType(declaration, contextStrings, isAsync);
|
|
286
|
+
if (returnType) {
|
|
287
|
+
lines.push(` * @returns {${returnType}}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
lines.push(' */');
|
|
292
|
+
return lines.join('\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Infer the purpose of a function from its context.
|
|
297
|
+
* @param {string} declaration
|
|
298
|
+
* @param {string[]} context
|
|
299
|
+
* @param {Array<{oldName: string, newName: string}>} renames
|
|
300
|
+
* @returns {string|null}
|
|
301
|
+
*/
|
|
302
|
+
function inferPurpose(declaration, context, renames) {
|
|
303
|
+
const ctx = context.join(' ').toLowerCase();
|
|
304
|
+
const decl = declaration.toLowerCase();
|
|
305
|
+
|
|
306
|
+
// Use the renamed function name as a hint
|
|
307
|
+
const funcNameMatch = declaration.match(/function\s+(\w+)|(\w+)\s*[=:]\s*(async\s+)?function/);
|
|
308
|
+
const funcName = funcNameMatch ? (funcNameMatch[1] || funcNameMatch[2]) : null;
|
|
309
|
+
|
|
310
|
+
if (funcName) {
|
|
311
|
+
// Convert camelCase to sentence
|
|
312
|
+
const words = funcName.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
|
|
313
|
+
if (words.length > 3 && !/^[a-z]$/.test(words)) {
|
|
314
|
+
return capitalizeFirst(words) + '.';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Context-based purpose
|
|
319
|
+
if (ctx.includes('stream') && ctx.includes('event')) return 'Process streaming events from the API.';
|
|
320
|
+
if (ctx.includes('permission') && ctx.includes('check')) return 'Check if the operation is permitted.';
|
|
321
|
+
if (ctx.includes('compact') && ctx.includes('token')) return 'Compact context to fit within token budget.';
|
|
322
|
+
if (ctx.includes('tool') && ctx.includes('dispatch')) return 'Dispatch tool invocations to handlers.';
|
|
323
|
+
if (ctx.includes('mcp') && ctx.includes('connect')) return 'Establish MCP server connection.';
|
|
324
|
+
if (ctx.includes('fetch') && ctx.includes('api')) return 'Make an API request.';
|
|
325
|
+
if (ctx.includes('parse') && ctx.includes('json')) return 'Parse JSON response data.';
|
|
326
|
+
if (ctx.includes('validate') && ctx.includes('input')) return 'Validate input parameters.';
|
|
327
|
+
if (ctx.includes('error') && ctx.includes('handle')) return 'Handle and format error responses.';
|
|
328
|
+
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Extract parameter names from a function declaration.
|
|
334
|
+
* @param {string} declaration
|
|
335
|
+
* @returns {string[]}
|
|
336
|
+
*/
|
|
337
|
+
function extractParams(declaration) {
|
|
338
|
+
// Match function parameters: function name(a, b, c) or (a, b) =>
|
|
339
|
+
const match = declaration.match(/\(([^)]*)\)/);
|
|
340
|
+
if (!match || !match[1].trim()) return [];
|
|
341
|
+
|
|
342
|
+
return match[1]
|
|
343
|
+
.split(',')
|
|
344
|
+
.map((p) => p.trim())
|
|
345
|
+
.filter((p) => p.length > 0)
|
|
346
|
+
.map((p) => {
|
|
347
|
+
// Handle destructuring: { a, b } -> "options"
|
|
348
|
+
if (p.startsWith('{')) return 'options';
|
|
349
|
+
if (p.startsWith('[')) return 'items';
|
|
350
|
+
// Handle defaults: a = 1 -> a
|
|
351
|
+
return p.split('=')[0].trim();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Infer a TypeScript-style type for a parameter from context.
|
|
357
|
+
* @param {string} paramName
|
|
358
|
+
* @param {string[]} context
|
|
359
|
+
* @returns {string}
|
|
360
|
+
*/
|
|
361
|
+
function inferParamType(paramName, context) {
|
|
362
|
+
const ctx = context.join(' ').toLowerCase();
|
|
363
|
+
|
|
364
|
+
if (paramName === 'options' || paramName === 'config') return 'Object';
|
|
365
|
+
if (paramName === 'items' || paramName === 'list') return 'Array';
|
|
366
|
+
if (ctx.includes('string') || ctx.includes('name') || ctx.includes('path')) return 'string';
|
|
367
|
+
if (ctx.includes('number') || ctx.includes('count') || ctx.includes('index')) return 'number';
|
|
368
|
+
if (ctx.includes('boolean') || ctx.includes('flag') || ctx.includes('enabled')) return 'boolean';
|
|
369
|
+
if (ctx.includes('callback') || ctx.includes('handler')) return 'Function';
|
|
370
|
+
if (ctx.includes('promise') || ctx.includes('async')) return 'Promise';
|
|
371
|
+
if (ctx.includes('array') || ctx.includes('list')) return 'Array';
|
|
372
|
+
if (ctx.includes('message')) return 'Message';
|
|
373
|
+
if (ctx.includes('request')) return 'Request';
|
|
374
|
+
if (ctx.includes('response')) return 'Response';
|
|
375
|
+
|
|
376
|
+
return '*';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Infer what a generator yields.
|
|
381
|
+
* @param {string} declaration
|
|
382
|
+
* @param {string[]} context
|
|
383
|
+
* @returns {string}
|
|
384
|
+
*/
|
|
385
|
+
function inferYieldType(declaration, context) {
|
|
386
|
+
const ctx = context.join(' ');
|
|
387
|
+
if (ctx.includes('stream_event') || ctx.includes('StreamEvent')) return 'StreamEvent';
|
|
388
|
+
if (ctx.includes('message') || ctx.includes('Message')) return 'Message';
|
|
389
|
+
if (ctx.includes('chunk') || ctx.includes('Chunk')) return 'Chunk';
|
|
390
|
+
return 'Object';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Infer the return type of a function.
|
|
395
|
+
* @param {string} declaration
|
|
396
|
+
* @param {string[]} context
|
|
397
|
+
* @param {boolean} isAsync
|
|
398
|
+
* @returns {string|null}
|
|
399
|
+
*/
|
|
400
|
+
function inferReturnType(declaration, context, isAsync) {
|
|
401
|
+
const ctx = context.join(' ');
|
|
402
|
+
|
|
403
|
+
let baseType = null;
|
|
404
|
+
if (ctx.includes('boolean') || ctx.includes('true') || ctx.includes('false')) {
|
|
405
|
+
baseType = 'boolean';
|
|
406
|
+
} else if (ctx.includes('string') || ctx.includes('"')) {
|
|
407
|
+
baseType = 'string';
|
|
408
|
+
} else if (ctx.includes('number') || ctx.includes('.length')) {
|
|
409
|
+
baseType = 'number';
|
|
410
|
+
} else if (ctx.includes('array') || ctx.includes('[]')) {
|
|
411
|
+
baseType = 'Array';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (isAsync && baseType) return `Promise<${baseType}>`;
|
|
415
|
+
if (isAsync) return 'Promise<*>';
|
|
416
|
+
return baseType;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Capitalize the first letter of a string.
|
|
421
|
+
* @param {string} s
|
|
422
|
+
* @returns {string}
|
|
423
|
+
*/
|
|
424
|
+
function capitalizeFirst(s) {
|
|
425
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = {
|
|
429
|
+
improveReadability,
|
|
430
|
+
convertMinifiedBooleans,
|
|
431
|
+
convertVoidZero,
|
|
432
|
+
convertToOptionalChaining,
|
|
433
|
+
expandCommaExprs,
|
|
434
|
+
addBlankLines,
|
|
435
|
+
generateJSDoc,
|
|
436
|
+
extractParams,
|
|
437
|
+
inferPurpose,
|
|
438
|
+
};
|