@flisk/analyze-tracking 0.7.6 → 0.8.1
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/README.md +29 -21
- package/bin/cli.js +1 -0
- package/package.json +1 -1
- package/src/analyze/go/astTraversal.js +22 -22
- package/src/analyze/go/eventExtractor.js +10 -7
- package/src/analyze/go/index.js +39 -19
- package/src/analyze/go/propertyExtractor.js +25 -5
- package/src/analyze/go/trackingExtractor.js +5 -5
- package/src/analyze/index.js +9 -6
- package/src/analyze/javascript/detectors/analytics-source.js +55 -2
- package/src/analyze/javascript/extractors/event-extractor.js +109 -12
- package/src/analyze/javascript/index.js +14 -8
- package/src/analyze/javascript/parser.js +140 -21
- package/src/analyze/python/index.js +32 -26
- package/src/analyze/python/pythonTrackingAnalyzer.py +113 -39
- package/src/analyze/ruby/extractors.js +46 -10
- package/src/analyze/ruby/index.js +14 -7
- package/src/analyze/ruby/visitor.js +24 -7
- package/src/analyze/typescript/extractors/event-extractor.js +141 -20
- package/src/analyze/typescript/index.js +16 -10
- package/src/analyze/typescript/parser.js +37 -14
- package/src/analyze/utils/customFunctionParser.js +55 -0
- package/src/index.js +2 -2
|
@@ -21,6 +21,7 @@ const EXTRACTION_STRATEGIES = {
|
|
|
21
21
|
googleanalytics: extractGoogleAnalyticsEvent,
|
|
22
22
|
snowplow: extractSnowplowEvent,
|
|
23
23
|
mparticle: extractMparticleEvent,
|
|
24
|
+
custom: extractCustomEvent,
|
|
24
25
|
default: extractDefaultEvent
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -30,10 +31,14 @@ const EXTRACTION_STRATEGIES = {
|
|
|
30
31
|
* @param {string} source - Analytics provider source
|
|
31
32
|
* @param {Object} checker - TypeScript type checker
|
|
32
33
|
* @param {Object} sourceFile - TypeScript source file
|
|
34
|
+
* @param {Object} customConfig - Custom configuration for custom extraction
|
|
33
35
|
* @returns {EventData} Extracted event data
|
|
34
36
|
*/
|
|
35
|
-
function extractEventData(node, source, checker, sourceFile) {
|
|
37
|
+
function extractEventData(node, source, checker, sourceFile, customConfig) {
|
|
36
38
|
const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default;
|
|
39
|
+
if (source === 'custom') {
|
|
40
|
+
return strategy(node, checker, sourceFile, customConfig);
|
|
41
|
+
}
|
|
37
42
|
return strategy(node, checker, sourceFile);
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -121,6 +126,32 @@ function extractMparticleEvent(node, checker, sourceFile) {
|
|
|
121
126
|
return { eventName, propertiesNode };
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Custom extraction
|
|
131
|
+
* @param {Object} node - CallExpression node
|
|
132
|
+
* @param {Object} checker - TypeScript type checker
|
|
133
|
+
* @param {Object} sourceFile - TypeScript source file
|
|
134
|
+
* @param {Object} customConfig - Custom configuration for custom extraction
|
|
135
|
+
* @returns {EventData}
|
|
136
|
+
*/
|
|
137
|
+
function extractCustomEvent(node, checker, sourceFile, customConfig) {
|
|
138
|
+
const args = node.arguments || [];
|
|
139
|
+
|
|
140
|
+
const eventArg = args[customConfig?.eventIndex ?? 0];
|
|
141
|
+
const propertiesArg = args[customConfig?.propertiesIndex ?? 1];
|
|
142
|
+
|
|
143
|
+
const eventName = getStringValue(eventArg, checker, sourceFile);
|
|
144
|
+
|
|
145
|
+
const extraArgs = {};
|
|
146
|
+
if (customConfig && customConfig.extraParams) {
|
|
147
|
+
customConfig.extraParams.forEach(extra => {
|
|
148
|
+
extraArgs[extra.name] = args[extra.idx];
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { eventName, propertiesNode: propertiesArg, extraArgs };
|
|
153
|
+
}
|
|
154
|
+
|
|
124
155
|
/**
|
|
125
156
|
* Default event extraction for standard providers
|
|
126
157
|
* @param {Object} node - CallExpression node
|
|
@@ -149,9 +180,10 @@ function extractDefaultEvent(node, checker, sourceFile) {
|
|
|
149
180
|
* @param {string} functionName - Containing function name
|
|
150
181
|
* @param {Object} checker - TypeScript type checker
|
|
151
182
|
* @param {Object} sourceFile - TypeScript source file
|
|
183
|
+
* @param {Object} customConfig - Custom configuration for custom extraction
|
|
152
184
|
* @returns {Object|null} Processed event object or null
|
|
153
185
|
*/
|
|
154
|
-
function processEventData(eventData, source, filePath, line, functionName, checker, sourceFile) {
|
|
186
|
+
function processEventData(eventData, source, filePath, line, functionName, checker, sourceFile, customConfig) {
|
|
155
187
|
const { eventName, propertiesNode } = eventData;
|
|
156
188
|
|
|
157
189
|
if (!eventName || !propertiesNode) {
|
|
@@ -184,6 +216,37 @@ function processEventData(eventData, source, filePath, line, functionName, check
|
|
|
184
216
|
// Clean up any unresolved type markers
|
|
185
217
|
const cleanedProperties = cleanupProperties(properties);
|
|
186
218
|
|
|
219
|
+
// Handle custom extra params
|
|
220
|
+
if (source === 'custom' && customConfig && eventData.extraArgs) {
|
|
221
|
+
for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) {
|
|
222
|
+
if (argNode && ts.isObjectLiteralExpression(argNode)) {
|
|
223
|
+
// Extract detailed properties from object literal expression
|
|
224
|
+
cleanedProperties[paramName] = {
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: extractProperties(checker, argNode)
|
|
227
|
+
};
|
|
228
|
+
} else if (argNode && ts.isIdentifier(argNode)) {
|
|
229
|
+
// Handle identifier references to objects
|
|
230
|
+
const resolvedNode = resolveIdentifierToInitializer(checker, argNode, sourceFile);
|
|
231
|
+
if (resolvedNode && ts.isObjectLiteralExpression(resolvedNode)) {
|
|
232
|
+
cleanedProperties[paramName] = {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: extractProperties(checker, resolvedNode)
|
|
235
|
+
};
|
|
236
|
+
} else {
|
|
237
|
+
cleanedProperties[paramName] = {
|
|
238
|
+
type: inferNodeValueType(argNode)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// For non-object arguments, use simple type inference
|
|
243
|
+
cleanedProperties[paramName] = {
|
|
244
|
+
type: inferNodeValueType(argNode)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
187
250
|
return {
|
|
188
251
|
eventName,
|
|
189
252
|
source,
|
|
@@ -233,27 +296,75 @@ function resolvePropertyAccessToString(node, checker, sourceFile) {
|
|
|
233
296
|
try {
|
|
234
297
|
// Get the symbol for the property access
|
|
235
298
|
const symbol = checker.getSymbolAtLocation(node);
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
299
|
+
if (symbol && symbol.valueDeclaration) {
|
|
300
|
+
// Check if it's a property assignment with a string initializer
|
|
301
|
+
if (ts.isPropertyAssignment(symbol.valueDeclaration) &&
|
|
302
|
+
symbol.valueDeclaration.initializer &&
|
|
303
|
+
ts.isStringLiteral(symbol.valueDeclaration.initializer)) {
|
|
304
|
+
return symbol.valueDeclaration.initializer.text;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check if it's a variable declaration property (string literal type)
|
|
308
|
+
if (ts.isPropertySignature(symbol.valueDeclaration) ||
|
|
309
|
+
ts.isMethodSignature(symbol.valueDeclaration)) {
|
|
310
|
+
const type = checker.getTypeAtLocation(node);
|
|
311
|
+
if (type && type.isStringLiteral && type.isStringLiteral()) {
|
|
312
|
+
return type.value;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
245
315
|
}
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------
|
|
318
|
+
// Fallback – manually resolve patterns like:
|
|
319
|
+
// const CONST = { KEY: 'value' };
|
|
320
|
+
// const CONST = Object.freeze({ KEY: 'value' });
|
|
321
|
+
// And later used as CONST.KEY
|
|
322
|
+
// ---------------------------------------------------------------------
|
|
323
|
+
if (ts.isIdentifier(node.expression)) {
|
|
324
|
+
const objIdentifier = node.expression;
|
|
325
|
+
const initializer = resolveIdentifierToInitializer(checker, objIdentifier, sourceFile);
|
|
326
|
+
if (initializer) {
|
|
327
|
+
let objectLiteral = null;
|
|
328
|
+
|
|
329
|
+
// Handle direct object literal initializers
|
|
330
|
+
if (ts.isObjectLiteralExpression(initializer)) {
|
|
331
|
+
objectLiteral = initializer;
|
|
332
|
+
}
|
|
333
|
+
// Handle Object.freeze({ ... }) pattern
|
|
334
|
+
else if (ts.isCallExpression(initializer)) {
|
|
335
|
+
const callee = initializer.expression;
|
|
336
|
+
if (
|
|
337
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
338
|
+
ts.isIdentifier(callee.expression) &&
|
|
339
|
+
callee.expression.escapedText === 'Object' &&
|
|
340
|
+
callee.name.escapedText === 'freeze' &&
|
|
341
|
+
initializer.arguments.length > 0 &&
|
|
342
|
+
ts.isObjectLiteralExpression(initializer.arguments[0])
|
|
343
|
+
) {
|
|
344
|
+
objectLiteral = initializer.arguments[0];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (objectLiteral) {
|
|
349
|
+
const propNode = findPropertyByKey(objectLiteral, node.name.escapedText || node.name.text);
|
|
350
|
+
if (propNode && propNode.initializer && ts.isStringLiteral(propNode.initializer)) {
|
|
351
|
+
return propNode.initializer.text;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
254
354
|
}
|
|
255
355
|
}
|
|
256
|
-
|
|
356
|
+
|
|
357
|
+
// Final fallback – use type information at location (works for imported Object.freeze constants)
|
|
358
|
+
try {
|
|
359
|
+
const t = checker.getTypeAtLocation(node);
|
|
360
|
+
if (t && t.isStringLiteral && typeof t.isStringLiteral === 'function' && t.isStringLiteral()) {
|
|
361
|
+
return t.value;
|
|
362
|
+
}
|
|
363
|
+
if (t && t.flags && (t.flags & ts.TypeFlags.StringLiteral)) {
|
|
364
|
+
return t.value;
|
|
365
|
+
}
|
|
366
|
+
} catch (_) {/* ignore */}
|
|
367
|
+
|
|
257
368
|
return null;
|
|
258
369
|
} catch (error) {
|
|
259
370
|
return null;
|
|
@@ -368,6 +479,16 @@ function cleanupProperties(properties) {
|
|
|
368
479
|
return cleaned;
|
|
369
480
|
}
|
|
370
481
|
|
|
482
|
+
function inferNodeValueType(node) {
|
|
483
|
+
if (!node) return 'any';
|
|
484
|
+
if (ts.isStringLiteral(node)) return 'string';
|
|
485
|
+
if (ts.isNumericLiteral(node)) return 'number';
|
|
486
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return 'boolean';
|
|
487
|
+
if (ts.isArrayLiteralExpression(node)) return 'array';
|
|
488
|
+
if (ts.isObjectLiteralExpression(node)) return 'object';
|
|
489
|
+
return 'any';
|
|
490
|
+
}
|
|
491
|
+
|
|
371
492
|
module.exports = {
|
|
372
493
|
extractEventData,
|
|
373
494
|
processEventData
|
|
@@ -9,16 +9,14 @@ const { getProgram, findTrackingEvents, ProgramError, SourceFileError } = requir
|
|
|
9
9
|
* Analyzes a TypeScript file for analytics tracking calls
|
|
10
10
|
* @param {string} filePath - Path to the TypeScript file to analyze
|
|
11
11
|
* @param {Object} [program] - Optional existing TypeScript program to reuse
|
|
12
|
-
* @param {string} [
|
|
12
|
+
* @param {string} [customFunctionSignature] - Optional custom function signature to detect
|
|
13
13
|
* @returns {Array<Object>} Array of tracking events found in the file
|
|
14
14
|
*/
|
|
15
|
-
function analyzeTsFile(filePath, program,
|
|
16
|
-
const events = [];
|
|
17
|
-
|
|
15
|
+
function analyzeTsFile(filePath, program = null, customFunctionSignatures = null) {
|
|
18
16
|
try {
|
|
19
|
-
// Get or create TypeScript program
|
|
17
|
+
// Get or create TypeScript program (only once)
|
|
20
18
|
const tsProgram = getProgram(filePath, program);
|
|
21
|
-
|
|
19
|
+
|
|
22
20
|
// Get source file from program
|
|
23
21
|
const sourceFile = tsProgram.getSourceFile(filePath);
|
|
24
22
|
if (!sourceFile) {
|
|
@@ -28,9 +26,17 @@ function analyzeTsFile(filePath, program, customFunction) {
|
|
|
28
26
|
// Get type checker
|
|
29
27
|
const checker = tsProgram.getTypeChecker();
|
|
30
28
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
29
|
+
// Single-pass collection covering built-in + all custom configs
|
|
30
|
+
const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
|
|
31
|
+
|
|
32
|
+
// Deduplicate events
|
|
33
|
+
const unique = new Map();
|
|
34
|
+
for (const evt of events) {
|
|
35
|
+
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
|
|
36
|
+
if (!unique.has(key)) unique.set(key, evt);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Array.from(unique.values());
|
|
34
40
|
|
|
35
41
|
} catch (error) {
|
|
36
42
|
if (error instanceof ProgramError) {
|
|
@@ -42,7 +48,7 @@ function analyzeTsFile(filePath, program, customFunction) {
|
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
return
|
|
51
|
+
return [];
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
module.exports = { analyzeTsFile };
|
|
@@ -65,32 +65,55 @@ function getProgram(filePath, existingProgram) {
|
|
|
65
65
|
* @param {Object} sourceFile - TypeScript source file
|
|
66
66
|
* @param {Object} checker - TypeScript type checker
|
|
67
67
|
* @param {string} filePath - Path to the file being analyzed
|
|
68
|
-
* @param {
|
|
68
|
+
* @param {Array<Object>} [customConfigs] - Array of custom function configurations
|
|
69
69
|
* @returns {Array<Object>} Array of found events
|
|
70
70
|
*/
|
|
71
|
-
function findTrackingEvents(sourceFile, checker, filePath,
|
|
71
|
+
function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
72
72
|
const events = [];
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* Helper to test if a CallExpression matches a custom function name.
|
|
76
|
+
* We simply rely on node.expression.getText() which preserves the fully qualified name.
|
|
77
77
|
*/
|
|
78
|
+
const matchesCustomFn = (callNode, fnName) => {
|
|
79
|
+
if (!fnName) return false;
|
|
80
|
+
try {
|
|
81
|
+
return callNode.expression && callNode.expression.getText() === fnName;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
78
87
|
function visit(node) {
|
|
79
88
|
try {
|
|
80
89
|
if (ts.isCallExpression(node)) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
let matchedCustom = null;
|
|
91
|
+
|
|
92
|
+
if (Array.isArray(customConfigs) && customConfigs.length > 0) {
|
|
93
|
+
for (const cfg of customConfigs) {
|
|
94
|
+
if (cfg && matchesCustomFn(node, cfg.functionName)) {
|
|
95
|
+
matchedCustom = cfg;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
84
99
|
}
|
|
100
|
+
|
|
101
|
+
const event = extractTrackingEvent(
|
|
102
|
+
node,
|
|
103
|
+
sourceFile,
|
|
104
|
+
checker,
|
|
105
|
+
filePath,
|
|
106
|
+
matchedCustom /* may be null */
|
|
107
|
+
);
|
|
108
|
+
if (event) events.push(event);
|
|
85
109
|
}
|
|
86
|
-
|
|
110
|
+
|
|
87
111
|
ts.forEachChild(node, visit);
|
|
88
112
|
} catch (error) {
|
|
89
113
|
console.error(`Error processing node in ${filePath}:`, error.message);
|
|
90
114
|
}
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
// Start traversal from the root
|
|
94
117
|
ts.forEachChild(sourceFile, visit);
|
|
95
118
|
|
|
96
119
|
return events;
|
|
@@ -102,25 +125,25 @@ function findTrackingEvents(sourceFile, checker, filePath, customFunction) {
|
|
|
102
125
|
* @param {Object} sourceFile - TypeScript source file
|
|
103
126
|
* @param {Object} checker - TypeScript type checker
|
|
104
127
|
* @param {string} filePath - File path
|
|
105
|
-
* @param {
|
|
128
|
+
* @param {Object} [customConfig] - Custom function configuration
|
|
106
129
|
* @returns {Object|null} Extracted event or null
|
|
107
130
|
*/
|
|
108
|
-
function extractTrackingEvent(node, sourceFile, checker, filePath,
|
|
131
|
+
function extractTrackingEvent(node, sourceFile, checker, filePath, customConfig) {
|
|
109
132
|
// Detect the analytics source
|
|
110
|
-
const source = detectAnalyticsSource(node,
|
|
133
|
+
const source = detectAnalyticsSource(node, customConfig?.functionName);
|
|
111
134
|
if (source === 'unknown') {
|
|
112
135
|
return null;
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
// Extract event data based on the source
|
|
116
|
-
const eventData = extractEventData(node, source, checker, sourceFile);
|
|
139
|
+
const eventData = extractEventData(node, source, checker, sourceFile, customConfig);
|
|
117
140
|
|
|
118
141
|
// Get location and context information
|
|
119
142
|
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
120
143
|
const functionName = findWrappingFunction(node);
|
|
121
144
|
|
|
122
145
|
// Process the event data into final format
|
|
123
|
-
return processEventData(eventData, source, filePath, line, functionName, checker, sourceFile);
|
|
146
|
+
return processEventData(eventData, source, filePath, line, functionName, checker, sourceFile, customConfig);
|
|
124
147
|
}
|
|
125
148
|
|
|
126
149
|
module.exports = {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Create new file with parser implementation
|
|
2
|
+
function parseCustomFunctionSignature(signature) {
|
|
3
|
+
if (!signature || typeof signature !== 'string') {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Match function name and optional parameter list
|
|
8
|
+
// Supports names with module prefix like Module.func
|
|
9
|
+
const match = signature.match(/^\s*([A-Za-z0-9_.]+)\s*(?:\(([^)]*)\))?\s*$/);
|
|
10
|
+
if (!match) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const functionName = match[1].trim();
|
|
15
|
+
const paramsPart = match[2];
|
|
16
|
+
|
|
17
|
+
// Default legacy behaviour: EVENT_NAME, PROPERTIES
|
|
18
|
+
if (!paramsPart) {
|
|
19
|
+
return {
|
|
20
|
+
functionName,
|
|
21
|
+
eventIndex: 0,
|
|
22
|
+
propertiesIndex: 1,
|
|
23
|
+
extraParams: []
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Split params by comma, trimming whitespace
|
|
28
|
+
const params = paramsPart.split(',').map(p => p.trim()).filter(Boolean);
|
|
29
|
+
|
|
30
|
+
const eventIndex = params.findIndex(p => p.toUpperCase() === 'EVENT_NAME');
|
|
31
|
+
let propertiesIndex = params.findIndex(p => p.toUpperCase() === 'PROPERTIES');
|
|
32
|
+
|
|
33
|
+
if (eventIndex === -1) {
|
|
34
|
+
throw new Error('EVENT_NAME is required in custom function signature');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (propertiesIndex === -1) {
|
|
38
|
+
// If PROPERTIES is missing, assume it's at the end of the parameters
|
|
39
|
+
propertiesIndex = params.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const extraParams = params.map((name, idx) => ({ idx, name }))
|
|
43
|
+
.filter(p => !(p.idx === eventIndex || p.idx === propertiesIndex));
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
functionName,
|
|
47
|
+
eventIndex,
|
|
48
|
+
propertiesIndex,
|
|
49
|
+
extraParams
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
parseCustomFunctionSignature
|
|
55
|
+
};
|
package/src/index.js
CHANGED
|
@@ -11,8 +11,8 @@ const { generateDescriptions } = require('./generateDescriptions');
|
|
|
11
11
|
const { ChatOpenAI } = require('@langchain/openai');
|
|
12
12
|
const { ChatVertexAI } = require('@langchain/google-vertexai');
|
|
13
13
|
|
|
14
|
-
async function run(targetDir, outputPath,
|
|
15
|
-
let events = await analyzeDirectory(targetDir,
|
|
14
|
+
async function run(targetDir, outputPath, customFunctions, customSourceDetails, generateDescription, provider, model, stdout, format) {
|
|
15
|
+
let events = await analyzeDirectory(targetDir, customFunctions);
|
|
16
16
|
if (generateDescription) {
|
|
17
17
|
let llm;
|
|
18
18
|
if (provider === 'openai') {
|