create-claude-context 1.2.0 → 1.2.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.
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Call Tracer
3
+ *
4
+ * Performs static call chain analysis to trace function calls.
5
+ * Used for workflow documentation.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Language-specific patterns for call extraction
13
+ */
14
+ const CALL_PATTERNS = {
15
+ javascript: {
16
+ functionCall: /(\w+)\s*\(/g,
17
+ import: /(?:import\s+(?:(?:\{[^}]+\})|(?:\*\s+as\s+\w+)|(?:\w+))\s+from\s+['"]([^'"]+)['"])|(?:require\s*\(\s*['"]([^'"]+)['"]\s*\))/g,
18
+ methodCall: /(\w+)\.(\w+)\s*\(/g,
19
+ functionDef: /(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(|(\w+)\s*[:=]\s*(?:async\s*)?\([^)]*\)\s*=>)/g,
20
+ asyncAwait: /await\s+(\w+)(?:\.(\w+))?\s*\(/g
21
+ },
22
+
23
+ typescript: {
24
+ functionCall: /(\w+)\s*\(/g,
25
+ import: /import\s+(?:\{[^}]+\}|(?:\*\s+as\s+\w+)|\w+)\s+from\s+['"]([^'"]+)['"]/g,
26
+ methodCall: /(\w+)\.(\w+)\s*\(/g,
27
+ functionDef: /(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(|(\w+)\s*[:=]\s*(?:async\s*)?\([^)]*\)\s*=>)/g,
28
+ asyncAwait: /await\s+(\w+)(?:\.(\w+))?\s*\(/g,
29
+ classMethod: /(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+)?\s*\{/g
30
+ },
31
+
32
+ python: {
33
+ functionCall: /(\w+)\s*\(/g,
34
+ import: /(?:from\s+(\S+)\s+import)|(?:import\s+(\S+))/g,
35
+ methodCall: /(\w+)\.(\w+)\s*\(/g,
36
+ functionDef: /def\s+(\w+)\s*\(/g,
37
+ classMethod: /def\s+(\w+)\s*\(\s*self/g,
38
+ asyncDef: /async\s+def\s+(\w+)\s*\(/g
39
+ },
40
+
41
+ go: {
42
+ functionCall: /(\w+)\s*\(/g,
43
+ import: /import\s+(?:\(\s*)?["']([^"']+)["']/g,
44
+ methodCall: /(\w+)\.(\w+)\s*\(/g,
45
+ functionDef: /func\s+(\w+)\s*\(/g,
46
+ methodDef: /func\s+\([^)]+\)\s+(\w+)\s*\(/g
47
+ },
48
+
49
+ ruby: {
50
+ functionCall: /(\w+)\s*[\(\s]/g,
51
+ methodCall: /(\w+)\.(\w+)/g,
52
+ functionDef: /def\s+(\w+)/g,
53
+ classMethod: /def\s+self\.(\w+)/g
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Keywords to exclude from call analysis
59
+ */
60
+ const EXCLUDED_CALLS = new Set([
61
+ // Control flow
62
+ 'if', 'else', 'for', 'while', 'switch', 'case', 'try', 'catch', 'finally',
63
+ // Built-ins
64
+ 'console', 'log', 'error', 'warn', 'info', 'debug',
65
+ 'print', 'println', 'printf',
66
+ 'require', 'import', 'export', 'from',
67
+ // Common JS
68
+ 'map', 'filter', 'reduce', 'forEach', 'find', 'some', 'every',
69
+ 'push', 'pop', 'shift', 'unshift', 'slice', 'splice',
70
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
71
+ 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
72
+ 'async', 'await',
73
+ // Common Python
74
+ 'len', 'range', 'list', 'dict', 'set', 'tuple', 'str', 'int', 'float',
75
+ 'open', 'read', 'write', 'close',
76
+ 'append', 'extend', 'update', 'get', 'keys', 'values', 'items',
77
+ // Types
78
+ 'String', 'Number', 'Boolean', 'Array', 'Object', 'Function',
79
+ 'typeof', 'instanceof'
80
+ ]);
81
+
82
+ /**
83
+ * Detect language from file extension
84
+ * @param {string} filePath - File path
85
+ * @returns {string} Language name
86
+ */
87
+ function detectLanguage(filePath) {
88
+ const ext = path.extname(filePath).toLowerCase();
89
+ const langMap = {
90
+ '.js': 'javascript',
91
+ '.jsx': 'javascript',
92
+ '.mjs': 'javascript',
93
+ '.cjs': 'javascript',
94
+ '.ts': 'typescript',
95
+ '.tsx': 'typescript',
96
+ '.py': 'python',
97
+ '.go': 'go',
98
+ '.rb': 'ruby',
99
+ '.rs': 'rust'
100
+ };
101
+ return langMap[ext] || 'javascript';
102
+ }
103
+
104
+ /**
105
+ * Extract function body from content
106
+ * @param {string} content - File content
107
+ * @param {string} functionName - Function name to find
108
+ * @param {string} language - Programming language
109
+ * @returns {object|null} Function body and line number
110
+ */
111
+ function extractFunctionBody(content, functionName, language) {
112
+ const lines = content.split('\n');
113
+
114
+ // Language-specific function patterns
115
+ const patterns = {
116
+ javascript: [
117
+ new RegExp(`function\\s+${functionName}\\s*\\(`),
118
+ new RegExp(`(?:const|let|var)\\s+${functionName}\\s*=\\s*(?:async\\s*)?\\(`),
119
+ new RegExp(`${functionName}\\s*[:=]\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>`),
120
+ new RegExp(`${functionName}\\s*\\([^)]*\\)\\s*\\{`)
121
+ ],
122
+ typescript: [
123
+ new RegExp(`function\\s+${functionName}\\s*[<(]`),
124
+ new RegExp(`(?:const|let|var)\\s+${functionName}\\s*=\\s*(?:async\\s*)?\\(`),
125
+ new RegExp(`${functionName}\\s*[:=]\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>`),
126
+ new RegExp(`(?:async\\s+)?${functionName}\\s*\\([^)]*\\)\\s*(?::\\s*\\w+)?\\s*\\{`)
127
+ ],
128
+ python: [
129
+ new RegExp(`def\\s+${functionName}\\s*\\(`),
130
+ new RegExp(`async\\s+def\\s+${functionName}\\s*\\(`)
131
+ ],
132
+ go: [
133
+ new RegExp(`func\\s+${functionName}\\s*\\(`),
134
+ new RegExp(`func\\s+\\([^)]+\\)\\s+${functionName}\\s*\\(`)
135
+ ],
136
+ ruby: [
137
+ new RegExp(`def\\s+${functionName}\\b`)
138
+ ]
139
+ };
140
+
141
+ const langPatterns = patterns[language] || patterns.javascript;
142
+
143
+ // Find function start
144
+ let startLine = -1;
145
+ for (let i = 0; i < lines.length; i++) {
146
+ for (const pattern of langPatterns) {
147
+ if (pattern.test(lines[i])) {
148
+ startLine = i;
149
+ break;
150
+ }
151
+ }
152
+ if (startLine >= 0) break;
153
+ }
154
+
155
+ if (startLine === -1) return null;
156
+
157
+ // Extract function body (simplified - track braces/indentation)
158
+ const bodyLines = [];
159
+ let braceCount = 0;
160
+ let inFunction = false;
161
+
162
+ for (let i = startLine; i < lines.length && i < startLine + 100; i++) {
163
+ const line = lines[i];
164
+ bodyLines.push(line);
165
+
166
+ if (language === 'python' || language === 'ruby') {
167
+ // Indentation-based
168
+ if (i > startLine && line.trim() && !line.match(/^\s/)) {
169
+ break;
170
+ }
171
+ } else {
172
+ // Brace-based
173
+ braceCount += (line.match(/\{/g) || []).length;
174
+ braceCount -= (line.match(/\}/g) || []).length;
175
+
176
+ if (braceCount > 0) inFunction = true;
177
+ if (inFunction && braceCount === 0) break;
178
+ }
179
+ }
180
+
181
+ return {
182
+ body: bodyLines.join('\n'),
183
+ startLine: startLine + 1,
184
+ endLine: startLine + bodyLines.length
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Find function calls in code
190
+ * @param {string} code - Code to analyze
191
+ * @param {string} language - Programming language
192
+ * @returns {object[]} Array of function calls
193
+ */
194
+ function findFunctionCalls(code, language = 'javascript') {
195
+ const calls = [];
196
+ const patterns = CALL_PATTERNS[language] || CALL_PATTERNS.javascript;
197
+
198
+ // Find regular function calls
199
+ const funcCallPattern = patterns.functionCall;
200
+ let match;
201
+ funcCallPattern.lastIndex = 0;
202
+
203
+ while ((match = funcCallPattern.exec(code)) !== null) {
204
+ const name = match[1];
205
+ if (!EXCLUDED_CALLS.has(name) && !name.match(/^[A-Z_]+$/)) {
206
+ calls.push({
207
+ name,
208
+ type: 'function',
209
+ position: match.index
210
+ });
211
+ }
212
+ }
213
+
214
+ // Find method calls
215
+ const methodCallPattern = patterns.methodCall;
216
+ methodCallPattern.lastIndex = 0;
217
+
218
+ while ((match = methodCallPattern.exec(code)) !== null) {
219
+ const obj = match[1];
220
+ const method = match[2];
221
+ if (!EXCLUDED_CALLS.has(obj) && !EXCLUDED_CALLS.has(method)) {
222
+ calls.push({
223
+ name: `${obj}.${method}`,
224
+ object: obj,
225
+ method,
226
+ type: 'method',
227
+ position: match.index
228
+ });
229
+ }
230
+ }
231
+
232
+ // Find async/await calls (JS/TS)
233
+ if (patterns.asyncAwait) {
234
+ const asyncPattern = patterns.asyncAwait;
235
+ asyncPattern.lastIndex = 0;
236
+
237
+ while ((match = asyncPattern.exec(code)) !== null) {
238
+ const name = match[2] ? `${match[1]}.${match[2]}` : match[1];
239
+ if (!EXCLUDED_CALLS.has(match[1])) {
240
+ calls.push({
241
+ name,
242
+ type: 'async',
243
+ position: match.index
244
+ });
245
+ }
246
+ }
247
+ }
248
+
249
+ // Deduplicate by name
250
+ const seen = new Set();
251
+ return calls.filter(c => {
252
+ if (seen.has(c.name)) return false;
253
+ seen.add(c.name);
254
+ return true;
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Resolve a call to its file location
260
+ * @param {object} call - Call object
261
+ * @param {string} currentFile - Current file path
262
+ * @param {string} projectRoot - Project root
263
+ * @returns {string|null} Resolved file path
264
+ */
265
+ function resolveCallLocation(call, currentFile, projectRoot) {
266
+ // This is a simplified resolution - real resolution would need import analysis
267
+ const callName = call.method || call.name;
268
+ const currentDir = path.dirname(currentFile);
269
+
270
+ // Check same directory
271
+ const extensions = ['.js', '.ts', '.py', '.go', '.rb'];
272
+ for (const ext of extensions) {
273
+ const sameDirFile = path.join(currentDir, callName + ext);
274
+ const fullPath = path.join(projectRoot, sameDirFile);
275
+ if (fs.existsSync(fullPath)) {
276
+ return sameDirFile;
277
+ }
278
+ }
279
+
280
+ // Check common directories
281
+ const commonDirs = ['lib', 'src', 'utils', 'helpers', 'services'];
282
+ for (const dir of commonDirs) {
283
+ for (const ext of extensions) {
284
+ const filePath = path.join(dir, callName + ext);
285
+ const fullPath = path.join(projectRoot, filePath);
286
+ if (fs.existsSync(fullPath)) {
287
+ return filePath;
288
+ }
289
+ }
290
+ }
291
+
292
+ return null;
293
+ }
294
+
295
+ /**
296
+ * Trace call chain from an entry point
297
+ * @param {string} entryFile - Entry file path (relative to project root)
298
+ * @param {string} entryFunction - Entry function name
299
+ * @param {string} projectRoot - Project root directory
300
+ * @param {object} options - Options
301
+ * @returns {object[]} Call chain
302
+ */
303
+ function traceCallChain(entryFile, entryFunction, projectRoot, options = {}) {
304
+ const maxDepth = options.maxDepth || 3;
305
+ const visited = new Set();
306
+ const chain = [];
307
+
308
+ function trace(file, func, depth) {
309
+ const key = `${file}:${func}`;
310
+
311
+ if (depth > maxDepth || visited.has(key)) {
312
+ return;
313
+ }
314
+
315
+ visited.add(key);
316
+
317
+ const fullPath = path.join(projectRoot, file);
318
+ if (!fs.existsSync(fullPath)) {
319
+ return;
320
+ }
321
+
322
+ try {
323
+ const content = fs.readFileSync(fullPath, 'utf-8');
324
+ const language = detectLanguage(file);
325
+ const funcData = extractFunctionBody(content, func, language);
326
+
327
+ if (!funcData) {
328
+ chain.push({
329
+ file,
330
+ function: func,
331
+ depth,
332
+ calls: [],
333
+ status: 'not-found'
334
+ });
335
+ return;
336
+ }
337
+
338
+ const calls = findFunctionCalls(funcData.body, language);
339
+
340
+ chain.push({
341
+ file,
342
+ function: func,
343
+ depth,
344
+ startLine: funcData.startLine,
345
+ endLine: funcData.endLine,
346
+ calls: calls.map(c => c.name),
347
+ status: 'traced'
348
+ });
349
+
350
+ // Trace each call (up to first 5)
351
+ for (const call of calls.slice(0, 5)) {
352
+ const callName = call.method || call.name;
353
+ const resolvedFile = resolveCallLocation(call, file, projectRoot);
354
+
355
+ if (resolvedFile) {
356
+ trace(resolvedFile, callName, depth + 1);
357
+ }
358
+ }
359
+ } catch (error) {
360
+ chain.push({
361
+ file,
362
+ function: func,
363
+ depth,
364
+ calls: [],
365
+ status: 'error',
366
+ error: error.message
367
+ });
368
+ }
369
+ }
370
+
371
+ trace(entryFile, entryFunction, 0);
372
+
373
+ return chain;
374
+ }
375
+
376
+ /**
377
+ * Format call chain as ASCII tree
378
+ * @param {object[]} chain - Call chain from traceCallChain
379
+ * @returns {string} ASCII tree representation
380
+ */
381
+ function formatCallChainAscii(chain) {
382
+ if (!chain || chain.length === 0) {
383
+ return 'No call chain traced';
384
+ }
385
+
386
+ let output = '';
387
+ const maxDepth = Math.max(...chain.map(c => c.depth));
388
+
389
+ for (const item of chain) {
390
+ const indent = ' '.repeat(item.depth);
391
+ const prefix = item.depth === 0 ? '' : '├─ ';
392
+ const status = item.status === 'traced' ? '' : ` [${item.status}]`;
393
+
394
+ output += `${indent}${prefix}${item.function}()${status}\n`;
395
+
396
+ if (item.startLine) {
397
+ output += `${indent} └─ ${item.file}:${item.startLine}\n`;
398
+ }
399
+
400
+ // Show immediate calls (depth 0 and 1 only)
401
+ if (item.depth < 2 && item.calls && item.calls.length > 0) {
402
+ const callsToShow = item.calls.slice(0, 5);
403
+ for (let i = 0; i < callsToShow.length; i++) {
404
+ const isLast = i === callsToShow.length - 1 && item.depth === maxDepth;
405
+ const callPrefix = isLast ? '└─' : '├─';
406
+ output += `${indent} ${callPrefix} → ${callsToShow[i]}()\n`;
407
+ }
408
+ if (item.calls.length > 5) {
409
+ output += `${indent} └─ ... and ${item.calls.length - 5} more\n`;
410
+ }
411
+ }
412
+ }
413
+
414
+ return output;
415
+ }
416
+
417
+ /**
418
+ * Generate call chain summary
419
+ * @param {object[]} chain - Call chain from traceCallChain
420
+ * @returns {object} Summary statistics
421
+ */
422
+ function summarizeCallChain(chain) {
423
+ return {
424
+ totalFunctions: chain.length,
425
+ maxDepth: Math.max(...chain.map(c => c.depth), 0),
426
+ tracedSuccessfully: chain.filter(c => c.status === 'traced').length,
427
+ notFound: chain.filter(c => c.status === 'not-found').length,
428
+ errors: chain.filter(c => c.status === 'error').length,
429
+ uniqueFiles: [...new Set(chain.map(c => c.file))].length,
430
+ allCalls: [...new Set(chain.flatMap(c => c.calls || []))]
431
+ };
432
+ }
433
+
434
+ module.exports = {
435
+ traceCallChain,
436
+ formatCallChainAscii,
437
+ summarizeCallChain,
438
+ findFunctionCalls,
439
+ extractFunctionBody,
440
+ detectLanguage,
441
+ resolveCallLocation,
442
+ CALL_PATTERNS,
443
+ EXCLUDED_CALLS
444
+ };
package/lib/detector.js CHANGED
@@ -156,6 +156,84 @@ const TECH_SIGNATURES = {
156
156
  },
157
157
  };
158
158
 
159
+ /**
160
+ * Entry point patterns for different frameworks
161
+ */
162
+ const ENTRY_POINT_PATTERNS = {
163
+ express: {
164
+ regex: /app\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/g,
165
+ extractor: (match) => ({ method: match[1].toUpperCase(), route: match[2] })
166
+ },
167
+ fastapi: {
168
+ regex: /@app\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/g,
169
+ extractor: (match) => ({ method: match[1].toUpperCase(), route: match[2] })
170
+ },
171
+ nextjs: {
172
+ regex: /export\s+(async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)/g,
173
+ extractor: (match) => ({ method: match[2], route: 'app-router' })
174
+ },
175
+ django: {
176
+ regex: /path\s*\(\s*['"`]([^'"`]+)['"`]/g,
177
+ extractor: (match) => ({ method: 'ANY', route: match[1] })
178
+ },
179
+ rails: {
180
+ regex: /(get|post|put|delete|patch)\s+['"`]([^'"`]+)['"`]/g,
181
+ extractor: (match) => ({ method: match[1].toUpperCase(), route: match[2] })
182
+ },
183
+ nestjs: {
184
+ regex: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]?([^'"`)\s]*)['"`]?\s*\)/g,
185
+ extractor: (match) => ({ method: match[1].toUpperCase(), route: match[2] || '/' })
186
+ }
187
+ };
188
+
189
+ /**
190
+ * Detect entry points in source files
191
+ */
192
+ async function detectEntryPoints(projectRoot, frameworks) {
193
+ const entryPoints = [];
194
+
195
+ for (const framework of frameworks) {
196
+ const pattern = ENTRY_POINT_PATTERNS[framework];
197
+ if (!pattern) continue;
198
+
199
+ // Get appropriate file extension
200
+ const lang = TECH_SIGNATURES.frameworks[framework]?.language || 'javascript';
201
+ const ext = TECH_SIGNATURES.languages[lang]?.extensions?.[0] || '.js';
202
+
203
+ try {
204
+ const files = await glob(`**/*${ext}`, {
205
+ cwd: projectRoot,
206
+ ignore: ['node_modules/**', 'vendor/**', '.git/**', 'dist/**', 'build/**', '__pycache__/**'],
207
+ nodir: true,
208
+ });
209
+
210
+ for (const file of files.slice(0, 20)) { // Limit to 20 files
211
+ try {
212
+ const content = fs.readFileSync(path.join(projectRoot, file), 'utf8');
213
+ let match;
214
+
215
+ while ((match = pattern.regex.exec(content)) !== null) {
216
+ const entry = pattern.extractor(match);
217
+ entryPoints.push({
218
+ ...entry,
219
+ file,
220
+ line: content.substring(0, match.index).split('\n').length,
221
+ framework
222
+ });
223
+ }
224
+ pattern.regex.lastIndex = 0; // Reset regex
225
+ } catch {
226
+ // Skip unreadable files
227
+ }
228
+ }
229
+ } catch {
230
+ // Skip glob errors
231
+ }
232
+ }
233
+
234
+ return entryPoints;
235
+ }
236
+
159
237
  /**
160
238
  * Detect technology stack
161
239
  */
@@ -342,6 +420,9 @@ async function detectTechStack(projectRoot, options = {}) {
342
420
  }
343
421
  }
344
422
 
423
+ // Detect entry points for discovered frameworks
424
+ result.entryPoints = await detectEntryPoints(projectRoot, result.frameworks);
425
+
345
426
  // Build stack string
346
427
  const stackParts = [];
347
428
  if (result.languages.length > 0) {
@@ -369,5 +450,7 @@ function capitalize(str) {
369
450
 
370
451
  module.exports = {
371
452
  detectTechStack,
453
+ detectEntryPoints,
372
454
  TECH_SIGNATURES,
455
+ ENTRY_POINT_PATTERNS,
373
456
  };