@tyroneross/navgator 0.1.1 → 0.2.0

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.
Files changed (59) hide show
  1. package/dist/cli/index.js +201 -10
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/diagram.d.ts.map +1 -1
  4. package/dist/diagram.js +33 -0
  5. package/dist/diagram.js.map +1 -1
  6. package/dist/scanner.d.ts +2 -0
  7. package/dist/scanner.d.ts.map +1 -1
  8. package/dist/scanner.js +131 -12
  9. package/dist/scanner.js.map +1 -1
  10. package/dist/scanners/connections/ast-scanner.d.ts.map +1 -1
  11. package/dist/scanners/connections/ast-scanner.js +0 -3
  12. package/dist/scanners/connections/ast-scanner.js.map +1 -1
  13. package/dist/scanners/connections/llm-call-tracer.d.ts +83 -0
  14. package/dist/scanners/connections/llm-call-tracer.d.ts.map +1 -0
  15. package/dist/scanners/connections/llm-call-tracer.js +801 -0
  16. package/dist/scanners/connections/llm-call-tracer.js.map +1 -0
  17. package/dist/scanners/connections/service-calls.d.ts.map +1 -1
  18. package/dist/scanners/connections/service-calls.js +44 -47
  19. package/dist/scanners/connections/service-calls.js.map +1 -1
  20. package/dist/scanners/infrastructure/index.d.ts.map +1 -1
  21. package/dist/scanners/infrastructure/index.js +34 -4
  22. package/dist/scanners/infrastructure/index.js.map +1 -1
  23. package/dist/scanners/packages/swift.d.ts +14 -0
  24. package/dist/scanners/packages/swift.d.ts.map +1 -0
  25. package/dist/scanners/packages/swift.js +320 -0
  26. package/dist/scanners/packages/swift.js.map +1 -0
  27. package/dist/scanners/prompts/detector.d.ts +16 -0
  28. package/dist/scanners/prompts/detector.d.ts.map +1 -1
  29. package/dist/scanners/prompts/detector.js +90 -1
  30. package/dist/scanners/prompts/detector.js.map +1 -1
  31. package/dist/scanners/prompts/types.d.ts +4 -0
  32. package/dist/scanners/prompts/types.d.ts.map +1 -1
  33. package/dist/scanners/prompts/types.js.map +1 -1
  34. package/dist/scanners/swift/code-scanner.d.ts +14 -0
  35. package/dist/scanners/swift/code-scanner.d.ts.map +1 -0
  36. package/dist/scanners/swift/code-scanner.js +803 -0
  37. package/dist/scanners/swift/code-scanner.js.map +1 -0
  38. package/dist/storage.d.ts +2 -2
  39. package/dist/storage.d.ts.map +1 -1
  40. package/dist/storage.js +41 -2
  41. package/dist/storage.js.map +1 -1
  42. package/dist/types.d.ts +26 -2
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/dist/ui-server.d.ts +7 -7
  46. package/dist/ui-server.d.ts.map +1 -1
  47. package/dist/ui-server.js +80 -792
  48. package/dist/ui-server.js.map +1 -1
  49. package/package.json +15 -9
  50. package/web/public/apple-icon.png +0 -0
  51. package/web/public/icon-dark-32x32.png +0 -0
  52. package/web/public/icon-light-32x32.png +0 -0
  53. package/web/public/icon.svg +26 -0
  54. package/web/public/navgator-logo.png +0 -0
  55. package/web/public/placeholder-logo.png +0 -0
  56. package/web/public/placeholder-logo.svg +1 -0
  57. package/web/public/placeholder-user.jpg +0 -0
  58. package/web/public/placeholder.jpg +0 -0
  59. package/web/public/placeholder.svg +1 -0
@@ -0,0 +1,801 @@
1
+ /**
2
+ * LLM Call Tracer
3
+ *
4
+ * Anchor-based detection of AI/LLM API calls in source code.
5
+ * Instead of searching for "prompt-like" patterns everywhere,
6
+ * starts from unambiguous API call sites and traces backwards
7
+ * to find the provider, model, prompt content, and configuration.
8
+ *
9
+ * 4-pass approach:
10
+ * Pass 1: Find SDK imports & client initializations
11
+ * Pass 2: Find API call sites (anchors)
12
+ * Pass 3: Map wrapper functions
13
+ * Pass 4: Extract call arguments (model, messages, config)
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import { glob } from 'glob';
18
+ import { generateConnectionId, generateComponentId, } from '../../types.js';
19
+ const SDK_DEFINITIONS = [
20
+ // OpenAI
21
+ {
22
+ packageNames: ['openai'],
23
+ providerName: 'openai',
24
+ classNames: ['OpenAI', 'OpenAIApi'],
25
+ callPatterns: [
26
+ { pattern: /\.chat\.completions\.create\s*\(/, method: 'chat.completions.create', callType: 'chat', requiresClientVar: true },
27
+ { pattern: /\.completions\.create\s*\(/, method: 'completions.create', callType: 'completion', requiresClientVar: true },
28
+ { pattern: /\.embeddings\.create\s*\(/, method: 'embeddings.create', callType: 'embedding', requiresClientVar: true },
29
+ { pattern: /\.images\.generate\s*\(/, method: 'images.generate', callType: 'image', requiresClientVar: true },
30
+ { pattern: /\.audio\.transcriptions\s*\.create\s*\(/, method: 'audio.transcriptions.create', callType: 'audio', requiresClientVar: true },
31
+ ],
32
+ },
33
+ // Anthropic
34
+ {
35
+ packageNames: ['@anthropic-ai/sdk'],
36
+ providerName: 'anthropic',
37
+ classNames: ['Anthropic'],
38
+ callPatterns: [
39
+ { pattern: /\.messages\.create\s*\(/, method: 'messages.create', callType: 'chat', requiresClientVar: true },
40
+ { pattern: /\.completions\.create\s*\(/, method: 'completions.create', callType: 'completion', requiresClientVar: true },
41
+ { pattern: /\.beta\./, method: 'beta', callType: 'chat', requiresClientVar: true },
42
+ ],
43
+ },
44
+ // Groq
45
+ {
46
+ packageNames: ['groq-sdk'],
47
+ providerName: 'groq',
48
+ classNames: ['Groq'],
49
+ callPatterns: [
50
+ { pattern: /\.chat\.completions\.create\s*\(/, method: 'chat.completions.create', callType: 'chat', requiresClientVar: true },
51
+ ],
52
+ },
53
+ // Cohere
54
+ {
55
+ packageNames: ['cohere-ai', 'cohere'],
56
+ providerName: 'cohere',
57
+ classNames: ['CohereClient', 'Cohere'],
58
+ callPatterns: [
59
+ { pattern: /\.generate\s*\(/, method: 'generate', callType: 'completion', requiresClientVar: true },
60
+ { pattern: /\.chat\s*\(/, method: 'chat', callType: 'chat', requiresClientVar: true },
61
+ { pattern: /\.embed\s*\(/, method: 'embed', callType: 'embedding', requiresClientVar: true },
62
+ ],
63
+ },
64
+ // Mistral
65
+ {
66
+ packageNames: ['@mistralai/mistralai'],
67
+ providerName: 'mistral',
68
+ classNames: ['MistralClient', 'Mistral'],
69
+ callPatterns: [
70
+ { pattern: /\.chat\s*\(/, method: 'chat', callType: 'chat', requiresClientVar: true },
71
+ { pattern: /\.chatStream\s*\(/, method: 'chatStream', callType: 'chat', requiresClientVar: true },
72
+ ],
73
+ },
74
+ // Vercel AI SDK (functional, no client var)
75
+ {
76
+ packageNames: ['ai', '@ai-sdk/openai', '@ai-sdk/anthropic', '@ai-sdk/google'],
77
+ providerName: 'vercel-ai-sdk',
78
+ classNames: [],
79
+ callPatterns: [
80
+ { pattern: /\bgenerateText\s*\(/, method: 'generateText', callType: 'completion', requiresClientVar: false },
81
+ { pattern: /\bstreamText\s*\(/, method: 'streamText', callType: 'chat', requiresClientVar: false },
82
+ { pattern: /\bgenerateObject\s*\(/, method: 'generateObject', callType: 'function-call', requiresClientVar: false },
83
+ { pattern: /\bstreamObject\s*\(/, method: 'streamObject', callType: 'function-call', requiresClientVar: false },
84
+ { pattern: /\bembed\s*\(/, method: 'embed', callType: 'embedding', requiresClientVar: false },
85
+ { pattern: /\bembedMany\s*\(/, method: 'embedMany', callType: 'embedding', requiresClientVar: false },
86
+ ],
87
+ },
88
+ // LangChain
89
+ {
90
+ packageNames: ['@langchain/openai', '@langchain/anthropic', '@langchain/groq', '@langchain/core', '@langchain/community', 'langchain'],
91
+ providerName: 'langchain',
92
+ classNames: ['ChatOpenAI', 'ChatAnthropic', 'ChatGroq', 'ChatGoogleGenerativeAI'],
93
+ callPatterns: [
94
+ { pattern: /\.invoke\s*\(/, method: 'invoke', callType: 'chat', requiresClientVar: true },
95
+ { pattern: /\.call\s*\(/, method: 'call', callType: 'chat', requiresClientVar: true },
96
+ { pattern: /\.stream\s*\(/, method: 'stream', callType: 'chat', requiresClientVar: true },
97
+ { pattern: /\.batch\s*\(/, method: 'batch', callType: 'chat', requiresClientVar: true },
98
+ ],
99
+ },
100
+ ];
101
+ // =============================================================================
102
+ // FILE EXCLUSIONS
103
+ // =============================================================================
104
+ function shouldExcludeFile(file) {
105
+ const excludePatterns = [
106
+ /NavGator\/src\//,
107
+ /NavGator\/web\//,
108
+ /\/__tests__\//,
109
+ /\/test\//,
110
+ /\/tests\//,
111
+ /\/mocks?\//,
112
+ /\/fixtures?\//,
113
+ /\.test\.(ts|tsx|js|jsx)$/,
114
+ /\.spec\.(ts|tsx|js|jsx)$/,
115
+ /\.mock\.(ts|tsx|js|jsx)$/,
116
+ /\.(d\.ts|map|min\.js)$/,
117
+ /\/dist\//,
118
+ /\/build\//,
119
+ /\/generated\//,
120
+ ];
121
+ return excludePatterns.some(p => p.test(file));
122
+ }
123
+ // =============================================================================
124
+ // PASS 1: FIND SDK IMPORTS
125
+ // =============================================================================
126
+ function findSDKImports(content, lines, file) {
127
+ const imports = [];
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const line = lines[i];
130
+ for (const sdk of SDK_DEFINITIONS) {
131
+ for (const pkg of sdk.packageNames) {
132
+ // Static imports: import X from 'pkg' or import { X } from 'pkg'
133
+ const staticImport = line.match(new RegExp(`import\\s+(?:(?:\\{\\s*([^}]+)\\s*\\})|(?:(\\w+)))\\s+from\\s+['"]${escapeRegex(pkg)}['"]`));
134
+ if (staticImport) {
135
+ const names = staticImport[1]
136
+ ? staticImport[1].split(',').map(n => n.trim().split(/\s+as\s+/).pop().trim())
137
+ : [staticImport[2]];
138
+ imports.push({
139
+ file,
140
+ line: i + 1,
141
+ sdk: pkg,
142
+ providerName: sdk.providerName,
143
+ importedNames: names.filter(Boolean),
144
+ });
145
+ continue;
146
+ }
147
+ // Dynamic imports: const { X } = await import('pkg')
148
+ const dynamicImport = line.match(new RegExp(`(?:const|let|var)\\s+(?:\\{\\s*([^}]+)\\s*\\}|(\\w+))\\s*=\\s*(?:await\\s+)?import\\s*\\(\\s*['"]${escapeRegex(pkg)}['"]`));
149
+ if (dynamicImport) {
150
+ const names = dynamicImport[1]
151
+ ? dynamicImport[1].split(',').map(n => n.trim())
152
+ : [dynamicImport[2]];
153
+ imports.push({
154
+ file,
155
+ line: i + 1,
156
+ sdk: pkg,
157
+ providerName: sdk.providerName,
158
+ importedNames: names.filter(Boolean),
159
+ });
160
+ continue;
161
+ }
162
+ // require(): const X = require('pkg')
163
+ const requireImport = line.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*require\\s*\\(\\s*['"]${escapeRegex(pkg)}['"]`));
164
+ if (requireImport) {
165
+ imports.push({
166
+ file,
167
+ line: i + 1,
168
+ sdk: pkg,
169
+ providerName: sdk.providerName,
170
+ importedNames: [requireImport[1]],
171
+ });
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return imports;
177
+ }
178
+ // =============================================================================
179
+ // PASS 1B: FIND CLIENT INITIALIZATIONS
180
+ // =============================================================================
181
+ function findClientInits(lines, file, imports) {
182
+ const inits = [];
183
+ // Build set of imported class names for this file
184
+ const importedClasses = new Map();
185
+ for (const imp of imports) {
186
+ for (const name of imp.importedNames) {
187
+ importedClasses.set(name, imp);
188
+ }
189
+ }
190
+ for (let i = 0; i < lines.length; i++) {
191
+ const line = lines[i];
192
+ // Match: const x = new ClassName(...)
193
+ const initMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+(\w+)\s*\(/);
194
+ if (initMatch) {
195
+ const [, varName, className] = initMatch;
196
+ // Check if the class is from a known SDK
197
+ const imp = importedClasses.get(className);
198
+ if (imp) {
199
+ inits.push({
200
+ file,
201
+ line: i + 1,
202
+ variableName: varName,
203
+ sdk: imp.sdk,
204
+ providerName: imp.providerName,
205
+ className,
206
+ });
207
+ continue;
208
+ }
209
+ // Also check SDK definitions directly for the class name
210
+ for (const sdk of SDK_DEFINITIONS) {
211
+ if (sdk.classNames.includes(className)) {
212
+ inits.push({
213
+ file,
214
+ line: i + 1,
215
+ variableName: varName,
216
+ sdk: sdk.packageNames[0],
217
+ providerName: sdk.providerName,
218
+ className,
219
+ });
220
+ break;
221
+ }
222
+ }
223
+ }
224
+ // Match: this.client = new ClassName(...)
225
+ const thisInitMatch = line.match(/this\.(\w+)\s*=\s*new\s+(\w+)\s*\(/);
226
+ if (thisInitMatch) {
227
+ const [, propName, className] = thisInitMatch;
228
+ const imp = importedClasses.get(className);
229
+ if (imp) {
230
+ inits.push({
231
+ file,
232
+ line: i + 1,
233
+ variableName: `this.${propName}`,
234
+ sdk: imp.sdk,
235
+ providerName: imp.providerName,
236
+ className,
237
+ });
238
+ }
239
+ }
240
+ }
241
+ return inits;
242
+ }
243
+ // =============================================================================
244
+ // PASS 2: FIND API CALL ANCHORS
245
+ // =============================================================================
246
+ function findCallAnchors(lines, file, imports, clientInits) {
247
+ const anchors = [];
248
+ // Build lookup of known client variables
249
+ const clientVars = new Map();
250
+ for (const init of clientInits) {
251
+ clientVars.set(init.variableName, init);
252
+ // Also track without 'this.' prefix for method access
253
+ if (init.variableName.startsWith('this.')) {
254
+ clientVars.set(init.variableName.replace('this.', ''), init);
255
+ }
256
+ }
257
+ // Build set of imported function names (for functional SDKs like Vercel AI)
258
+ const importedFunctions = new Set();
259
+ for (const imp of imports) {
260
+ for (const name of imp.importedNames) {
261
+ importedFunctions.add(name);
262
+ }
263
+ }
264
+ for (let i = 0; i < lines.length; i++) {
265
+ const line = lines[i];
266
+ // Skip comments
267
+ const trimmed = line.trimStart();
268
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('#')) {
269
+ continue;
270
+ }
271
+ for (const sdk of SDK_DEFINITIONS) {
272
+ for (const cp of sdk.callPatterns) {
273
+ if (!cp.pattern.test(line))
274
+ continue;
275
+ if (cp.requiresClientVar) {
276
+ // OOP style: find the variable calling the method
277
+ // Match: varName.method( or this.varName.method(
278
+ const varMatch = line.match(/(\w+(?:\.\w+)?)\.(?:chat|messages|completions|embeddings|images|audio|beta)/);
279
+ if (!varMatch)
280
+ continue;
281
+ const callerVar = varMatch[1];
282
+ // Check if this variable is a known client
283
+ const clientInit = clientVars.get(callerVar);
284
+ if (clientInit && clientInit.providerName === sdk.providerName) {
285
+ const funcName = extractFunctionName(lines, i);
286
+ anchors.push({
287
+ file,
288
+ line: i + 1,
289
+ code: line.trim().slice(0, 120),
290
+ method: cp.method,
291
+ clientVariable: callerVar,
292
+ providerName: sdk.providerName,
293
+ sdk: clientInit.sdk,
294
+ callType: cp.callType,
295
+ containingFunction: funcName,
296
+ });
297
+ break;
298
+ }
299
+ // For LangChain .invoke() / .call() — match chains and LangChain model instances
300
+ if (sdk.providerName === 'langchain') {
301
+ // Check if file has any LangChain imports
302
+ const hasLangChainImport = imports.some(imp => imp.providerName === 'langchain');
303
+ if (hasLangChainImport) {
304
+ const funcName = extractFunctionName(lines, i);
305
+ anchors.push({
306
+ file,
307
+ line: i + 1,
308
+ code: line.trim().slice(0, 120),
309
+ method: cp.method,
310
+ clientVariable: callerVar,
311
+ providerName: 'langchain',
312
+ sdk: imports.find(imp => imp.providerName === 'langchain')?.sdk || 'langchain',
313
+ callType: cp.callType,
314
+ containingFunction: funcName,
315
+ });
316
+ break;
317
+ }
318
+ }
319
+ }
320
+ else {
321
+ // Functional style (Vercel AI SDK): generateText(...)
322
+ // Check if the function was imported from the right package
323
+ const funcMatch = line.match(new RegExp(`\\b(${sdk.callPatterns.map(p => {
324
+ const src = p.pattern.source;
325
+ // Extract function name from pattern like \bgenerateText\s*\(
326
+ const m = src.match(/\\b(\w+)/);
327
+ return m ? m[1] : '';
328
+ }).filter(Boolean).join('|')})\\s*\\(`));
329
+ if (funcMatch && importedFunctions.has(funcMatch[1])) {
330
+ const funcName = extractFunctionName(lines, i);
331
+ anchors.push({
332
+ file,
333
+ line: i + 1,
334
+ code: line.trim().slice(0, 120),
335
+ method: funcMatch[1],
336
+ clientVariable: funcMatch[1],
337
+ providerName: sdk.providerName,
338
+ sdk: sdk.packageNames[0],
339
+ callType: cp.callType,
340
+ containingFunction: funcName,
341
+ });
342
+ break;
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+ return anchors;
349
+ }
350
+ // =============================================================================
351
+ // PASS 3: MAP WRAPPER FUNCTIONS
352
+ // =============================================================================
353
+ function mapWrapperFunctions(anchors, fileContents) {
354
+ const wrappers = [];
355
+ // Group anchors by file+function
356
+ const anchorsByFunction = new Map();
357
+ for (const anchor of anchors) {
358
+ if (anchor.containingFunction) {
359
+ const key = `${anchor.file}::${anchor.containingFunction}`;
360
+ if (!anchorsByFunction.has(key)) {
361
+ anchorsByFunction.set(key, []);
362
+ }
363
+ anchorsByFunction.get(key).push(anchor);
364
+ }
365
+ }
366
+ // For each function that contains an anchor, build a wrapper entry
367
+ for (const [key, fnAnchors] of anchorsByFunction) {
368
+ const [file, funcName] = key.split('::');
369
+ const fileData = fileContents.get(file);
370
+ if (!fileData)
371
+ continue;
372
+ // Check if function has @traceable() decorator or LangSmith wrapping
373
+ const hasTraceable = fileData.content.includes('traceable(') ||
374
+ fileData.content.includes('@traceable');
375
+ // Detect class membership
376
+ const className = detectClassName(fileData.lines, fnAnchors[0].line - 1);
377
+ // Check if the function is exported
378
+ const exportedAs = detectExport(fileData.lines, funcName);
379
+ wrappers.push({
380
+ file,
381
+ functionName: funcName,
382
+ className,
383
+ exportedAs,
384
+ containedAnchors: fnAnchors,
385
+ hasTraceable,
386
+ });
387
+ }
388
+ return wrappers;
389
+ }
390
+ // =============================================================================
391
+ // PASS 4: EXTRACT CALL ARGUMENTS
392
+ // =============================================================================
393
+ function extractCallArguments(anchor, lines) {
394
+ const anchorLine = anchor.line - 1;
395
+ // Look at the anchor line and the following lines for the arguments object
396
+ const contextLines = lines.slice(anchorLine, Math.min(anchorLine + 30, lines.length));
397
+ const context = contextLines.join('\n');
398
+ // Extract model
399
+ const model = extractModel(context, lines, anchorLine);
400
+ // Extract prompt/messages
401
+ const prompt = extractPromptInfo(context, lines, anchorLine);
402
+ // Extract config
403
+ const config = extractConfig(context);
404
+ return { model, prompt, config };
405
+ }
406
+ function extractModel(context, lines, anchorLine) {
407
+ // Look for model: "value" or model: variable
408
+ const modelStringMatch = context.match(/model\s*:\s*['"`]([^'"`]+)['"`]/);
409
+ if (modelStringMatch) {
410
+ return {
411
+ value: modelStringMatch[1],
412
+ isDynamic: false,
413
+ line: findLineOffset(lines, anchorLine, modelStringMatch[0]),
414
+ };
415
+ }
416
+ // Model as variable reference
417
+ const modelVarMatch = context.match(/model\s*:\s*(\w+(?:\.\w+)*)/);
418
+ if (modelVarMatch) {
419
+ const varName = modelVarMatch[1];
420
+ // Try to resolve the variable value
421
+ const resolved = resolveVariable(lines, anchorLine, varName);
422
+ return {
423
+ value: resolved || null,
424
+ isDynamic: !resolved,
425
+ variableName: varName,
426
+ line: findLineOffset(lines, anchorLine, modelVarMatch[0]),
427
+ };
428
+ }
429
+ return { value: null, isDynamic: true, line: anchorLine + 1 };
430
+ }
431
+ function extractPromptInfo(context, lines, anchorLine) {
432
+ // Check for messages array
433
+ const messagesMatch = context.match(/messages\s*:\s*\[/);
434
+ if (messagesMatch) {
435
+ // Try to extract system prompt from inline messages
436
+ const systemMatch = context.match(/role\s*:\s*['"]system['"]\s*,\s*content\s*:\s*(?:['"`]([^'"`]{0,500})['"`]|(\w+))/);
437
+ // Try to extract user template
438
+ const userMatch = context.match(/role\s*:\s*['"]user['"]\s*,\s*content\s*:\s*(?:['"`]([^'"`]{0,500})['"`]|(\w+))/);
439
+ // Detect template variables
440
+ const variables = detectTemplateVars(context);
441
+ return {
442
+ type: 'messages-array',
443
+ content: userMatch?.[1] || userMatch?.[2] || undefined,
444
+ systemPrompt: systemMatch?.[1] || systemMatch?.[2] || undefined,
445
+ hasUserTemplate: !!userMatch,
446
+ variables,
447
+ };
448
+ }
449
+ // Check for messages as variable reference
450
+ const messagesVarMatch = context.match(/messages\s*:\s*(\w+)/);
451
+ if (messagesVarMatch) {
452
+ return {
453
+ type: 'variable-ref',
454
+ content: undefined,
455
+ systemPrompt: undefined,
456
+ hasUserTemplate: false,
457
+ variables: [],
458
+ };
459
+ }
460
+ // Check for prompt: string (Vercel AI SDK style)
461
+ const promptStringMatch = context.match(/prompt\s*:\s*['"`]([^'"`]{0,500})/);
462
+ if (promptStringMatch) {
463
+ return {
464
+ type: 'string-prompt',
465
+ content: promptStringMatch[1],
466
+ hasUserTemplate: true,
467
+ variables: detectTemplateVars(promptStringMatch[1]),
468
+ };
469
+ }
470
+ // Check for system: string (Vercel AI SDK style)
471
+ const systemStringMatch = context.match(/system\s*:\s*['"`]([^'"`]{0,500})/);
472
+ return {
473
+ type: 'string-prompt',
474
+ content: undefined,
475
+ systemPrompt: systemStringMatch?.[1] || undefined,
476
+ hasUserTemplate: false,
477
+ variables: [],
478
+ };
479
+ }
480
+ function extractConfig(context) {
481
+ const config = {};
482
+ const tempMatch = context.match(/temperature\s*:\s*([\d.]+)/);
483
+ if (tempMatch)
484
+ config.temperature = parseFloat(tempMatch[1]);
485
+ const maxTokensMatch = context.match(/(?:max_tokens|maxTokens)\s*:\s*(\d+)/);
486
+ if (maxTokensMatch)
487
+ config.maxTokens = parseInt(maxTokensMatch[1]);
488
+ const streamMatch = context.match(/stream\s*:\s*(true|false)/);
489
+ if (streamMatch)
490
+ config.stream = streamMatch[1] === 'true';
491
+ const toolsMatch = context.match(/tools\s*:\s*\[/);
492
+ if (toolsMatch)
493
+ config.tools = ['detected'];
494
+ return config;
495
+ }
496
+ export async function traceLLMCalls(projectRoot) {
497
+ const sourceFiles = await glob('**/*.{ts,tsx,js,jsx,py}', {
498
+ cwd: projectRoot,
499
+ ignore: [
500
+ 'node_modules/**', 'dist/**', 'build/**', '.next/**',
501
+ '__pycache__/**', 'venv/**', '**/node_modules/**', '**/.git/**',
502
+ ],
503
+ });
504
+ // Read all source files
505
+ const fileContents = new Map();
506
+ for (const file of sourceFiles) {
507
+ if (shouldExcludeFile(file))
508
+ continue;
509
+ const filePath = path.join(projectRoot, file);
510
+ try {
511
+ const stat = await fs.promises.stat(filePath);
512
+ if (!stat.isFile())
513
+ continue;
514
+ const content = await fs.promises.readFile(filePath, 'utf-8');
515
+ fileContents.set(file, { content, lines: content.split('\n') });
516
+ }
517
+ catch {
518
+ continue;
519
+ }
520
+ }
521
+ // -------------------------------------------------------------------------
522
+ // Pass 1: Find SDK imports and client initializations
523
+ // -------------------------------------------------------------------------
524
+ const allImports = [];
525
+ const allClientInits = [];
526
+ for (const [file, { content, lines }] of fileContents) {
527
+ const imports = findSDKImports(content, lines, file);
528
+ allImports.push(...imports);
529
+ const inits = findClientInits(lines, file, imports);
530
+ allClientInits.push(...inits);
531
+ }
532
+ // -------------------------------------------------------------------------
533
+ // Pass 2: Find API call anchors
534
+ // -------------------------------------------------------------------------
535
+ const allAnchors = [];
536
+ for (const [file, { content, lines }] of fileContents) {
537
+ // Only scan files that have SDK imports or that use known client variables
538
+ const fileImports = allImports.filter(i => i.file === file);
539
+ const fileInits = allClientInits.filter(i => i.file === file);
540
+ // Also include inits from other files that might be imported here
541
+ const importedVars = findImportedClientVars(lines, file, allClientInits);
542
+ const combinedInits = [...fileInits, ...importedVars];
543
+ if (fileImports.length === 0 && combinedInits.length === 0)
544
+ continue;
545
+ const anchors = findCallAnchors(lines, file, fileImports, combinedInits);
546
+ allAnchors.push(...anchors);
547
+ }
548
+ // -------------------------------------------------------------------------
549
+ // Pass 3: Map wrapper functions
550
+ // -------------------------------------------------------------------------
551
+ const wrappers = mapWrapperFunctions(allAnchors, fileContents);
552
+ // -------------------------------------------------------------------------
553
+ // Pass 4: Extract arguments and build TracedLLMCalls
554
+ // -------------------------------------------------------------------------
555
+ const tracedCalls = [];
556
+ const seen = new Set(); // Dedup by file:line
557
+ for (const anchor of allAnchors) {
558
+ const dedupKey = `${anchor.file}:${anchor.line}`;
559
+ if (seen.has(dedupKey))
560
+ continue;
561
+ seen.add(dedupKey);
562
+ const fileData = fileContents.get(anchor.file);
563
+ if (!fileData)
564
+ continue;
565
+ const args = extractCallArguments(anchor, fileData.lines);
566
+ // Find the matching SDK import
567
+ const matchingImport = allImports.find(i => i.file === anchor.file && i.providerName === anchor.providerName) || allImports.find(i => i.providerName === anchor.providerName);
568
+ const call = {
569
+ id: `TRACE_${anchor.file.replace(/[^a-zA-Z0-9]/g, '_')}_L${anchor.line}`,
570
+ name: anchor.containingFunction || `anonymous_${anchor.line}`,
571
+ anchor: {
572
+ file: anchor.file,
573
+ line: anchor.line,
574
+ code: anchor.code,
575
+ method: anchor.method,
576
+ },
577
+ provider: {
578
+ name: anchor.providerName,
579
+ sdk: anchor.sdk,
580
+ importLine: matchingImport?.line || 0,
581
+ clientVariable: anchor.clientVariable,
582
+ },
583
+ model: args.model || { value: null, isDynamic: true, line: anchor.line },
584
+ prompt: args.prompt || {
585
+ type: 'variable-ref',
586
+ hasUserTemplate: false,
587
+ variables: [],
588
+ },
589
+ config: args.config || {},
590
+ callType: anchor.callType,
591
+ confidence: computeConfidence(anchor, matchingImport, args),
592
+ };
593
+ tracedCalls.push(call);
594
+ }
595
+ // -------------------------------------------------------------------------
596
+ // Convert to NavGator ScanResult format
597
+ // -------------------------------------------------------------------------
598
+ const scanResult = convertToScanResult(tracedCalls, allImports);
599
+ return { calls: tracedCalls, wrappers, scanResult };
600
+ }
601
+ // =============================================================================
602
+ // CONVERSION TO SCAN RESULT
603
+ // =============================================================================
604
+ function convertToScanResult(calls, imports) {
605
+ const components = [];
606
+ const connections = [];
607
+ const timestamp = Date.now();
608
+ // Create a component per unique provider
609
+ const providerComponents = new Map();
610
+ for (const call of calls) {
611
+ const providerKey = call.provider.name;
612
+ if (!providerComponents.has(providerKey)) {
613
+ const comp = {
614
+ component_id: generateComponentId('llm', providerKey),
615
+ name: providerKey,
616
+ type: 'llm',
617
+ role: {
618
+ purpose: `${providerKey} AI API`,
619
+ layer: 'external',
620
+ critical: true,
621
+ },
622
+ source: {
623
+ detection_method: 'auto',
624
+ config_files: [],
625
+ confidence: Math.max(...calls.filter(c => c.provider.name === providerKey).map(c => c.confidence)),
626
+ },
627
+ connects_to: [],
628
+ connected_from: [],
629
+ status: 'active',
630
+ tags: ['llm', 'external', providerKey],
631
+ timestamp,
632
+ last_updated: timestamp,
633
+ };
634
+ providerComponents.set(providerKey, comp);
635
+ components.push(comp);
636
+ }
637
+ }
638
+ // Create a connection per call site
639
+ for (const call of calls) {
640
+ const providerComp = providerComponents.get(call.provider.name);
641
+ const conn = {
642
+ connection_id: generateConnectionId('service-call'),
643
+ from: {
644
+ component_id: `FILE:${call.anchor.file}`,
645
+ location: {
646
+ file: call.anchor.file,
647
+ line: call.anchor.line,
648
+ function: call.name,
649
+ },
650
+ },
651
+ to: {
652
+ component_id: providerComp.component_id,
653
+ },
654
+ connection_type: 'service-call',
655
+ code_reference: {
656
+ file: call.anchor.file,
657
+ symbol: call.name,
658
+ symbol_type: 'function',
659
+ line_start: call.anchor.line,
660
+ code_snippet: call.anchor.code,
661
+ },
662
+ description: `${call.provider.name}.${call.anchor.method}${call.model.value ? ` (${call.model.value})` : ''}`,
663
+ detected_from: 'LLM call tracer (anchor-based)',
664
+ confidence: call.confidence,
665
+ timestamp,
666
+ last_verified: timestamp,
667
+ };
668
+ connections.push(conn);
669
+ }
670
+ return { components, connections, warnings: [] };
671
+ }
672
+ // =============================================================================
673
+ // HELPERS
674
+ // =============================================================================
675
+ function escapeRegex(str) {
676
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
677
+ }
678
+ function extractFunctionName(lines, lineIndex) {
679
+ for (let i = lineIndex; i >= Math.max(0, lineIndex - 30); i--) {
680
+ const line = lines[i];
681
+ // JS/TS function patterns
682
+ const funcMatch = line.match(/(?:async\s+)?(?:function\s+)?(\w+)\s*(?:=\s*(?:async\s*)?\(|[\(:])/);
683
+ if (funcMatch && funcMatch[1] !== 'if' && funcMatch[1] !== 'for' && funcMatch[1] !== 'while') {
684
+ return funcMatch[1];
685
+ }
686
+ // Arrow function assignment
687
+ const arrowMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/);
688
+ if (arrowMatch)
689
+ return arrowMatch[1];
690
+ // Method definition
691
+ const methodMatch = line.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
692
+ if (methodMatch && methodMatch[1] !== 'if' && methodMatch[1] !== 'for') {
693
+ return methodMatch[1];
694
+ }
695
+ // Python function
696
+ const pyMatch = line.match(/(?:async\s+)?def\s+(\w+)\s*\(/);
697
+ if (pyMatch)
698
+ return pyMatch[1];
699
+ }
700
+ return undefined;
701
+ }
702
+ function detectClassName(lines, lineIndex) {
703
+ for (let i = lineIndex; i >= Math.max(0, lineIndex - 100); i--) {
704
+ const line = lines[i];
705
+ const classMatch = line.match(/class\s+(\w+)/);
706
+ if (classMatch)
707
+ return classMatch[1];
708
+ }
709
+ return undefined;
710
+ }
711
+ function detectExport(lines, funcName) {
712
+ for (const line of lines) {
713
+ if (line.includes(`export`) && line.includes(funcName)) {
714
+ return funcName;
715
+ }
716
+ }
717
+ return undefined;
718
+ }
719
+ function detectTemplateVars(content) {
720
+ const vars = [];
721
+ const seen = new Set();
722
+ // JS template literals: ${varName}
723
+ const jsVars = content.matchAll(/\$\{(\w+)\}/g);
724
+ for (const m of jsVars) {
725
+ if (!seen.has(m[1])) {
726
+ seen.add(m[1]);
727
+ vars.push(m[1]);
728
+ }
729
+ }
730
+ // Jinja/Mustache: {{varName}}
731
+ const jinjaVars = content.matchAll(/\{\{\s*(\w+)\s*\}\}/g);
732
+ for (const m of jinjaVars) {
733
+ if (!seen.has(m[1])) {
734
+ seen.add(m[1]);
735
+ vars.push(m[1]);
736
+ }
737
+ }
738
+ return vars;
739
+ }
740
+ function resolveVariable(lines, fromLine, varName) {
741
+ // Simple resolution: look for const/let/var assignment above
742
+ const parts = varName.split('.');
743
+ const baseName = parts[0];
744
+ for (let i = fromLine; i >= Math.max(0, fromLine - 50); i--) {
745
+ const line = lines[i];
746
+ const match = line.match(new RegExp(`(?:const|let|var)\\s+${escapeRegex(baseName)}\\s*=\\s*['"\`]([^'"\`]+)['"\`]`));
747
+ if (match)
748
+ return match[1];
749
+ }
750
+ // Check for common model constant patterns
751
+ for (let i = 0; i < Math.min(50, lines.length); i++) {
752
+ const line = lines[i];
753
+ const match = line.match(new RegExp(`(?:const|let|var)\\s+${escapeRegex(baseName)}\\s*=\\s*['"\`]([^'"\`]+)['"\`]`));
754
+ if (match)
755
+ return match[1];
756
+ }
757
+ return null;
758
+ }
759
+ function findImportedClientVars(lines, file, allInits) {
760
+ // If file imports a module that exports a client init, track it
761
+ const imported = [];
762
+ for (const line of lines) {
763
+ // Look for imports that might bring in client instances
764
+ const importMatch = line.match(/import\s+\{?\s*(\w+)\s*\}?\s+from\s+['"]([^'"]+)['"]/);
765
+ if (importMatch) {
766
+ const importedName = importMatch[1];
767
+ // Check if any file exports a client init with this name
768
+ for (const init of allInits) {
769
+ if (init.variableName === importedName || init.className === importedName) {
770
+ imported.push({ ...init, file, variableName: importedName });
771
+ }
772
+ }
773
+ }
774
+ }
775
+ return imported;
776
+ }
777
+ function findLineOffset(lines, startLine, searchStr) {
778
+ for (let i = startLine; i < Math.min(startLine + 30, lines.length); i++) {
779
+ if (lines[i].includes(searchStr.split(':')[0])) {
780
+ return i + 1;
781
+ }
782
+ }
783
+ return startLine + 1;
784
+ }
785
+ function computeConfidence(anchor, matchingImport, args) {
786
+ let confidence = 0.6; // Base: we found an anchor
787
+ // Has corroborating import
788
+ if (matchingImport)
789
+ confidence += 0.15;
790
+ // Has resolved model
791
+ if (args.model?.value)
792
+ confidence += 0.1;
793
+ // Has prompt content
794
+ if (args.prompt?.content || args.prompt?.systemPrompt)
795
+ confidence += 0.1;
796
+ // Has config extracted
797
+ if (args.config && Object.keys(args.config).length > 0)
798
+ confidence += 0.05;
799
+ return Math.min(confidence, 1.0);
800
+ }
801
+ //# sourceMappingURL=llm-call-tracer.js.map