@vibecheckai/cli 3.2.5 → 3.3.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 (197) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +192 -5
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  6. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  7. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  8. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  11. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  12. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  13. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  14. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  15. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  16. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  17. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  18. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  19. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  20. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  21. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  22. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  23. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  24. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  25. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  26. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  27. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  28. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  29. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  30. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  31. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  32. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  35. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  36. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  37. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  38. package/bin/runners/lib/analyzers.js +81 -18
  39. package/bin/runners/lib/api-client.js +269 -0
  40. package/bin/runners/lib/auth-truth.js +193 -193
  41. package/bin/runners/lib/authority-badge.js +425 -0
  42. package/bin/runners/lib/backup.js +62 -62
  43. package/bin/runners/lib/billing.js +107 -107
  44. package/bin/runners/lib/claims.js +118 -118
  45. package/bin/runners/lib/cli-output.js +7 -1
  46. package/bin/runners/lib/cli-ui.js +540 -540
  47. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  48. package/bin/runners/lib/contracts/env-contract.js +181 -181
  49. package/bin/runners/lib/contracts/external-contract.js +206 -206
  50. package/bin/runners/lib/contracts/guard.js +168 -168
  51. package/bin/runners/lib/contracts/index.js +89 -89
  52. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  53. package/bin/runners/lib/contracts/route-contract.js +199 -199
  54. package/bin/runners/lib/contracts.js +804 -804
  55. package/bin/runners/lib/detect.js +89 -89
  56. package/bin/runners/lib/doctor/autofix.js +254 -254
  57. package/bin/runners/lib/doctor/index.js +37 -37
  58. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  59. package/bin/runners/lib/doctor/modules/index.js +46 -46
  60. package/bin/runners/lib/doctor/modules/network.js +250 -250
  61. package/bin/runners/lib/doctor/modules/project.js +312 -312
  62. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  63. package/bin/runners/lib/doctor/modules/security.js +348 -348
  64. package/bin/runners/lib/doctor/modules/system.js +213 -213
  65. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  66. package/bin/runners/lib/doctor/reporter.js +262 -262
  67. package/bin/runners/lib/doctor/service.js +262 -262
  68. package/bin/runners/lib/doctor/types.js +113 -113
  69. package/bin/runners/lib/doctor/ui.js +263 -263
  70. package/bin/runners/lib/doctor-v2.js +608 -608
  71. package/bin/runners/lib/drift.js +425 -425
  72. package/bin/runners/lib/enforcement.js +72 -72
  73. package/bin/runners/lib/enterprise-detect.js +603 -603
  74. package/bin/runners/lib/enterprise-init.js +942 -942
  75. package/bin/runners/lib/env-resolver.js +417 -417
  76. package/bin/runners/lib/env-template.js +66 -66
  77. package/bin/runners/lib/env.js +189 -189
  78. package/bin/runners/lib/error-handler.js +16 -9
  79. package/bin/runners/lib/exit-codes.js +275 -0
  80. package/bin/runners/lib/extractors/client-calls.js +990 -990
  81. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  82. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  83. package/bin/runners/lib/extractors/index.js +363 -363
  84. package/bin/runners/lib/extractors/next-routes.js +524 -524
  85. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  86. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  87. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  88. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  89. package/bin/runners/lib/findings-schema.js +281 -281
  90. package/bin/runners/lib/firewall-prompt.js +50 -50
  91. package/bin/runners/lib/global-flags.js +37 -0
  92. package/bin/runners/lib/graph/graph-builder.js +265 -265
  93. package/bin/runners/lib/graph/html-renderer.js +413 -413
  94. package/bin/runners/lib/graph/index.js +32 -32
  95. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  96. package/bin/runners/lib/graph/static-extractor.js +518 -518
  97. package/bin/runners/lib/help-formatter.js +413 -0
  98. package/bin/runners/lib/html-report.js +650 -650
  99. package/bin/runners/lib/llm.js +75 -75
  100. package/bin/runners/lib/logger.js +38 -0
  101. package/bin/runners/lib/meter.js +61 -61
  102. package/bin/runners/lib/missions/evidence.js +126 -126
  103. package/bin/runners/lib/patch.js +40 -40
  104. package/bin/runners/lib/permissions/auth-model.js +213 -213
  105. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  106. package/bin/runners/lib/permissions/index.js +45 -45
  107. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  108. package/bin/runners/lib/pkgjson.js +28 -28
  109. package/bin/runners/lib/policy.js +295 -295
  110. package/bin/runners/lib/preflight.js +142 -142
  111. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  112. package/bin/runners/lib/reality/index.js +318 -318
  113. package/bin/runners/lib/reality/request-hashing.js +416 -416
  114. package/bin/runners/lib/reality/request-mapper.js +453 -453
  115. package/bin/runners/lib/reality/safety-rails.js +463 -463
  116. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  117. package/bin/runners/lib/reality/toast-detector.js +393 -393
  118. package/bin/runners/lib/reality-findings.js +84 -84
  119. package/bin/runners/lib/receipts.js +179 -179
  120. package/bin/runners/lib/redact.js +29 -29
  121. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  122. package/bin/runners/lib/replay/index.js +263 -263
  123. package/bin/runners/lib/replay/player.js +348 -348
  124. package/bin/runners/lib/replay/recorder.js +331 -331
  125. package/bin/runners/lib/report.js +135 -135
  126. package/bin/runners/lib/route-detection.js +1140 -1140
  127. package/bin/runners/lib/sandbox/index.js +59 -59
  128. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  129. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  130. package/bin/runners/lib/sandbox/worktree.js +174 -174
  131. package/bin/runners/lib/schema-validator.js +350 -350
  132. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  133. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  134. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  135. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  136. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  137. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  138. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  139. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  140. package/bin/runners/lib/schemas/validator.js +438 -438
  141. package/bin/runners/lib/score-history.js +282 -282
  142. package/bin/runners/lib/share-pack.js +239 -239
  143. package/bin/runners/lib/snippets.js +67 -67
  144. package/bin/runners/lib/unified-cli-output.js +604 -0
  145. package/bin/runners/lib/upsell.js +658 -510
  146. package/bin/runners/lib/usage.js +153 -153
  147. package/bin/runners/lib/validate-patch.js +156 -156
  148. package/bin/runners/lib/verdict-engine.js +628 -628
  149. package/bin/runners/reality/engine.js +917 -917
  150. package/bin/runners/reality/flows.js +122 -122
  151. package/bin/runners/reality/report.js +378 -378
  152. package/bin/runners/reality/session.js +193 -193
  153. package/bin/runners/runAgent.d.ts +5 -0
  154. package/bin/runners/runApprove.js +1200 -0
  155. package/bin/runners/runAuth.js +324 -95
  156. package/bin/runners/runCheckpoint.js +39 -21
  157. package/bin/runners/runClassify.js +859 -0
  158. package/bin/runners/runContext.js +136 -24
  159. package/bin/runners/runDoctor.js +108 -68
  160. package/bin/runners/runFirewall.d.ts +5 -0
  161. package/bin/runners/runFirewallHook.d.ts +5 -0
  162. package/bin/runners/runFix.js +6 -5
  163. package/bin/runners/runGuard.js +262 -168
  164. package/bin/runners/runInit.js +3 -2
  165. package/bin/runners/runMcp.js +130 -52
  166. package/bin/runners/runPolish.js +43 -20
  167. package/bin/runners/runProve.js +1 -2
  168. package/bin/runners/runReport.js +3 -2
  169. package/bin/runners/runScan.js +145 -44
  170. package/bin/runners/runShip.js +3 -4
  171. package/bin/runners/runTruth.d.ts +5 -0
  172. package/bin/runners/runValidate.js +19 -2
  173. package/bin/runners/runWatch.js +104 -53
  174. package/bin/vibecheck.js +106 -19
  175. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  176. package/mcp-server/agent-firewall-interceptor.js +367 -31
  177. package/mcp-server/authority-tools.js +569 -0
  178. package/mcp-server/conductor/conflict-resolver.js +588 -0
  179. package/mcp-server/conductor/execution-planner.js +544 -0
  180. package/mcp-server/conductor/index.js +377 -0
  181. package/mcp-server/conductor/lock-manager.js +615 -0
  182. package/mcp-server/conductor/request-queue.js +550 -0
  183. package/mcp-server/conductor/session-manager.js +500 -0
  184. package/mcp-server/conductor/tools.js +510 -0
  185. package/mcp-server/index.js +1199 -208
  186. package/mcp-server/lib/api-client.cjs +305 -0
  187. package/mcp-server/lib/logger.cjs +30 -0
  188. package/mcp-server/logger.js +173 -0
  189. package/mcp-server/package.json +2 -2
  190. package/mcp-server/premium-tools.js +2 -2
  191. package/mcp-server/tier-auth.js +351 -136
  192. package/mcp-server/tools/index.js +72 -72
  193. package/mcp-server/truth-firewall-tools.js +145 -15
  194. package/mcp-server/vibecheck-tools.js +2 -2
  195. package/package.json +2 -3
  196. package/mcp-server/index.old.js +0 -4137
  197. package/mcp-server/package-lock.json +0 -165
@@ -1,990 +1,990 @@
1
- /**
2
- * Client Call Extractor v2
3
- *
4
- * Extracts client-side API calls: fetch, axios, tRPC, GraphQL, Server Actions, SDK calls.
5
- * Links them to the Reality Proof Graph for dead UI detection.
6
- *
7
- * Output: ClientCall records with evidence, confidence, and canonical paths.
8
- */
9
-
10
- "use strict";
11
-
12
- const fs = require("fs");
13
- const path = require("path");
14
- const fg = require("fast-glob");
15
- const crypto = require("crypto");
16
- const parser = require("@babel/parser");
17
- const traverse = require("@babel/traverse").default;
18
- const t = require("@babel/types");
19
-
20
- // =============================================================================
21
- // TYPES AND CONSTANTS
22
- // =============================================================================
23
-
24
- const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
25
-
26
- const CALL_KINDS = {
27
- HTTP: "http",
28
- TRPC: "trpc",
29
- GRAPHQL: "graphql",
30
- SERVER_ACTION: "server_action",
31
- SDK: "sdk",
32
- };
33
-
34
- const RUNTIMES = {
35
- CLIENT: "client",
36
- SERVER: "server",
37
- SHARED: "shared",
38
- UNKNOWN: "unknown",
39
- };
40
-
41
- // =============================================================================
42
- // MAIN EXTRACTION
43
- // =============================================================================
44
-
45
- /**
46
- * Extract all client calls from a project
47
- */
48
- function extractClientCalls(projectRoot, options = {}) {
49
- const { include = [], exclude = [] } = options;
50
-
51
- // Find source files
52
- const defaultInclude = ["**/*.{ts,tsx,js,jsx,mts,cts}"];
53
- const defaultExclude = [
54
- "**/node_modules/**",
55
- "**/.next/**",
56
- "**/dist/**",
57
- "**/build/**",
58
- "**/coverage/**",
59
- "**/*.min.js",
60
- "**/*.d.ts",
61
- ];
62
-
63
- const patterns = include.length > 0 ? include : defaultInclude;
64
- const ignorePatterns = [...defaultExclude, ...exclude];
65
-
66
- const files = fg.sync(patterns, {
67
- cwd: projectRoot,
68
- absolute: true,
69
- ignore: ignorePatterns,
70
- onlyFiles: true,
71
- });
72
-
73
- const calls = [];
74
- const wrappers = [];
75
- const uiBindings = [];
76
- const errors = [];
77
-
78
- // Pass 1: Extract wrappers (axios instances, custom request helpers)
79
- for (const file of files) {
80
- try {
81
- const content = fs.readFileSync(file, "utf8");
82
- const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
83
- const fileWrappers = extractWrappers(content, relPath, projectRoot);
84
- wrappers.push(...fileWrappers);
85
- } catch (err) {
86
- errors.push({ file, phase: "wrappers", error: err.message });
87
- }
88
- }
89
-
90
- // Build wrapper lookup
91
- const wrapperMap = new Map();
92
- for (const w of wrappers) {
93
- wrapperMap.set(w.name, w);
94
- }
95
-
96
- // Pass 2: Extract calls
97
- for (const file of files) {
98
- try {
99
- const content = fs.readFileSync(file, "utf8");
100
- const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
101
- const runtime = inferRuntime(content, relPath);
102
-
103
- const fileCalls = extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot);
104
- calls.push(...fileCalls);
105
-
106
- const bindings = extractUIBindings(content, relPath, fileCalls);
107
- uiBindings.push(...bindings);
108
- } catch (err) {
109
- errors.push({ file, phase: "calls", error: err.message });
110
- }
111
- }
112
-
113
- return {
114
- calls,
115
- wrappers,
116
- uiBindings,
117
- errors,
118
- stats: {
119
- filesScanned: files.length,
120
- callsFound: calls.length,
121
- wrappersFound: wrappers.length,
122
- bindingsFound: uiBindings.length,
123
- },
124
- };
125
- }
126
-
127
- /**
128
- * Extract calls from a single file
129
- */
130
- function extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot) {
131
- const calls = [];
132
-
133
- let ast;
134
- try {
135
- ast = parser.parse(content, {
136
- sourceType: "module",
137
- plugins: ["typescript", "jsx", "decorators-legacy"],
138
- errorRecovery: true,
139
- });
140
- } catch {
141
- return calls;
142
- }
143
-
144
- traverse(ast, {
145
- CallExpression(path) {
146
- // fetch() calls
147
- const fetchCall = extractFetchCall(path, content, relPath, runtime);
148
- if (fetchCall) {
149
- calls.push(fetchCall);
150
- return;
151
- }
152
-
153
- // axios calls
154
- const axiosCall = extractAxiosCall(path, content, relPath, runtime, wrapperMap);
155
- if (axiosCall) {
156
- calls.push(axiosCall);
157
- return;
158
- }
159
-
160
- // tRPC calls
161
- const trpcCall = extractTRPCCall(path, content, relPath, runtime);
162
- if (trpcCall) {
163
- calls.push(trpcCall);
164
- return;
165
- }
166
-
167
- // GraphQL calls
168
- const gqlCall = extractGraphQLCall(path, content, relPath, runtime);
169
- if (gqlCall) {
170
- calls.push(gqlCall);
171
- return;
172
- }
173
-
174
- // Wrapper calls
175
- const wrapperCall = extractWrapperCall(path, content, relPath, runtime, wrapperMap);
176
- if (wrapperCall) {
177
- calls.push(wrapperCall);
178
- return;
179
- }
180
- },
181
-
182
- // Server Actions (form action={...})
183
- JSXAttribute(path) {
184
- if (path.node.name.name === "action" && t.isJSXExpressionContainer(path.node.value)) {
185
- const actionCall = extractServerAction(path, content, relPath);
186
- if (actionCall) {
187
- calls.push(actionCall);
188
- }
189
- }
190
- },
191
- });
192
-
193
- return calls;
194
- }
195
-
196
- // =============================================================================
197
- // FETCH EXTRACTION
198
- // =============================================================================
199
-
200
- /**
201
- * Extract fetch() call
202
- */
203
- function extractFetchCall(path, content, relPath, runtime) {
204
- const { node } = path;
205
-
206
- // Check if callee is 'fetch'
207
- if (!t.isIdentifier(node.callee, { name: "fetch" })) {
208
- // Also check for fetch(new Request(...))
209
- if (t.isNewExpression(node.callee) && t.isIdentifier(node.callee.callee, { name: "Request" })) {
210
- return extractRequestCall(path, content, relPath, runtime);
211
- }
212
- return null;
213
- }
214
-
215
- const urlArg = node.arguments[0];
216
- const initArg = node.arguments[1];
217
-
218
- if (!urlArg) return null;
219
-
220
- // Extract URL
221
- const urlInfo = extractUrlFromNode(urlArg, content);
222
- if (!urlInfo.value) return null;
223
-
224
- // Extract method
225
- const method = extractMethodFromInit(initArg) || "GET";
226
-
227
- // Extract other info
228
- const expectsJson = extractExpectsJson(initArg, path);
229
- const authHint = extractAuthHint(initArg);
230
-
231
- const lineNum = node.loc?.start?.line || 1;
232
-
233
- return createClientCall({
234
- kind: CALL_KINDS.HTTP,
235
- runtime,
236
- method,
237
- urlTemplate: urlInfo.value,
238
- canonicalPath: canonicalizeUrl(urlInfo.value),
239
- expectsJson,
240
- authHint,
241
- confidence: urlInfo.confidence,
242
- file: relPath,
243
- lines: `${lineNum}-${lineNum + 5}`,
244
- snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
245
- reason: `fetch('${urlInfo.value.slice(0, 50)}') call`,
246
- });
247
- }
248
-
249
- /**
250
- * Extract new Request() call inside fetch
251
- */
252
- function extractRequestCall(path, content, relPath, runtime) {
253
- const { node } = path;
254
- const requestNode = node.callee;
255
-
256
- if (!t.isNewExpression(requestNode)) return null;
257
-
258
- const urlArg = requestNode.arguments[0];
259
- const initArg = requestNode.arguments[1];
260
-
261
- if (!urlArg) return null;
262
-
263
- const urlInfo = extractUrlFromNode(urlArg, content);
264
- if (!urlInfo.value) return null;
265
-
266
- const method = extractMethodFromInit(initArg) || "GET";
267
- const lineNum = node.loc?.start?.line || 1;
268
-
269
- return createClientCall({
270
- kind: CALL_KINDS.HTTP,
271
- runtime,
272
- method,
273
- urlTemplate: urlInfo.value,
274
- canonicalPath: canonicalizeUrl(urlInfo.value),
275
- expectsJson: false,
276
- authHint: "unknown",
277
- confidence: urlInfo.confidence,
278
- file: relPath,
279
- lines: `${lineNum}-${lineNum + 3}`,
280
- snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
281
- reason: `fetch(new Request('${urlInfo.value.slice(0, 50)}')) call`,
282
- });
283
- }
284
-
285
- // =============================================================================
286
- // AXIOS EXTRACTION
287
- // =============================================================================
288
-
289
- /**
290
- * Extract axios call
291
- */
292
- function extractAxiosCall(path, content, relPath, runtime, wrapperMap) {
293
- const { node } = path;
294
-
295
- // Pattern A: axios.get/post/etc(url)
296
- if (t.isMemberExpression(node.callee)) {
297
- const obj = node.callee.object;
298
- const prop = node.callee.property;
299
-
300
- if (t.isIdentifier(obj, { name: "axios" }) && t.isIdentifier(prop)) {
301
- const methodName = prop.name.toUpperCase();
302
- if (HTTP_METHODS.includes(methodName) || methodName === "REQUEST") {
303
- const urlArg = node.arguments[0];
304
- if (!urlArg) return null;
305
-
306
- const urlInfo = extractUrlFromNode(urlArg, content);
307
- if (!urlInfo.value) return null;
308
-
309
- const lineNum = node.loc?.start?.line || 1;
310
-
311
- return createClientCall({
312
- kind: CALL_KINDS.HTTP,
313
- runtime,
314
- method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
315
- urlTemplate: urlInfo.value,
316
- canonicalPath: canonicalizeUrl(urlInfo.value),
317
- expectsJson: true,
318
- authHint: "unknown",
319
- confidence: urlInfo.confidence,
320
- file: relPath,
321
- lines: `${lineNum}-${lineNum + 3}`,
322
- snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
323
- reason: `axios.${prop.name}('${urlInfo.value.slice(0, 50)}') call`,
324
- });
325
- }
326
- }
327
- }
328
-
329
- // Pattern B: axios({ method, url })
330
- if (t.isIdentifier(node.callee, { name: "axios" })) {
331
- const configArg = node.arguments[0];
332
- if (t.isObjectExpression(configArg)) {
333
- const config = extractObjectLiteralProps(configArg);
334
- if (config.url) {
335
- const lineNum = node.loc?.start?.line || 1;
336
-
337
- return createClientCall({
338
- kind: CALL_KINDS.HTTP,
339
- runtime,
340
- method: (config.method || "GET").toUpperCase(),
341
- urlTemplate: config.url,
342
- canonicalPath: canonicalizeUrl(combineBaseUrl(config.baseURL, config.url)),
343
- expectsJson: true,
344
- authHint: "unknown",
345
- confidence: "medium",
346
- file: relPath,
347
- lines: `${lineNum}-${lineNum + 5}`,
348
- snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
349
- reason: `axios({ url: '${config.url.slice(0, 50)}' }) call`,
350
- });
351
- }
352
- }
353
- }
354
-
355
- return null;
356
- }
357
-
358
- // =============================================================================
359
- // TRPC EXTRACTION
360
- // =============================================================================
361
-
362
- /**
363
- * Extract tRPC call
364
- */
365
- function extractTRPCCall(path, content, relPath, runtime) {
366
- const { node } = path;
367
-
368
- // Pattern: trpc.user.get.useQuery() or trpc.billing.portal.useMutation()
369
- if (t.isMemberExpression(node.callee)) {
370
- const calleeCode = extractMemberChain(node.callee);
371
-
372
- // Check for tRPC patterns
373
- const trpcMatch = calleeCode.match(/^(?:trpc|api)\.(.+?)\.(useQuery|useMutation|query|mutate)$/);
374
- if (trpcMatch) {
375
- const procedure = trpcMatch[1];
376
- const operationType = trpcMatch[2].includes("Mutation") || trpcMatch[2] === "mutate" ? "mutation" : "query";
377
- const lineNum = node.loc?.start?.line || 1;
378
-
379
- return createClientCall({
380
- kind: CALL_KINDS.TRPC,
381
- runtime,
382
- method: "POST",
383
- urlTemplate: `/api/trpc/${procedure}`,
384
- canonicalPath: `/api/trpc/${procedure}`,
385
- expectsJson: true,
386
- authHint: "unknown",
387
- confidence: "medium",
388
- file: relPath,
389
- lines: `${lineNum}-${lineNum + 3}`,
390
- snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
391
- reason: `tRPC ${operationType}: ${procedure}`,
392
- meta: {
393
- procedure,
394
- operationType,
395
- },
396
- });
397
- }
398
- }
399
-
400
- return null;
401
- }
402
-
403
- // =============================================================================
404
- // GRAPHQL EXTRACTION
405
- // =============================================================================
406
-
407
- /**
408
- * Extract GraphQL call
409
- */
410
- function extractGraphQLCall(path, content, relPath, runtime) {
411
- const { node } = path;
412
-
413
- // Pattern: useQuery(gql`...`) or useMutation(gql`...`)
414
- if (t.isIdentifier(node.callee)) {
415
- const calleeName = node.callee.name;
416
-
417
- if (calleeName === "useQuery" || calleeName === "useMutation" || calleeName === "request") {
418
- const queryArg = node.arguments[0];
419
- let operationName = null;
420
-
421
- // Try to extract operation name from gql template
422
- if (t.isTaggedTemplateExpression(queryArg)) {
423
- const template = queryArg.quasi.quasis.map(q => q.value.raw).join("");
424
- const opMatch = template.match(/(?:query|mutation|subscription)\s+(\w+)/);
425
- if (opMatch) {
426
- operationName = opMatch[1];
427
- }
428
- }
429
-
430
- const lineNum = node.loc?.start?.line || 1;
431
-
432
- return createClientCall({
433
- kind: CALL_KINDS.GRAPHQL,
434
- runtime,
435
- method: "POST",
436
- urlTemplate: "/graphql",
437
- canonicalPath: "/graphql",
438
- expectsJson: true,
439
- authHint: "unknown",
440
- confidence: operationName ? "medium" : "low",
441
- file: relPath,
442
- lines: `${lineNum}-${lineNum + 5}`,
443
- snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
444
- reason: `GraphQL ${calleeName}${operationName ? `: ${operationName}` : ""}`,
445
- meta: {
446
- operationName,
447
- operationType: calleeName.includes("Mutation") ? "mutation" : "query",
448
- },
449
- });
450
- }
451
- }
452
-
453
- return null;
454
- }
455
-
456
- // =============================================================================
457
- // SERVER ACTIONS EXTRACTION
458
- // =============================================================================
459
-
460
- /**
461
- * Extract Next.js Server Action
462
- */
463
- function extractServerAction(path, content, relPath) {
464
- const jsxAttr = path.node;
465
- const container = jsxAttr.value;
466
-
467
- if (!t.isJSXExpressionContainer(container)) return null;
468
-
469
- const expr = container.expression;
470
- let actionName = null;
471
-
472
- if (t.isIdentifier(expr)) {
473
- actionName = expr.name;
474
- } else if (t.isMemberExpression(expr)) {
475
- actionName = extractMemberChain(expr);
476
- }
477
-
478
- if (!actionName) return null;
479
-
480
- const lineNum = jsxAttr.loc?.start?.line || 1;
481
-
482
- return createClientCall({
483
- kind: CALL_KINDS.SERVER_ACTION,
484
- runtime: RUNTIMES.SERVER,
485
- method: "POST",
486
- urlTemplate: `action://${relPath}#${actionName}`,
487
- canonicalPath: `action://${relPath}#${actionName}`,
488
- expectsJson: false,
489
- authHint: "unknown",
490
- confidence: "high",
491
- file: relPath,
492
- lines: `${lineNum}-${lineNum + 1}`,
493
- snippet: `action={${actionName}}`,
494
- reason: `Server Action: ${actionName}`,
495
- meta: {
496
- actionExport: actionName,
497
- module: relPath,
498
- },
499
- });
500
- }
501
-
502
- // =============================================================================
503
- // WRAPPER EXTRACTION
504
- // =============================================================================
505
-
506
- /**
507
- * Extract wrapper definitions (axios instances, custom fetch wrappers)
508
- */
509
- function extractWrappers(content, relPath, projectRoot) {
510
- const wrappers = [];
511
-
512
- let ast;
513
- try {
514
- ast = parser.parse(content, {
515
- sourceType: "module",
516
- plugins: ["typescript", "jsx", "decorators-legacy"],
517
- errorRecovery: true,
518
- });
519
- } catch {
520
- return wrappers;
521
- }
522
-
523
- traverse(ast, {
524
- // axios.create()
525
- CallExpression(path) {
526
- const { node } = path;
527
-
528
- if (
529
- t.isMemberExpression(node.callee) &&
530
- t.isIdentifier(node.callee.object, { name: "axios" }) &&
531
- t.isIdentifier(node.callee.property, { name: "create" })
532
- ) {
533
- const configArg = node.arguments[0];
534
- const config = t.isObjectExpression(configArg) ? extractObjectLiteralProps(configArg) : {};
535
-
536
- // Find variable name
537
- let name = "axiosInstance";
538
- if (t.isVariableDeclarator(path.parent)) {
539
- if (t.isIdentifier(path.parent.id)) {
540
- name = path.parent.id.name;
541
- }
542
- }
543
-
544
- wrappers.push({
545
- id: `W_AXIOS_${hashShort(relPath + name)}`,
546
- name,
547
- kind: "axios",
548
- baseURL: config.baseURL || "",
549
- defaultHeaders: config.headers ? Object.keys(config.headers) : [],
550
- file: relPath,
551
- lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
552
- });
553
- }
554
- },
555
-
556
- // Custom fetch wrappers: export const api = { get: ..., post: ... }
557
- VariableDeclarator(path) {
558
- const { node } = path;
559
-
560
- if (t.isIdentifier(node.id) && t.isObjectExpression(node.init)) {
561
- const props = node.init.properties;
562
- const methodProps = props.filter(
563
- p => t.isIdentifier(p.key) && HTTP_METHODS.map(m => m.toLowerCase()).includes(p.key.name)
564
- );
565
-
566
- if (methodProps.length >= 2) {
567
- // Looks like an API client object
568
- wrappers.push({
569
- id: `W_CUSTOM_${hashShort(relPath + node.id.name)}`,
570
- name: node.id.name,
571
- kind: "custom",
572
- baseURL: "",
573
- methods: methodProps.map(p => p.key.name),
574
- file: relPath,
575
- lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
576
- });
577
- }
578
- }
579
- },
580
- });
581
-
582
- return wrappers;
583
- }
584
-
585
- /**
586
- * Extract wrapper call (api.get(), apiClient.post(), etc.)
587
- */
588
- function extractWrapperCall(path, content, relPath, runtime, wrapperMap) {
589
- const { node } = path;
590
-
591
- if (!t.isMemberExpression(node.callee)) return null;
592
-
593
- const obj = node.callee.object;
594
- const prop = node.callee.property;
595
-
596
- if (!t.isIdentifier(obj) || !t.isIdentifier(prop)) return null;
597
-
598
- const wrapper = wrapperMap.get(obj.name);
599
- if (!wrapper) return null;
600
-
601
- const methodName = prop.name.toUpperCase();
602
- if (!HTTP_METHODS.includes(methodName) && methodName !== "REQUEST") return null;
603
-
604
- const urlArg = node.arguments[0];
605
- if (!urlArg) return null;
606
-
607
- const urlInfo = extractUrlFromNode(urlArg, content);
608
- if (!urlInfo.value) return null;
609
-
610
- // Combine with wrapper baseURL
611
- const fullUrl = combineBaseUrl(wrapper.baseURL, urlInfo.value);
612
- const lineNum = node.loc?.start?.line || 1;
613
-
614
- return createClientCall({
615
- kind: CALL_KINDS.HTTP,
616
- runtime,
617
- method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
618
- urlTemplate: urlInfo.value,
619
- canonicalPath: canonicalizeUrl(fullUrl),
620
- expectsJson: true,
621
- authHint: "unknown",
622
- confidence: urlInfo.confidence,
623
- file: relPath,
624
- lines: `${lineNum}-${lineNum + 3}`,
625
- snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
626
- reason: `${obj.name}.${prop.name}('${urlInfo.value.slice(0, 50)}') via wrapper`,
627
- meta: {
628
- wrapperId: wrapper.id,
629
- },
630
- });
631
- }
632
-
633
- // =============================================================================
634
- // UI BINDING EXTRACTION
635
- // =============================================================================
636
-
637
- /**
638
- * Extract UI bindings (onClick, onSubmit that call API)
639
- */
640
- function extractUIBindings(content, relPath, calls) {
641
- const bindings = [];
642
- const callIds = new Set(calls.map(c => c.id));
643
-
644
- let ast;
645
- try {
646
- ast = parser.parse(content, {
647
- sourceType: "module",
648
- plugins: ["typescript", "jsx", "decorators-legacy"],
649
- errorRecovery: true,
650
- });
651
- } catch {
652
- return bindings;
653
- }
654
-
655
- traverse(ast, {
656
- JSXAttribute(path) {
657
- const { node } = path;
658
- const attrName = node.name?.name;
659
-
660
- if (!["onClick", "onSubmit", "onBlur", "onChange", "action"].includes(attrName)) return;
661
-
662
- if (!t.isJSXExpressionContainer(node.value)) return;
663
-
664
- const expr = node.value.expression;
665
- const lineNum = node.loc?.start?.line || 1;
666
-
667
- // Find associated calls in this handler
668
- const handlerCalls = calls.filter(c => {
669
- const callLine = parseInt(c.evidence?.[0]?.lines?.split("-")[0] || "0");
670
- return Math.abs(callLine - lineNum) < 20; // Within 20 lines
671
- });
672
-
673
- if (handlerCalls.length > 0) {
674
- // Try to find label hint
675
- const labelHint = findLabelHint(path);
676
-
677
- bindings.push({
678
- bindingId: `UIB_${hashShort(relPath + lineNum)}`,
679
- file: relPath,
680
- lines: `${lineNum}-${lineNum + 5}`,
681
- event: attrName,
682
- labelHint,
683
- calls: handlerCalls.map(c => c.id),
684
- });
685
- }
686
- },
687
- });
688
-
689
- return bindings;
690
- }
691
-
692
- /**
693
- * Find label hint from JSX context
694
- */
695
- function findLabelHint(path) {
696
- // Look for button text, aria-label, etc.
697
- const parent = path.parentPath;
698
- if (!parent) return null;
699
-
700
- // Check for aria-label
701
- if (t.isJSXOpeningElement(parent.node)) {
702
- const ariaLabel = parent.node.attributes.find(
703
- a => t.isJSXAttribute(a) && a.name?.name === "aria-label"
704
- );
705
- if (ariaLabel && t.isStringLiteral(ariaLabel.value)) {
706
- return ariaLabel.value.value;
707
- }
708
- }
709
-
710
- // Check for children text
711
- const jsxElement = parent.parentPath;
712
- if (t.isJSXElement(jsxElement?.node)) {
713
- const textChild = jsxElement.node.children.find(
714
- c => t.isJSXText(c) && c.value.trim()
715
- );
716
- if (textChild) {
717
- return textChild.value.trim();
718
- }
719
- }
720
-
721
- return null;
722
- }
723
-
724
- // =============================================================================
725
- // HELPERS
726
- // =============================================================================
727
-
728
- /**
729
- * Extract URL value and confidence from AST node
730
- */
731
- function extractUrlFromNode(node, content) {
732
- // String literal: HIGH confidence
733
- if (t.isStringLiteral(node)) {
734
- return { value: node.value, confidence: "high" };
735
- }
736
-
737
- // Template literal
738
- if (t.isTemplateLiteral(node)) {
739
- if (node.expressions.length === 0) {
740
- // No expressions: HIGH
741
- return { value: node.quasis[0].value.raw, confidence: "high" };
742
- }
743
- // Has expressions: MEDIUM
744
- const template = node.quasis.map((q, i) => {
745
- const expr = node.expressions[i];
746
- return q.value.raw + (expr ? `{${i}}` : "");
747
- }).join("");
748
- return { value: template, confidence: "medium" };
749
- }
750
-
751
- // Binary expression (concatenation)
752
- if (t.isBinaryExpression(node, { operator: "+" })) {
753
- const left = extractUrlFromNode(node.left, content);
754
- const right = extractUrlFromNode(node.right, content);
755
- if (left.value && right.value) {
756
- return {
757
- value: left.value + right.value,
758
- confidence: "medium",
759
- };
760
- }
761
- }
762
-
763
- // Variable: LOW confidence
764
- if (t.isIdentifier(node)) {
765
- return { value: `\${${node.name}}`, confidence: "low" };
766
- }
767
-
768
- return { value: null, confidence: "low" };
769
- }
770
-
771
- /**
772
- * Extract method from fetch init argument
773
- */
774
- function extractMethodFromInit(initNode) {
775
- if (!initNode || !t.isObjectExpression(initNode)) return null;
776
-
777
- const methodProp = initNode.properties.find(
778
- p => t.isObjectProperty(p) && t.isIdentifier(p.key, { name: "method" })
779
- );
780
-
781
- if (methodProp && t.isStringLiteral(methodProp.value)) {
782
- return methodProp.value.value.toUpperCase();
783
- }
784
-
785
- return null;
786
- }
787
-
788
- /**
789
- * Extract expectsJson from init
790
- */
791
- function extractExpectsJson(initNode, path) {
792
- if (!initNode) return false;
793
-
794
- if (t.isObjectExpression(initNode)) {
795
- const props = extractObjectLiteralProps(initNode);
796
- if (props.headers?.["content-type"]?.includes("application/json")) return true;
797
- if (props.body && typeof props.body === "string" && props.body.includes("JSON.stringify")) return true;
798
- }
799
-
800
- // Check if .json() is called on response
801
- // This would require more complex flow analysis
802
- return false;
803
- }
804
-
805
- /**
806
- * Extract auth hint from init
807
- */
808
- function extractAuthHint(initNode) {
809
- if (!initNode || !t.isObjectExpression(initNode)) return "unknown";
810
-
811
- const props = extractObjectLiteralProps(initNode);
812
-
813
- if (props.credentials === "include") return "cookie";
814
- if (props.headers?.authorization?.toLowerCase().startsWith("bearer")) return "bearer";
815
- if (props.headers?.Authorization?.toLowerCase().startsWith("bearer")) return "bearer";
816
-
817
- return "unknown";
818
- }
819
-
820
- /**
821
- * Extract object literal properties as plain object
822
- */
823
- function extractObjectLiteralProps(node) {
824
- const props = {};
825
-
826
- for (const prop of node.properties) {
827
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
828
- const key = prop.key.name;
829
-
830
- if (t.isStringLiteral(prop.value)) {
831
- props[key] = prop.value.value;
832
- } else if (t.isObjectExpression(prop.value)) {
833
- props[key] = extractObjectLiteralProps(prop.value);
834
- } else if (t.isIdentifier(prop.value)) {
835
- props[key] = `$${prop.value.name}`;
836
- }
837
- }
838
- }
839
-
840
- return props;
841
- }
842
-
843
- /**
844
- * Extract member expression chain as string
845
- */
846
- function extractMemberChain(node) {
847
- const parts = [];
848
-
849
- let current = node;
850
- while (t.isMemberExpression(current)) {
851
- if (t.isIdentifier(current.property)) {
852
- parts.unshift(current.property.name);
853
- }
854
- current = current.object;
855
- }
856
-
857
- if (t.isIdentifier(current)) {
858
- parts.unshift(current.name);
859
- }
860
-
861
- return parts.join(".");
862
- }
863
-
864
- /**
865
- * Infer runtime from file content
866
- */
867
- function inferRuntime(content, relPath) {
868
- if (content.includes('"use client"') || content.includes("'use client'")) {
869
- return RUNTIMES.CLIENT;
870
- }
871
- if (content.includes('"use server"') || content.includes("'use server'")) {
872
- return RUNTIMES.SERVER;
873
- }
874
- if (relPath.includes("/api/") || relPath.includes("server")) {
875
- return RUNTIMES.SERVER;
876
- }
877
- if (relPath.includes("/lib/") || relPath.includes("/utils/")) {
878
- return RUNTIMES.SHARED;
879
- }
880
- return RUNTIMES.UNKNOWN;
881
- }
882
-
883
- /**
884
- * Canonicalize URL path
885
- */
886
- function canonicalizeUrl(url) {
887
- if (!url) return null;
888
-
889
- let canonical = url;
890
-
891
- // Strip protocol and host
892
- canonical = canonical.replace(/^https?:\/\/[^/]+/, "");
893
-
894
- // Strip query string
895
- canonical = canonical.split("?")[0];
896
-
897
- // Strip hash
898
- canonical = canonical.split("#")[0];
899
-
900
- // Replace template expressions with params
901
- canonical = canonical.replace(/\$\{[^}]+\}/g, "{param}");
902
- canonical = canonical.replace(/\{[0-9]+\}/g, "{param}");
903
-
904
- // Normalize slashes
905
- canonical = canonical.replace(/\/+/g, "/");
906
-
907
- // Ensure leading slash
908
- if (!canonical.startsWith("/") && !canonical.startsWith("action://")) {
909
- canonical = "/" + canonical;
910
- }
911
-
912
- // Remove trailing slash (except root)
913
- if (canonical.length > 1) {
914
- canonical = canonical.replace(/\/$/, "");
915
- }
916
-
917
- return canonical;
918
- }
919
-
920
- /**
921
- * Combine base URL with path
922
- */
923
- function combineBaseUrl(base, path) {
924
- if (!base) return path;
925
- if (!path) return base;
926
-
927
- const baseTrimmed = base.replace(/\/$/, "");
928
- const pathTrimmed = path.startsWith("/") ? path : "/" + path;
929
-
930
- return baseTrimmed + pathTrimmed;
931
- }
932
-
933
- /**
934
- * Create a ClientCall record
935
- */
936
- function createClientCall(opts) {
937
- const id = `C_CALL_${hashShort(opts.file + opts.urlTemplate + opts.method)}`;
938
-
939
- return {
940
- id,
941
- kind: opts.kind,
942
- runtime: opts.runtime,
943
- method: opts.method,
944
- urlTemplate: opts.urlTemplate,
945
- canonicalPath: opts.canonicalPath,
946
- expectsJson: opts.expectsJson,
947
- authHint: opts.authHint,
948
- confidence: opts.confidence,
949
- evidence: [{
950
- id: `E_${hashShort(opts.file + opts.lines)}`,
951
- kind: "file",
952
- file: opts.file,
953
- lines: opts.lines,
954
- snippetHash: hashSnippet(opts.snippet || ""),
955
- reason: opts.reason,
956
- }],
957
- meta: opts.meta || {},
958
- };
959
- }
960
-
961
- function hashShort(text) {
962
- return crypto.createHash("sha256").update(text).digest("hex").slice(0, 12).toUpperCase();
963
- }
964
-
965
- function hashSnippet(text) {
966
- return `sha256:${crypto.createHash("sha256").update(text).digest("hex")}`;
967
- }
968
-
969
- // =============================================================================
970
- // EXPORTS
971
- // =============================================================================
972
-
973
- module.exports = {
974
- extractClientCalls,
975
- extractCallsFromFile,
976
- extractFetchCall,
977
- extractAxiosCall,
978
- extractTRPCCall,
979
- extractGraphQLCall,
980
- extractServerAction,
981
- extractWrappers,
982
- extractWrapperCall,
983
- extractUIBindings,
984
- canonicalizeUrl,
985
- combineBaseUrl,
986
- inferRuntime,
987
- CALL_KINDS,
988
- RUNTIMES,
989
- HTTP_METHODS,
990
- };
1
+ /**
2
+ * Client Call Extractor v2
3
+ *
4
+ * Extracts client-side API calls: fetch, axios, tRPC, GraphQL, Server Actions, SDK calls.
5
+ * Links them to the Reality Proof Graph for dead UI detection.
6
+ *
7
+ * Output: ClientCall records with evidence, confidence, and canonical paths.
8
+ */
9
+
10
+ "use strict";
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const fg = require("fast-glob");
15
+ const crypto = require("crypto");
16
+ const parser = require("@babel/parser");
17
+ const traverse = require("@babel/traverse").default;
18
+ const t = require("@babel/types");
19
+
20
+ // =============================================================================
21
+ // TYPES AND CONSTANTS
22
+ // =============================================================================
23
+
24
+ const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
25
+
26
+ const CALL_KINDS = {
27
+ HTTP: "http",
28
+ TRPC: "trpc",
29
+ GRAPHQL: "graphql",
30
+ SERVER_ACTION: "server_action",
31
+ SDK: "sdk",
32
+ };
33
+
34
+ const RUNTIMES = {
35
+ CLIENT: "client",
36
+ SERVER: "server",
37
+ SHARED: "shared",
38
+ UNKNOWN: "unknown",
39
+ };
40
+
41
+ // =============================================================================
42
+ // MAIN EXTRACTION
43
+ // =============================================================================
44
+
45
+ /**
46
+ * Extract all client calls from a project
47
+ */
48
+ function extractClientCalls(projectRoot, options = {}) {
49
+ const { include = [], exclude = [] } = options;
50
+
51
+ // Find source files
52
+ const defaultInclude = ["**/*.{ts,tsx,js,jsx,mts,cts}"];
53
+ const defaultExclude = [
54
+ "**/node_modules/**",
55
+ "**/.next/**",
56
+ "**/dist/**",
57
+ "**/build/**",
58
+ "**/coverage/**",
59
+ "**/*.min.js",
60
+ "**/*.d.ts",
61
+ ];
62
+
63
+ const patterns = include.length > 0 ? include : defaultInclude;
64
+ const ignorePatterns = [...defaultExclude, ...exclude];
65
+
66
+ const files = fg.sync(patterns, {
67
+ cwd: projectRoot,
68
+ absolute: true,
69
+ ignore: ignorePatterns,
70
+ onlyFiles: true,
71
+ });
72
+
73
+ const calls = [];
74
+ const wrappers = [];
75
+ const uiBindings = [];
76
+ const errors = [];
77
+
78
+ // Pass 1: Extract wrappers (axios instances, custom request helpers)
79
+ for (const file of files) {
80
+ try {
81
+ const content = fs.readFileSync(file, "utf8");
82
+ const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
83
+ const fileWrappers = extractWrappers(content, relPath, projectRoot);
84
+ wrappers.push(...fileWrappers);
85
+ } catch (err) {
86
+ errors.push({ file, phase: "wrappers", error: err.message });
87
+ }
88
+ }
89
+
90
+ // Build wrapper lookup
91
+ const wrapperMap = new Map();
92
+ for (const w of wrappers) {
93
+ wrapperMap.set(w.name, w);
94
+ }
95
+
96
+ // Pass 2: Extract calls
97
+ for (const file of files) {
98
+ try {
99
+ const content = fs.readFileSync(file, "utf8");
100
+ const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
101
+ const runtime = inferRuntime(content, relPath);
102
+
103
+ const fileCalls = extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot);
104
+ calls.push(...fileCalls);
105
+
106
+ const bindings = extractUIBindings(content, relPath, fileCalls);
107
+ uiBindings.push(...bindings);
108
+ } catch (err) {
109
+ errors.push({ file, phase: "calls", error: err.message });
110
+ }
111
+ }
112
+
113
+ return {
114
+ calls,
115
+ wrappers,
116
+ uiBindings,
117
+ errors,
118
+ stats: {
119
+ filesScanned: files.length,
120
+ callsFound: calls.length,
121
+ wrappersFound: wrappers.length,
122
+ bindingsFound: uiBindings.length,
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Extract calls from a single file
129
+ */
130
+ function extractCallsFromFile(content, relPath, runtime, wrapperMap, projectRoot) {
131
+ const calls = [];
132
+
133
+ let ast;
134
+ try {
135
+ ast = parser.parse(content, {
136
+ sourceType: "module",
137
+ plugins: ["typescript", "jsx", "decorators-legacy"],
138
+ errorRecovery: true,
139
+ });
140
+ } catch {
141
+ return calls;
142
+ }
143
+
144
+ traverse(ast, {
145
+ CallExpression(path) {
146
+ // fetch() calls
147
+ const fetchCall = extractFetchCall(path, content, relPath, runtime);
148
+ if (fetchCall) {
149
+ calls.push(fetchCall);
150
+ return;
151
+ }
152
+
153
+ // axios calls
154
+ const axiosCall = extractAxiosCall(path, content, relPath, runtime, wrapperMap);
155
+ if (axiosCall) {
156
+ calls.push(axiosCall);
157
+ return;
158
+ }
159
+
160
+ // tRPC calls
161
+ const trpcCall = extractTRPCCall(path, content, relPath, runtime);
162
+ if (trpcCall) {
163
+ calls.push(trpcCall);
164
+ return;
165
+ }
166
+
167
+ // GraphQL calls
168
+ const gqlCall = extractGraphQLCall(path, content, relPath, runtime);
169
+ if (gqlCall) {
170
+ calls.push(gqlCall);
171
+ return;
172
+ }
173
+
174
+ // Wrapper calls
175
+ const wrapperCall = extractWrapperCall(path, content, relPath, runtime, wrapperMap);
176
+ if (wrapperCall) {
177
+ calls.push(wrapperCall);
178
+ return;
179
+ }
180
+ },
181
+
182
+ // Server Actions (form action={...})
183
+ JSXAttribute(path) {
184
+ if (path.node.name.name === "action" && t.isJSXExpressionContainer(path.node.value)) {
185
+ const actionCall = extractServerAction(path, content, relPath);
186
+ if (actionCall) {
187
+ calls.push(actionCall);
188
+ }
189
+ }
190
+ },
191
+ });
192
+
193
+ return calls;
194
+ }
195
+
196
+ // =============================================================================
197
+ // FETCH EXTRACTION
198
+ // =============================================================================
199
+
200
+ /**
201
+ * Extract fetch() call
202
+ */
203
+ function extractFetchCall(path, content, relPath, runtime) {
204
+ const { node } = path;
205
+
206
+ // Check if callee is 'fetch'
207
+ if (!t.isIdentifier(node.callee, { name: "fetch" })) {
208
+ // Also check for fetch(new Request(...))
209
+ if (t.isNewExpression(node.callee) && t.isIdentifier(node.callee.callee, { name: "Request" })) {
210
+ return extractRequestCall(path, content, relPath, runtime);
211
+ }
212
+ return null;
213
+ }
214
+
215
+ const urlArg = node.arguments[0];
216
+ const initArg = node.arguments[1];
217
+
218
+ if (!urlArg) return null;
219
+
220
+ // Extract URL
221
+ const urlInfo = extractUrlFromNode(urlArg, content);
222
+ if (!urlInfo.value) return null;
223
+
224
+ // Extract method
225
+ const method = extractMethodFromInit(initArg) || "GET";
226
+
227
+ // Extract other info
228
+ const expectsJson = extractExpectsJson(initArg, path);
229
+ const authHint = extractAuthHint(initArg);
230
+
231
+ const lineNum = node.loc?.start?.line || 1;
232
+
233
+ return createClientCall({
234
+ kind: CALL_KINDS.HTTP,
235
+ runtime,
236
+ method,
237
+ urlTemplate: urlInfo.value,
238
+ canonicalPath: canonicalizeUrl(urlInfo.value),
239
+ expectsJson,
240
+ authHint,
241
+ confidence: urlInfo.confidence,
242
+ file: relPath,
243
+ lines: `${lineNum}-${lineNum + 5}`,
244
+ snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
245
+ reason: `fetch('${urlInfo.value.slice(0, 50)}') call`,
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Extract new Request() call inside fetch
251
+ */
252
+ function extractRequestCall(path, content, relPath, runtime) {
253
+ const { node } = path;
254
+ const requestNode = node.callee;
255
+
256
+ if (!t.isNewExpression(requestNode)) return null;
257
+
258
+ const urlArg = requestNode.arguments[0];
259
+ const initArg = requestNode.arguments[1];
260
+
261
+ if (!urlArg) return null;
262
+
263
+ const urlInfo = extractUrlFromNode(urlArg, content);
264
+ if (!urlInfo.value) return null;
265
+
266
+ const method = extractMethodFromInit(initArg) || "GET";
267
+ const lineNum = node.loc?.start?.line || 1;
268
+
269
+ return createClientCall({
270
+ kind: CALL_KINDS.HTTP,
271
+ runtime,
272
+ method,
273
+ urlTemplate: urlInfo.value,
274
+ canonicalPath: canonicalizeUrl(urlInfo.value),
275
+ expectsJson: false,
276
+ authHint: "unknown",
277
+ confidence: urlInfo.confidence,
278
+ file: relPath,
279
+ lines: `${lineNum}-${lineNum + 3}`,
280
+ snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
281
+ reason: `fetch(new Request('${urlInfo.value.slice(0, 50)}')) call`,
282
+ });
283
+ }
284
+
285
+ // =============================================================================
286
+ // AXIOS EXTRACTION
287
+ // =============================================================================
288
+
289
+ /**
290
+ * Extract axios call
291
+ */
292
+ function extractAxiosCall(path, content, relPath, runtime, wrapperMap) {
293
+ const { node } = path;
294
+
295
+ // Pattern A: axios.get/post/etc(url)
296
+ if (t.isMemberExpression(node.callee)) {
297
+ const obj = node.callee.object;
298
+ const prop = node.callee.property;
299
+
300
+ if (t.isIdentifier(obj, { name: "axios" }) && t.isIdentifier(prop)) {
301
+ const methodName = prop.name.toUpperCase();
302
+ if (HTTP_METHODS.includes(methodName) || methodName === "REQUEST") {
303
+ const urlArg = node.arguments[0];
304
+ if (!urlArg) return null;
305
+
306
+ const urlInfo = extractUrlFromNode(urlArg, content);
307
+ if (!urlInfo.value) return null;
308
+
309
+ const lineNum = node.loc?.start?.line || 1;
310
+
311
+ return createClientCall({
312
+ kind: CALL_KINDS.HTTP,
313
+ runtime,
314
+ method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
315
+ urlTemplate: urlInfo.value,
316
+ canonicalPath: canonicalizeUrl(urlInfo.value),
317
+ expectsJson: true,
318
+ authHint: "unknown",
319
+ confidence: urlInfo.confidence,
320
+ file: relPath,
321
+ lines: `${lineNum}-${lineNum + 3}`,
322
+ snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
323
+ reason: `axios.${prop.name}('${urlInfo.value.slice(0, 50)}') call`,
324
+ });
325
+ }
326
+ }
327
+ }
328
+
329
+ // Pattern B: axios({ method, url })
330
+ if (t.isIdentifier(node.callee, { name: "axios" })) {
331
+ const configArg = node.arguments[0];
332
+ if (t.isObjectExpression(configArg)) {
333
+ const config = extractObjectLiteralProps(configArg);
334
+ if (config.url) {
335
+ const lineNum = node.loc?.start?.line || 1;
336
+
337
+ return createClientCall({
338
+ kind: CALL_KINDS.HTTP,
339
+ runtime,
340
+ method: (config.method || "GET").toUpperCase(),
341
+ urlTemplate: config.url,
342
+ canonicalPath: canonicalizeUrl(combineBaseUrl(config.baseURL, config.url)),
343
+ expectsJson: true,
344
+ authHint: "unknown",
345
+ confidence: "medium",
346
+ file: relPath,
347
+ lines: `${lineNum}-${lineNum + 5}`,
348
+ snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
349
+ reason: `axios({ url: '${config.url.slice(0, 50)}' }) call`,
350
+ });
351
+ }
352
+ }
353
+ }
354
+
355
+ return null;
356
+ }
357
+
358
+ // =============================================================================
359
+ // TRPC EXTRACTION
360
+ // =============================================================================
361
+
362
+ /**
363
+ * Extract tRPC call
364
+ */
365
+ function extractTRPCCall(path, content, relPath, runtime) {
366
+ const { node } = path;
367
+
368
+ // Pattern: trpc.user.get.useQuery() or trpc.billing.portal.useMutation()
369
+ if (t.isMemberExpression(node.callee)) {
370
+ const calleeCode = extractMemberChain(node.callee);
371
+
372
+ // Check for tRPC patterns
373
+ const trpcMatch = calleeCode.match(/^(?:trpc|api)\.(.+?)\.(useQuery|useMutation|query|mutate)$/);
374
+ if (trpcMatch) {
375
+ const procedure = trpcMatch[1];
376
+ const operationType = trpcMatch[2].includes("Mutation") || trpcMatch[2] === "mutate" ? "mutation" : "query";
377
+ const lineNum = node.loc?.start?.line || 1;
378
+
379
+ return createClientCall({
380
+ kind: CALL_KINDS.TRPC,
381
+ runtime,
382
+ method: "POST",
383
+ urlTemplate: `/api/trpc/${procedure}`,
384
+ canonicalPath: `/api/trpc/${procedure}`,
385
+ expectsJson: true,
386
+ authHint: "unknown",
387
+ confidence: "medium",
388
+ file: relPath,
389
+ lines: `${lineNum}-${lineNum + 3}`,
390
+ snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
391
+ reason: `tRPC ${operationType}: ${procedure}`,
392
+ meta: {
393
+ procedure,
394
+ operationType,
395
+ },
396
+ });
397
+ }
398
+ }
399
+
400
+ return null;
401
+ }
402
+
403
+ // =============================================================================
404
+ // GRAPHQL EXTRACTION
405
+ // =============================================================================
406
+
407
+ /**
408
+ * Extract GraphQL call
409
+ */
410
+ function extractGraphQLCall(path, content, relPath, runtime) {
411
+ const { node } = path;
412
+
413
+ // Pattern: useQuery(gql`...`) or useMutation(gql`...`)
414
+ if (t.isIdentifier(node.callee)) {
415
+ const calleeName = node.callee.name;
416
+
417
+ if (calleeName === "useQuery" || calleeName === "useMutation" || calleeName === "request") {
418
+ const queryArg = node.arguments[0];
419
+ let operationName = null;
420
+
421
+ // Try to extract operation name from gql template
422
+ if (t.isTaggedTemplateExpression(queryArg)) {
423
+ const template = queryArg.quasi.quasis.map(q => q.value.raw).join("");
424
+ const opMatch = template.match(/(?:query|mutation|subscription)\s+(\w+)/);
425
+ if (opMatch) {
426
+ operationName = opMatch[1];
427
+ }
428
+ }
429
+
430
+ const lineNum = node.loc?.start?.line || 1;
431
+
432
+ return createClientCall({
433
+ kind: CALL_KINDS.GRAPHQL,
434
+ runtime,
435
+ method: "POST",
436
+ urlTemplate: "/graphql",
437
+ canonicalPath: "/graphql",
438
+ expectsJson: true,
439
+ authHint: "unknown",
440
+ confidence: operationName ? "medium" : "low",
441
+ file: relPath,
442
+ lines: `${lineNum}-${lineNum + 5}`,
443
+ snippet: content.substring(node.start, Math.min(node.end, node.start + 200)),
444
+ reason: `GraphQL ${calleeName}${operationName ? `: ${operationName}` : ""}`,
445
+ meta: {
446
+ operationName,
447
+ operationType: calleeName.includes("Mutation") ? "mutation" : "query",
448
+ },
449
+ });
450
+ }
451
+ }
452
+
453
+ return null;
454
+ }
455
+
456
+ // =============================================================================
457
+ // SERVER ACTIONS EXTRACTION
458
+ // =============================================================================
459
+
460
+ /**
461
+ * Extract Next.js Server Action
462
+ */
463
+ function extractServerAction(path, content, relPath) {
464
+ const jsxAttr = path.node;
465
+ const container = jsxAttr.value;
466
+
467
+ if (!t.isJSXExpressionContainer(container)) return null;
468
+
469
+ const expr = container.expression;
470
+ let actionName = null;
471
+
472
+ if (t.isIdentifier(expr)) {
473
+ actionName = expr.name;
474
+ } else if (t.isMemberExpression(expr)) {
475
+ actionName = extractMemberChain(expr);
476
+ }
477
+
478
+ if (!actionName) return null;
479
+
480
+ const lineNum = jsxAttr.loc?.start?.line || 1;
481
+
482
+ return createClientCall({
483
+ kind: CALL_KINDS.SERVER_ACTION,
484
+ runtime: RUNTIMES.SERVER,
485
+ method: "POST",
486
+ urlTemplate: `action://${relPath}#${actionName}`,
487
+ canonicalPath: `action://${relPath}#${actionName}`,
488
+ expectsJson: false,
489
+ authHint: "unknown",
490
+ confidence: "high",
491
+ file: relPath,
492
+ lines: `${lineNum}-${lineNum + 1}`,
493
+ snippet: `action={${actionName}}`,
494
+ reason: `Server Action: ${actionName}`,
495
+ meta: {
496
+ actionExport: actionName,
497
+ module: relPath,
498
+ },
499
+ });
500
+ }
501
+
502
+ // =============================================================================
503
+ // WRAPPER EXTRACTION
504
+ // =============================================================================
505
+
506
+ /**
507
+ * Extract wrapper definitions (axios instances, custom fetch wrappers)
508
+ */
509
+ function extractWrappers(content, relPath, projectRoot) {
510
+ const wrappers = [];
511
+
512
+ let ast;
513
+ try {
514
+ ast = parser.parse(content, {
515
+ sourceType: "module",
516
+ plugins: ["typescript", "jsx", "decorators-legacy"],
517
+ errorRecovery: true,
518
+ });
519
+ } catch {
520
+ return wrappers;
521
+ }
522
+
523
+ traverse(ast, {
524
+ // axios.create()
525
+ CallExpression(path) {
526
+ const { node } = path;
527
+
528
+ if (
529
+ t.isMemberExpression(node.callee) &&
530
+ t.isIdentifier(node.callee.object, { name: "axios" }) &&
531
+ t.isIdentifier(node.callee.property, { name: "create" })
532
+ ) {
533
+ const configArg = node.arguments[0];
534
+ const config = t.isObjectExpression(configArg) ? extractObjectLiteralProps(configArg) : {};
535
+
536
+ // Find variable name
537
+ let name = "axiosInstance";
538
+ if (t.isVariableDeclarator(path.parent)) {
539
+ if (t.isIdentifier(path.parent.id)) {
540
+ name = path.parent.id.name;
541
+ }
542
+ }
543
+
544
+ wrappers.push({
545
+ id: `W_AXIOS_${hashShort(relPath + name)}`,
546
+ name,
547
+ kind: "axios",
548
+ baseURL: config.baseURL || "",
549
+ defaultHeaders: config.headers ? Object.keys(config.headers) : [],
550
+ file: relPath,
551
+ lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
552
+ });
553
+ }
554
+ },
555
+
556
+ // Custom fetch wrappers: export const api = { get: ..., post: ... }
557
+ VariableDeclarator(path) {
558
+ const { node } = path;
559
+
560
+ if (t.isIdentifier(node.id) && t.isObjectExpression(node.init)) {
561
+ const props = node.init.properties;
562
+ const methodProps = props.filter(
563
+ p => t.isIdentifier(p.key) && HTTP_METHODS.map(m => m.toLowerCase()).includes(p.key.name)
564
+ );
565
+
566
+ if (methodProps.length >= 2) {
567
+ // Looks like an API client object
568
+ wrappers.push({
569
+ id: `W_CUSTOM_${hashShort(relPath + node.id.name)}`,
570
+ name: node.id.name,
571
+ kind: "custom",
572
+ baseURL: "",
573
+ methods: methodProps.map(p => p.key.name),
574
+ file: relPath,
575
+ lines: `${node.loc?.start?.line || 1}-${node.loc?.end?.line || 1}`,
576
+ });
577
+ }
578
+ }
579
+ },
580
+ });
581
+
582
+ return wrappers;
583
+ }
584
+
585
+ /**
586
+ * Extract wrapper call (api.get(), apiClient.post(), etc.)
587
+ */
588
+ function extractWrapperCall(path, content, relPath, runtime, wrapperMap) {
589
+ const { node } = path;
590
+
591
+ if (!t.isMemberExpression(node.callee)) return null;
592
+
593
+ const obj = node.callee.object;
594
+ const prop = node.callee.property;
595
+
596
+ if (!t.isIdentifier(obj) || !t.isIdentifier(prop)) return null;
597
+
598
+ const wrapper = wrapperMap.get(obj.name);
599
+ if (!wrapper) return null;
600
+
601
+ const methodName = prop.name.toUpperCase();
602
+ if (!HTTP_METHODS.includes(methodName) && methodName !== "REQUEST") return null;
603
+
604
+ const urlArg = node.arguments[0];
605
+ if (!urlArg) return null;
606
+
607
+ const urlInfo = extractUrlFromNode(urlArg, content);
608
+ if (!urlInfo.value) return null;
609
+
610
+ // Combine with wrapper baseURL
611
+ const fullUrl = combineBaseUrl(wrapper.baseURL, urlInfo.value);
612
+ const lineNum = node.loc?.start?.line || 1;
613
+
614
+ return createClientCall({
615
+ kind: CALL_KINDS.HTTP,
616
+ runtime,
617
+ method: methodName === "REQUEST" ? "UNKNOWN" : methodName,
618
+ urlTemplate: urlInfo.value,
619
+ canonicalPath: canonicalizeUrl(fullUrl),
620
+ expectsJson: true,
621
+ authHint: "unknown",
622
+ confidence: urlInfo.confidence,
623
+ file: relPath,
624
+ lines: `${lineNum}-${lineNum + 3}`,
625
+ snippet: content.substring(node.start, Math.min(node.end, node.start + 150)),
626
+ reason: `${obj.name}.${prop.name}('${urlInfo.value.slice(0, 50)}') via wrapper`,
627
+ meta: {
628
+ wrapperId: wrapper.id,
629
+ },
630
+ });
631
+ }
632
+
633
+ // =============================================================================
634
+ // UI BINDING EXTRACTION
635
+ // =============================================================================
636
+
637
+ /**
638
+ * Extract UI bindings (onClick, onSubmit that call API)
639
+ */
640
+ function extractUIBindings(content, relPath, calls) {
641
+ const bindings = [];
642
+ const callIds = new Set(calls.map(c => c.id));
643
+
644
+ let ast;
645
+ try {
646
+ ast = parser.parse(content, {
647
+ sourceType: "module",
648
+ plugins: ["typescript", "jsx", "decorators-legacy"],
649
+ errorRecovery: true,
650
+ });
651
+ } catch {
652
+ return bindings;
653
+ }
654
+
655
+ traverse(ast, {
656
+ JSXAttribute(path) {
657
+ const { node } = path;
658
+ const attrName = node.name?.name;
659
+
660
+ if (!["onClick", "onSubmit", "onBlur", "onChange", "action"].includes(attrName)) return;
661
+
662
+ if (!t.isJSXExpressionContainer(node.value)) return;
663
+
664
+ const expr = node.value.expression;
665
+ const lineNum = node.loc?.start?.line || 1;
666
+
667
+ // Find associated calls in this handler
668
+ const handlerCalls = calls.filter(c => {
669
+ const callLine = parseInt(c.evidence?.[0]?.lines?.split("-")[0] || "0");
670
+ return Math.abs(callLine - lineNum) < 20; // Within 20 lines
671
+ });
672
+
673
+ if (handlerCalls.length > 0) {
674
+ // Try to find label hint
675
+ const labelHint = findLabelHint(path);
676
+
677
+ bindings.push({
678
+ bindingId: `UIB_${hashShort(relPath + lineNum)}`,
679
+ file: relPath,
680
+ lines: `${lineNum}-${lineNum + 5}`,
681
+ event: attrName,
682
+ labelHint,
683
+ calls: handlerCalls.map(c => c.id),
684
+ });
685
+ }
686
+ },
687
+ });
688
+
689
+ return bindings;
690
+ }
691
+
692
+ /**
693
+ * Find label hint from JSX context
694
+ */
695
+ function findLabelHint(path) {
696
+ // Look for button text, aria-label, etc.
697
+ const parent = path.parentPath;
698
+ if (!parent) return null;
699
+
700
+ // Check for aria-label
701
+ if (t.isJSXOpeningElement(parent.node)) {
702
+ const ariaLabel = parent.node.attributes.find(
703
+ a => t.isJSXAttribute(a) && a.name?.name === "aria-label"
704
+ );
705
+ if (ariaLabel && t.isStringLiteral(ariaLabel.value)) {
706
+ return ariaLabel.value.value;
707
+ }
708
+ }
709
+
710
+ // Check for children text
711
+ const jsxElement = parent.parentPath;
712
+ if (t.isJSXElement(jsxElement?.node)) {
713
+ const textChild = jsxElement.node.children.find(
714
+ c => t.isJSXText(c) && c.value.trim()
715
+ );
716
+ if (textChild) {
717
+ return textChild.value.trim();
718
+ }
719
+ }
720
+
721
+ return null;
722
+ }
723
+
724
+ // =============================================================================
725
+ // HELPERS
726
+ // =============================================================================
727
+
728
+ /**
729
+ * Extract URL value and confidence from AST node
730
+ */
731
+ function extractUrlFromNode(node, content) {
732
+ // String literal: HIGH confidence
733
+ if (t.isStringLiteral(node)) {
734
+ return { value: node.value, confidence: "high" };
735
+ }
736
+
737
+ // Template literal
738
+ if (t.isTemplateLiteral(node)) {
739
+ if (node.expressions.length === 0) {
740
+ // No expressions: HIGH
741
+ return { value: node.quasis[0].value.raw, confidence: "high" };
742
+ }
743
+ // Has expressions: MEDIUM
744
+ const template = node.quasis.map((q, i) => {
745
+ const expr = node.expressions[i];
746
+ return q.value.raw + (expr ? `{${i}}` : "");
747
+ }).join("");
748
+ return { value: template, confidence: "medium" };
749
+ }
750
+
751
+ // Binary expression (concatenation)
752
+ if (t.isBinaryExpression(node, { operator: "+" })) {
753
+ const left = extractUrlFromNode(node.left, content);
754
+ const right = extractUrlFromNode(node.right, content);
755
+ if (left.value && right.value) {
756
+ return {
757
+ value: left.value + right.value,
758
+ confidence: "medium",
759
+ };
760
+ }
761
+ }
762
+
763
+ // Variable: LOW confidence
764
+ if (t.isIdentifier(node)) {
765
+ return { value: `\${${node.name}}`, confidence: "low" };
766
+ }
767
+
768
+ return { value: null, confidence: "low" };
769
+ }
770
+
771
+ /**
772
+ * Extract method from fetch init argument
773
+ */
774
+ function extractMethodFromInit(initNode) {
775
+ if (!initNode || !t.isObjectExpression(initNode)) return null;
776
+
777
+ const methodProp = initNode.properties.find(
778
+ p => t.isObjectProperty(p) && t.isIdentifier(p.key, { name: "method" })
779
+ );
780
+
781
+ if (methodProp && t.isStringLiteral(methodProp.value)) {
782
+ return methodProp.value.value.toUpperCase();
783
+ }
784
+
785
+ return null;
786
+ }
787
+
788
+ /**
789
+ * Extract expectsJson from init
790
+ */
791
+ function extractExpectsJson(initNode, path) {
792
+ if (!initNode) return false;
793
+
794
+ if (t.isObjectExpression(initNode)) {
795
+ const props = extractObjectLiteralProps(initNode);
796
+ if (props.headers?.["content-type"]?.includes("application/json")) return true;
797
+ if (props.body && typeof props.body === "string" && props.body.includes("JSON.stringify")) return true;
798
+ }
799
+
800
+ // Check if .json() is called on response
801
+ // This would require more complex flow analysis
802
+ return false;
803
+ }
804
+
805
+ /**
806
+ * Extract auth hint from init
807
+ */
808
+ function extractAuthHint(initNode) {
809
+ if (!initNode || !t.isObjectExpression(initNode)) return "unknown";
810
+
811
+ const props = extractObjectLiteralProps(initNode);
812
+
813
+ if (props.credentials === "include") return "cookie";
814
+ if (props.headers?.authorization?.toLowerCase().startsWith("bearer")) return "bearer";
815
+ if (props.headers?.Authorization?.toLowerCase().startsWith("bearer")) return "bearer";
816
+
817
+ return "unknown";
818
+ }
819
+
820
+ /**
821
+ * Extract object literal properties as plain object
822
+ */
823
+ function extractObjectLiteralProps(node) {
824
+ const props = {};
825
+
826
+ for (const prop of node.properties) {
827
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
828
+ const key = prop.key.name;
829
+
830
+ if (t.isStringLiteral(prop.value)) {
831
+ props[key] = prop.value.value;
832
+ } else if (t.isObjectExpression(prop.value)) {
833
+ props[key] = extractObjectLiteralProps(prop.value);
834
+ } else if (t.isIdentifier(prop.value)) {
835
+ props[key] = `$${prop.value.name}`;
836
+ }
837
+ }
838
+ }
839
+
840
+ return props;
841
+ }
842
+
843
+ /**
844
+ * Extract member expression chain as string
845
+ */
846
+ function extractMemberChain(node) {
847
+ const parts = [];
848
+
849
+ let current = node;
850
+ while (t.isMemberExpression(current)) {
851
+ if (t.isIdentifier(current.property)) {
852
+ parts.unshift(current.property.name);
853
+ }
854
+ current = current.object;
855
+ }
856
+
857
+ if (t.isIdentifier(current)) {
858
+ parts.unshift(current.name);
859
+ }
860
+
861
+ return parts.join(".");
862
+ }
863
+
864
+ /**
865
+ * Infer runtime from file content
866
+ */
867
+ function inferRuntime(content, relPath) {
868
+ if (content.includes('"use client"') || content.includes("'use client'")) {
869
+ return RUNTIMES.CLIENT;
870
+ }
871
+ if (content.includes('"use server"') || content.includes("'use server'")) {
872
+ return RUNTIMES.SERVER;
873
+ }
874
+ if (relPath.includes("/api/") || relPath.includes("server")) {
875
+ return RUNTIMES.SERVER;
876
+ }
877
+ if (relPath.includes("/lib/") || relPath.includes("/utils/")) {
878
+ return RUNTIMES.SHARED;
879
+ }
880
+ return RUNTIMES.UNKNOWN;
881
+ }
882
+
883
+ /**
884
+ * Canonicalize URL path
885
+ */
886
+ function canonicalizeUrl(url) {
887
+ if (!url) return null;
888
+
889
+ let canonical = url;
890
+
891
+ // Strip protocol and host
892
+ canonical = canonical.replace(/^https?:\/\/[^/]+/, "");
893
+
894
+ // Strip query string
895
+ canonical = canonical.split("?")[0];
896
+
897
+ // Strip hash
898
+ canonical = canonical.split("#")[0];
899
+
900
+ // Replace template expressions with params
901
+ canonical = canonical.replace(/\$\{[^}]+\}/g, "{param}");
902
+ canonical = canonical.replace(/\{[0-9]+\}/g, "{param}");
903
+
904
+ // Normalize slashes
905
+ canonical = canonical.replace(/\/+/g, "/");
906
+
907
+ // Ensure leading slash
908
+ if (!canonical.startsWith("/") && !canonical.startsWith("action://")) {
909
+ canonical = "/" + canonical;
910
+ }
911
+
912
+ // Remove trailing slash (except root)
913
+ if (canonical.length > 1) {
914
+ canonical = canonical.replace(/\/$/, "");
915
+ }
916
+
917
+ return canonical;
918
+ }
919
+
920
+ /**
921
+ * Combine base URL with path
922
+ */
923
+ function combineBaseUrl(base, path) {
924
+ if (!base) return path;
925
+ if (!path) return base;
926
+
927
+ const baseTrimmed = base.replace(/\/$/, "");
928
+ const pathTrimmed = path.startsWith("/") ? path : "/" + path;
929
+
930
+ return baseTrimmed + pathTrimmed;
931
+ }
932
+
933
+ /**
934
+ * Create a ClientCall record
935
+ */
936
+ function createClientCall(opts) {
937
+ const id = `C_CALL_${hashShort(opts.file + opts.urlTemplate + opts.method)}`;
938
+
939
+ return {
940
+ id,
941
+ kind: opts.kind,
942
+ runtime: opts.runtime,
943
+ method: opts.method,
944
+ urlTemplate: opts.urlTemplate,
945
+ canonicalPath: opts.canonicalPath,
946
+ expectsJson: opts.expectsJson,
947
+ authHint: opts.authHint,
948
+ confidence: opts.confidence,
949
+ evidence: [{
950
+ id: `E_${hashShort(opts.file + opts.lines)}`,
951
+ kind: "file",
952
+ file: opts.file,
953
+ lines: opts.lines,
954
+ snippetHash: hashSnippet(opts.snippet || ""),
955
+ reason: opts.reason,
956
+ }],
957
+ meta: opts.meta || {},
958
+ };
959
+ }
960
+
961
+ function hashShort(text) {
962
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 12).toUpperCase();
963
+ }
964
+
965
+ function hashSnippet(text) {
966
+ return `sha256:${crypto.createHash("sha256").update(text).digest("hex")}`;
967
+ }
968
+
969
+ // =============================================================================
970
+ // EXPORTS
971
+ // =============================================================================
972
+
973
+ module.exports = {
974
+ extractClientCalls,
975
+ extractCallsFromFile,
976
+ extractFetchCall,
977
+ extractAxiosCall,
978
+ extractTRPCCall,
979
+ extractGraphQLCall,
980
+ extractServerAction,
981
+ extractWrappers,
982
+ extractWrapperCall,
983
+ extractUIBindings,
984
+ canonicalizeUrl,
985
+ combineBaseUrl,
986
+ inferRuntime,
987
+ CALL_KINDS,
988
+ RUNTIMES,
989
+ HTTP_METHODS,
990
+ };