within-sdk 1.0.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 (69) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +77 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.js +243 -0
  5. package/dist/middleware.d.ts +40 -0
  6. package/dist/middleware.js +562 -0
  7. package/dist/modules/compatibility.d.ts +18 -0
  8. package/dist/modules/compatibility.js +99 -0
  9. package/dist/modules/constants.d.ts +12 -0
  10. package/dist/modules/constants.js +12 -0
  11. package/dist/modules/context-parameters.d.ts +13 -0
  12. package/dist/modules/context-parameters.js +74 -0
  13. package/dist/modules/diagnostics.d.ts +7 -0
  14. package/dist/modules/diagnostics.js +12 -0
  15. package/dist/modules/eventQueue.d.ts +32 -0
  16. package/dist/modules/eventQueue.js +234 -0
  17. package/dist/modules/exceptions.d.ts +13 -0
  18. package/dist/modules/exceptions.js +730 -0
  19. package/dist/modules/exporters/datadog.d.ts +17 -0
  20. package/dist/modules/exporters/datadog.js +166 -0
  21. package/dist/modules/exporters/otlp.d.ts +13 -0
  22. package/dist/modules/exporters/otlp.js +140 -0
  23. package/dist/modules/exporters/posthog.d.ts +32 -0
  24. package/dist/modules/exporters/posthog.js +272 -0
  25. package/dist/modules/exporters/sentry.d.ts +27 -0
  26. package/dist/modules/exporters/sentry.js +386 -0
  27. package/dist/modules/exporters/trace-context.d.ts +8 -0
  28. package/dist/modules/exporters/trace-context.js +27 -0
  29. package/dist/modules/index.d.ts +8 -0
  30. package/dist/modules/index.js +8 -0
  31. package/dist/modules/internal.d.ts +33 -0
  32. package/dist/modules/internal.js +195 -0
  33. package/dist/modules/logging.d.ts +2 -0
  34. package/dist/modules/logging.js +74 -0
  35. package/dist/modules/mcp-sdk-compat.d.ts +32 -0
  36. package/dist/modules/mcp-sdk-compat.js +111 -0
  37. package/dist/modules/privacy.d.ts +15 -0
  38. package/dist/modules/privacy.js +179 -0
  39. package/dist/modules/redaction.d.ts +11 -0
  40. package/dist/modules/redaction.js +81 -0
  41. package/dist/modules/sanitization.d.ts +9 -0
  42. package/dist/modules/sanitization.js +111 -0
  43. package/dist/modules/session.d.ts +22 -0
  44. package/dist/modules/session.js +109 -0
  45. package/dist/modules/telemetry.d.ts +8 -0
  46. package/dist/modules/telemetry.js +53 -0
  47. package/dist/modules/tools.d.ts +25 -0
  48. package/dist/modules/tools.js +119 -0
  49. package/dist/modules/tracing.d.ts +4 -0
  50. package/dist/modules/tracing.js +263 -0
  51. package/dist/modules/tracingV2.d.ts +2 -0
  52. package/dist/modules/tracingV2.js +300 -0
  53. package/dist/modules/truncation.d.ts +24 -0
  54. package/dist/modules/truncation.js +303 -0
  55. package/dist/modules/validation.d.ts +6 -0
  56. package/dist/modules/validation.js +51 -0
  57. package/dist/network.d.ts +83 -0
  58. package/dist/network.js +411 -0
  59. package/dist/proxy.d.ts +31 -0
  60. package/dist/proxy.js +158 -0
  61. package/dist/thirdparty/ksuid/base-convert-int-array.js +49 -0
  62. package/dist/thirdparty/ksuid/base62.js +24 -0
  63. package/dist/thirdparty/ksuid/index.d.ts +30 -0
  64. package/dist/thirdparty/ksuid/index.js +201 -0
  65. package/dist/types.d.ts +197 -0
  66. package/dist/types.js +5 -0
  67. package/dist/validate.d.ts +53 -0
  68. package/dist/validate.js +139 -0
  69. package/package.json +43 -0
@@ -0,0 +1,730 @@
1
+ import { createRequire } from "module";
2
+ // Lazy-loaded fs module for context_line extraction (Node.js only)
3
+ // Edge environments don't have filesystem access
4
+ let fsModule = null;
5
+ let fsInitAttempted = false;
6
+ function getFsSync() {
7
+ if (!fsInitAttempted) {
8
+ fsInitAttempted = true;
9
+ try {
10
+ // Use createRequire for ESM compatibility
11
+ // Works in Node.js ESM/CJS, fails gracefully in Workers/edge environments
12
+ const require = createRequire(import.meta.url);
13
+ fsModule = require("fs");
14
+ }
15
+ catch {
16
+ fsModule = null;
17
+ }
18
+ }
19
+ return fsModule;
20
+ }
21
+ // Maximum number of exceptions to capture in a cause chain
22
+ const MAX_EXCEPTION_CHAIN_DEPTH = 10;
23
+ // Maximum number of stack frames to capture per exception
24
+ const MAX_STACK_FRAMES = 50;
25
+ /**
26
+ * Captures detailed exception information including stack traces and cause chains.
27
+ *
28
+ * This function extracts error metadata (type, message, stack trace) and recursively
29
+ * unwraps Error.cause chains. It parses V8 stack traces into structured frames and
30
+ * detects whether each frame is user code (in_app: true) or library code (in_app: false).
31
+ *
32
+ * @param error - The error to capture (can be Error, string, object, or any value)
33
+ * @param contextStack - Optional Error object to use for stack context (for validation errors)
34
+ * @returns ErrorData object with structured error information
35
+ */
36
+ export function captureException(error, contextStack) {
37
+ // Handle CallToolResult objects (SDK 1.21.0+ converts errors to these)
38
+ if (isCallToolResult(error)) {
39
+ return captureCallToolResultError(error, contextStack);
40
+ }
41
+ // Handle non-Error objects
42
+ if (!(error instanceof Error)) {
43
+ return {
44
+ message: stringifyNonError(error),
45
+ type: undefined,
46
+ platform: "javascript",
47
+ };
48
+ }
49
+ const errorData = {
50
+ message: error.message || "",
51
+ type: error.name || error.constructor?.name || undefined,
52
+ platform: "javascript",
53
+ };
54
+ // Capture stack trace if available
55
+ if (error.stack) {
56
+ errorData.stack = error.stack;
57
+ errorData.frames = parseV8StackTrace(error.stack);
58
+ }
59
+ // Unwrap Error.cause chain
60
+ const chainedErrors = unwrapErrorCauses(error);
61
+ if (chainedErrors.length > 0) {
62
+ errorData.chained_errors = chainedErrors;
63
+ }
64
+ return errorData;
65
+ }
66
+ /**
67
+ * Parses V8 stack trace string into structured StackFrame array.
68
+ *
69
+ * V8 stack traces have the format:
70
+ * Error: message
71
+ * at functionName (filename:line:col)
72
+ * at Object.method (filename:line:col)
73
+ * ...
74
+ *
75
+ * This function handles various V8 format variations including:
76
+ * - Regular functions: "at functionName (file:10:5)"
77
+ * - Anonymous functions: "at file:10:5"
78
+ * - Async functions: "at async functionName (file:10:5)"
79
+ * - Object methods: "at Object.method (file:10:5)"
80
+ * - Native code: "at Array.map (native)"
81
+ *
82
+ * @param stackTrace - Raw V8 stack trace string from Error.stack
83
+ * @returns Array of parsed StackFrame objects (limited to MAX_STACK_FRAMES)
84
+ */
85
+ function parseV8StackTrace(stackTrace) {
86
+ const frames = [];
87
+ const lines = stackTrace.split("\n");
88
+ for (const line of lines) {
89
+ // Skip the first line (error message) and empty lines
90
+ if (!line.trim().startsWith("at ")) {
91
+ continue;
92
+ }
93
+ const frame = parseV8StackFrame(line.trim());
94
+ if (frame) {
95
+ addContextToFrame(frame);
96
+ frames.push(frame);
97
+ }
98
+ // Limit number of frames
99
+ if (frames.length >= MAX_STACK_FRAMES) {
100
+ break;
101
+ }
102
+ }
103
+ return frames;
104
+ }
105
+ /**
106
+ * Adds context_line to a stack frame by reading the source file.
107
+ *
108
+ * This function extracts the line of code where the error occurred by:
109
+ * 1. Reading the source file using abs_path
110
+ * 2. Extracting the line at the specified line number
111
+ * 3. Setting the context_line field on the frame
112
+ *
113
+ * Only extracts context for user code (in_app: true)
114
+ * If the file cannot be read or the line number is invalid, context_line remains undefined.
115
+ *
116
+ * @param frame - The StackFrame to add context to (modified in place)
117
+ * @returns The modified StackFrame
118
+ */
119
+ function addContextToFrame(frame) {
120
+ if (!frame.in_app || !frame.abs_path || !frame.lineno) {
121
+ return frame;
122
+ }
123
+ // Get fs module lazily - returns null in edge environments
124
+ const fs = getFsSync();
125
+ if (!fs) {
126
+ return frame; // File reading not available in this environment
127
+ }
128
+ try {
129
+ const source = fs.readFileSync(frame.abs_path, "utf8");
130
+ const lines = source.split("\n");
131
+ const lineIndex = frame.lineno - 1; // Convert to 0-based index
132
+ if (lineIndex >= 0 && lineIndex < lines.length) {
133
+ frame.context_line = lines[lineIndex];
134
+ }
135
+ }
136
+ catch {
137
+ // File not found or not readable - silently skip
138
+ }
139
+ return frame;
140
+ }
141
+ /**
142
+ * Parses a location string from a V8 stack frame.
143
+ *
144
+ * Handles different location formats:
145
+ * - "fileName:lineNumber:columnNumber" - normal file location
146
+ * - "eval at functionName (location)" - eval'd code (recursively unwraps)
147
+ * - "native" - V8 internal code
148
+ * - "unknown location" - location unavailable
149
+ *
150
+ * @param location - Location string from stack frame
151
+ * @returns Object with filename, abs_path, and optional lineno/colno, or null if unparseable
152
+ */
153
+ function parseLocation(location) {
154
+ // Handle special cases first
155
+ if (location === "native") {
156
+ return { filename: "native", abs_path: "native" };
157
+ }
158
+ if (location === "unknown location") {
159
+ return { filename: "<unknown>", abs_path: "<unknown>" };
160
+ }
161
+ // Handle eval locations
162
+ if (location.startsWith("eval at ")) {
163
+ return parseEvalOrigin(location);
164
+ }
165
+ // Handle normal location format: fileName:lineNumber:columnNumber
166
+ const match = location.match(/^(.+):(\d+):(\d+)$/);
167
+ if (match) {
168
+ const [, filename, lineStr, colStr] = match;
169
+ return {
170
+ filename: makeRelativePath(filename),
171
+ abs_path: filename,
172
+ lineno: parseInt(lineStr, 10),
173
+ colno: parseInt(colStr, 10),
174
+ };
175
+ }
176
+ return null;
177
+ }
178
+ /**
179
+ * Recursively unwraps eval location chains to extract the underlying file location.
180
+ *
181
+ * Eval locations have the format: "eval at functionName (location), <anonymous>:line:col"
182
+ * where location can be another eval or a file location.
183
+ *
184
+ * V8 formats:
185
+ * - "eval at Bar.z (myscript.js:10:3)" → extract myscript.js:10:3
186
+ * - "eval at Foo (eval at Bar (file.js:10:3)), <anonymous>:5:2" → extract file.js:10:3
187
+ *
188
+ * @param evalLocation - Eval location string starting with "eval at "
189
+ * @returns Object with extracted file location, or null if unparseable
190
+ */
191
+ function parseEvalOrigin(evalLocation) {
192
+ // V8 format: "eval at functionName (parentLocation), <anonymous>:line:col"
193
+ // or simpler: "eval at functionName (parentLocation)"
194
+ //
195
+ // Strategy: Find balanced parentheses to extract the parent location,
196
+ // then recursively parse it to find the actual file.
197
+ // First, check if there's a comma separating eval chain from eval code location
198
+ // Format: "eval at FUNC (...), <anonymous>:line:col"
199
+ // We want to extract just the "eval at FUNC (...)" part
200
+ let evalChainPart = evalLocation;
201
+ const commaIndex = findCommaAfterBalancedParens(evalLocation);
202
+ if (commaIndex !== -1) {
203
+ evalChainPart = evalLocation.substring(0, commaIndex);
204
+ }
205
+ // Match "eval at <anything> (<innerLocation>)"
206
+ const match = evalChainPart.match(/^eval at (.+?) \((.+)\)$/);
207
+ if (!match) {
208
+ return null;
209
+ }
210
+ const innerLocation = match[2];
211
+ // Recursively parse the inner location
212
+ if (innerLocation.startsWith("eval at ")) {
213
+ return parseEvalOrigin(innerLocation);
214
+ }
215
+ // Base case: parse as normal location
216
+ const locationMatch = innerLocation.match(/^(.+):(\d+):(\d+)$/);
217
+ if (locationMatch) {
218
+ const [, filename, lineStr, colStr] = locationMatch;
219
+ return {
220
+ filename: makeRelativePath(filename),
221
+ abs_path: filename,
222
+ lineno: parseInt(lineStr, 10),
223
+ colno: parseInt(colStr, 10),
224
+ };
225
+ }
226
+ return null;
227
+ }
228
+ /**
229
+ * Finds the index of the comma that appears after balanced parentheses.
230
+ *
231
+ * For "eval at f (eval at g (x)), <anonymous>:1:2", returns the index of the comma
232
+ * after the closing ")" and before "<anonymous>".
233
+ *
234
+ * @param str - String to search
235
+ * @returns Index of comma, or -1 if not found
236
+ */
237
+ function findCommaAfterBalancedParens(str) {
238
+ let depth = 0;
239
+ let foundOpenParen = false;
240
+ for (let i = 0; i < str.length; i++) {
241
+ if (str[i] === "(") {
242
+ depth++;
243
+ foundOpenParen = true;
244
+ }
245
+ else if (str[i] === ")") {
246
+ depth--;
247
+ if (depth === 0 && foundOpenParen) {
248
+ // Found the closing paren of the eval at (...) part
249
+ for (let j = i + 1; j < str.length; j++) {
250
+ if (str[j] === ",") {
251
+ return j;
252
+ }
253
+ else if (str[j] !== " ") {
254
+ // Non-comma, non-space character found, no comma separator
255
+ return -1;
256
+ }
257
+ }
258
+ return -1;
259
+ }
260
+ }
261
+ }
262
+ return -1;
263
+ }
264
+ /**
265
+ * Parses a single V8 stack frame line into a StackFrame object.
266
+ *
267
+ * Handles multiple V8 stack frame formats:
268
+ * - "at functionName (filename:line:col)"
269
+ * - "at filename:line:col" (top-level code)
270
+ * - "at async functionName (filename:line:col)"
271
+ * - "at Object.method (filename:line:col)"
272
+ * - "at Module._compile (node:internal/...)" (internal modules)
273
+ * - "at functionName (eval at ...)" (eval'd code)
274
+ * - "at functionName (native)" (native code)
275
+ *
276
+ * @param line - Single line from V8 stack trace (trimmed, starts with "at ")
277
+ * @returns Parsed StackFrame or null if line cannot be parsed
278
+ */
279
+ function parseV8StackFrame(line) {
280
+ // Remove "at " prefix
281
+ const withoutAt = line.substring(3);
282
+ // Try to extract function name and location
283
+ // Format 1: "functionName (location)"
284
+ // Location can be: filename:line:col, eval at ..., native, unknown location
285
+ const matchWithFunction = withoutAt.match(/^(.+?)\s+\((.+)\)$/);
286
+ if (matchWithFunction) {
287
+ const [, functionName, location] = matchWithFunction;
288
+ const parsedLocation = parseLocation(location);
289
+ if (parsedLocation) {
290
+ return {
291
+ function: functionName.trim(),
292
+ filename: parsedLocation.filename,
293
+ abs_path: parsedLocation.abs_path,
294
+ lineno: parsedLocation.lineno,
295
+ colno: parsedLocation.colno,
296
+ in_app: isInApp(parsedLocation.abs_path),
297
+ };
298
+ }
299
+ }
300
+ // Format 2: "location" (no function name, top-level code)
301
+ // Try to parse as location directly
302
+ const parsedLocation = parseLocation(withoutAt);
303
+ if (parsedLocation) {
304
+ return {
305
+ function: "<anonymous>",
306
+ filename: parsedLocation.filename,
307
+ abs_path: parsedLocation.abs_path,
308
+ lineno: parsedLocation.lineno,
309
+ colno: parsedLocation.colno,
310
+ in_app: isInApp(parsedLocation.abs_path),
311
+ };
312
+ }
313
+ // Format 3: Unparseable
314
+ // Fallback for formats we don't recognize
315
+ return {
316
+ function: withoutAt,
317
+ filename: "<unknown>",
318
+ in_app: false,
319
+ };
320
+ }
321
+ /**
322
+ * Determines if a file path represents user code (in_app: true) or library code (in_app: false).
323
+ *
324
+ * Library code is identified by:
325
+ * - Paths containing "/node_modules/"
326
+ * - Node.js internal modules (e.g., "node:internal/...")
327
+ * - Native code
328
+ *
329
+ * @param filename - File path from stack frame
330
+ * @returns true if user code, false if library code
331
+ */
332
+ function isInApp(filename) {
333
+ // Exclude node_modules
334
+ if (filename.includes("/node_modules/") ||
335
+ filename.includes("\\node_modules\\")) {
336
+ return false;
337
+ }
338
+ // Exclude Node.js internal modules (node:internal/...)
339
+ if (filename.startsWith("node:")) {
340
+ return false;
341
+ }
342
+ // Exclude native code
343
+ if (filename === "native" || filename === "<unknown>") {
344
+ return false;
345
+ }
346
+ return true;
347
+ }
348
+ /**
349
+ * Normalizes URL schemes to regular file paths.
350
+ *
351
+ * Handles file:// URLs commonly seen in ESM modules and local testing:
352
+ * - "file:///Users/john/project/src/index.ts" → "/Users/john/project/src/index.ts"
353
+ * - "file:///C:/projects/app/src/index.ts" → "C:/projects/app/src/index.ts"
354
+ *
355
+ * @param filename - File path that may be a file:// URL
356
+ * @returns Clean file path without URL scheme
357
+ */
358
+ function normalizeUrl(filename) {
359
+ // Handle file:// URLs (common in ESM modules and local testing)
360
+ if (filename.startsWith("file://")) {
361
+ let result = filename.substring(7); // Remove "file://"
362
+ // Ensure Unix paths start with /
363
+ if (!result.startsWith("/") && !result.match(/^[A-Za-z]:/)) {
364
+ result = "/" + result;
365
+ }
366
+ return result;
367
+ }
368
+ return filename;
369
+ }
370
+ /**
371
+ * Normalizes Node.js internal module paths for consistent error grouping.
372
+ *
373
+ * Examples:
374
+ * - "node:internal/modules/cjs/loader" → "node:internal"
375
+ * - "node:fs/promises" → "node:fs"
376
+ * - "node:fs" → "node:fs" (unchanged)
377
+ *
378
+ * @param filename - File path that may be a Node.js internal module
379
+ * @returns Simplified module path or original filename
380
+ */
381
+ function normalizeNodeInternals(filename) {
382
+ if (filename.startsWith("node:internal")) {
383
+ return "node:internal";
384
+ }
385
+ if (filename.startsWith("node:")) {
386
+ // Extract just the module name: node:fs/promises → node:fs
387
+ const parts = filename.split("/");
388
+ return parts[0];
389
+ }
390
+ return filename;
391
+ }
392
+ /**
393
+ * Strips user-specific and system path prefixes.
394
+ *
395
+ * Removes prefixes like:
396
+ * - /Users/username/ → ~/
397
+ * - /home/username/ → ~/
398
+ * - C:\Users\username\ → ~\
399
+ * - C:/Users/username/ → ~/ (mixed separators)
400
+ *
401
+ * @param path - File path to normalize
402
+ * @returns Path with system prefixes removed
403
+ */
404
+ function stripSystemPrefixes(path) {
405
+ // Unix/macOS: /Users/username/
406
+ path = path.replace(/^\/Users\/[^/]+\//, "~/");
407
+ // Linux: /home/username/
408
+ path = path.replace(/^\/home\/[^/]+\//, "~/");
409
+ // Windows: C:\Users\username\ or C:/Users/username/ (with any separator)
410
+ path = path.replace(/^[A-Za-z]:[\\\/]Users[\\\/][^\\\/]+[\\\/]/, "~/");
411
+ return path;
412
+ }
413
+ /**
414
+ * Normalizes node_modules paths to be consistent across deployments.
415
+ *
416
+ * Extracts only the package-relative portion of the path:
417
+ * - /Users/john/project/node_modules/express/lib/router.js → node_modules/express/lib/router.js
418
+ * - /app/node_modules/@scope/pkg/index.js → node_modules/@scope/pkg/index.js
419
+ *
420
+ * @param path - File path that may contain node_modules
421
+ * @returns Normalized node_modules path or original path
422
+ */
423
+ function normalizeNodeModules(path) {
424
+ // Find the last occurrence of /node_modules/ or \node_modules\
425
+ const unixIndex = path.lastIndexOf("/node_modules/");
426
+ const winIndex = path.lastIndexOf("\\node_modules\\");
427
+ if (unixIndex !== -1) {
428
+ return path.substring(unixIndex + 1); // +1 to exclude leading slash
429
+ }
430
+ if (winIndex !== -1) {
431
+ return path.substring(winIndex + 1).replace(/\\/g, "/");
432
+ }
433
+ return path;
434
+ }
435
+ /**
436
+ * Strips common deployment-specific path prefixes.
437
+ *
438
+ * Removes prefixes like:
439
+ * - /var/www/app/ → ""
440
+ * - /app/ → ""
441
+ * - /opt/project/ → ""
442
+ * - /var/task/ → "" (AWS Lambda)
443
+ * - /usr/src/app/ → "" (Docker)
444
+ *
445
+ * @param path - File path to normalize
446
+ * @returns Path with deployment prefixes removed
447
+ */
448
+ function stripDeploymentPaths(path) {
449
+ // Common deployment paths
450
+ const deploymentPrefixes = [
451
+ /^\/var\/www\/[^/]+\//, // Apache/nginx: /var/www/myapp/
452
+ /^\/var\/task\//, // AWS Lambda: /var/task/
453
+ /^\/usr\/src\/app\//, // Docker: /usr/src/app/
454
+ /^\/app\//, // Heroku, Docker, generic: /app/
455
+ /^\/opt\/[^/]+\//, // Optional software: /opt/myapp/
456
+ /^\/srv\/[^/]+\//, // Service data: /srv/myapp/
457
+ ];
458
+ for (const prefix of deploymentPrefixes) {
459
+ path = path.replace(prefix, "");
460
+ }
461
+ return path;
462
+ }
463
+ /**
464
+ * Finds project-relative path using common project boundary markers.
465
+ *
466
+ * Looks for markers like /src/, /lib/, /dist/, /build/ and extracts the path
467
+ * from that marker onwards:
468
+ * - /Users/john/project/src/components/Button.tsx → src/components/Button.tsx
469
+ * - /app/dist/index.js → dist/index.js
470
+ *
471
+ * Priority order: looks for primary markers first (src, lib, dist, build),
472
+ * then secondary markers. Uses the highest-priority marker found.
473
+ *
474
+ * @param path - File path to search for project boundaries
475
+ * @returns Project-relative path or original path if no marker found
476
+ */
477
+ function findProjectPath(path) {
478
+ // Project boundary markers in priority order
479
+ // Primary markers (most likely to be project root)
480
+ const primaryMarkers = ["/src/", "/lib/", "/dist/", "/build/"];
481
+ // Secondary markers (could be subdirectories)
482
+ const secondaryMarkers = [
483
+ "/app/",
484
+ "/components/",
485
+ "/pages/",
486
+ "/api/",
487
+ "/utils/",
488
+ "/services/",
489
+ "/modules/",
490
+ ];
491
+ // Check primary markers first
492
+ for (const marker of primaryMarkers) {
493
+ const index = path.lastIndexOf(marker);
494
+ if (index !== -1) {
495
+ return path.substring(index + 1); // +1 to remove leading slash
496
+ }
497
+ }
498
+ // If no primary marker, check secondary markers
499
+ for (const marker of secondaryMarkers) {
500
+ const index = path.lastIndexOf(marker);
501
+ if (index !== -1) {
502
+ return path.substring(index + 1);
503
+ }
504
+ }
505
+ return path;
506
+ }
507
+ /**
508
+ * Converts absolute file paths to normalized relative paths for consistent error grouping.
509
+ *
510
+ * This function performs comprehensive path normalization to ensure errors from the same
511
+ * code location group together regardless of deployment environment, user directories,
512
+ * or system-specific paths. The original absolute path is always preserved in abs_path.
513
+ *
514
+ * Normalization steps:
515
+ * 1. Normalize URL schemes (file://, etc.) - must be first to strip URL prefixes
516
+ * 2. Preserve special paths (already relative, Node internals, etc.)
517
+ * 3. Normalize path separators to forward slashes (for consistent processing)
518
+ * 4. Normalize Node.js internal modules (node:internal/*, node:fs/*)
519
+ * 5. Normalize node_modules paths to package-relative format
520
+ * 6. Strip user home directories (/Users/*, /home/*, C:\Users\*)
521
+ * 7. Strip deployment-specific paths (/var/www/*, /app/, AWS Lambda, Docker)
522
+ * 8. Strip current working directory
523
+ * 9. Find project boundaries (/src/, /lib/, /dist/, etc.)
524
+ * 10. Remove leading slashes for clean relative paths
525
+ *
526
+ * @param filename - Absolute or relative file path from stack trace
527
+ * @returns Normalized relative path for error grouping
528
+ *
529
+ * @example
530
+ * makeRelativePath('/Users/john/project/src/index.ts')
531
+ * // Returns: 'src/index.ts'
532
+ *
533
+ * @example
534
+ * makeRelativePath('/home/ubuntu/app/node_modules/express/lib/router.js')
535
+ * // Returns: 'node_modules/express/lib/router.js'
536
+ *
537
+ * @example
538
+ * makeRelativePath('/var/www/myapp/dist/server.js')
539
+ * // Returns: 'dist/server.js'
540
+ *
541
+ * @example
542
+ * makeRelativePath('node:internal/modules/cjs/loader')
543
+ * // Returns: 'node:internal'
544
+ *
545
+ * @example
546
+ * makeRelativePath('C:\\Users\\John\\projects\\myapp\\src\\index.ts')
547
+ * // Returns: 'src/index.ts'
548
+ */
549
+ function makeRelativePath(filename) {
550
+ let result = filename;
551
+ // Step 1: Normalize URL schemes (file://, etc.)
552
+ result = normalizeUrl(result);
553
+ // Step 2: Handle already-relative paths and special cases
554
+ if (!result.startsWith("/") && !result.match(/^[A-Za-z]:\\/)) {
555
+ // Already relative or special path (native, <unknown>, etc.)
556
+ // Still normalize Node internals
557
+ if (result.startsWith("node:")) {
558
+ return normalizeNodeInternals(result);
559
+ }
560
+ return result;
561
+ }
562
+ // Step 3: Normalize path separators early for consistent processing
563
+ result = result.replace(/\\/g, "/");
564
+ // Step 4: Normalize Node.js internal modules (should be rare at this point)
565
+ if (result.startsWith("node:")) {
566
+ return normalizeNodeInternals(result);
567
+ }
568
+ // Step 5: Handle node_modules specially - preserve package structure
569
+ if (result.includes("/node_modules/")) {
570
+ return normalizeNodeModules(result);
571
+ }
572
+ // Step 6: Strip user home directories
573
+ result = stripSystemPrefixes(result);
574
+ // Step 7: Strip deployment-specific paths
575
+ result = stripDeploymentPaths(result);
576
+ // Step 8: Strip current working directory (if available)
577
+ // process.cwd() may not be available in edge environments
578
+ let cwd = null;
579
+ try {
580
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
581
+ cwd = process.cwd();
582
+ }
583
+ }
584
+ catch {
585
+ // process.cwd() not available in this environment
586
+ }
587
+ if (cwd && result.startsWith(cwd)) {
588
+ result = result.substring(cwd.length + 1); // +1 to remove leading /
589
+ }
590
+ // Step 9: Find project boundaries if still absolute-looking
591
+ // Also apply to tilde paths that might have project markers after the tilde
592
+ if (result.startsWith("/") || result.match(/^[A-Za-z]:[/]/)) {
593
+ result = findProjectPath(result);
594
+ }
595
+ else if (result.startsWith("~")) {
596
+ // For tilde paths, strip the tilde and find markers in the remaining path
597
+ const withoutTilde = result.substring(2); // Remove ~/
598
+ const projectPath = findProjectPath("/" + withoutTilde);
599
+ // If a marker was found (path changed), use it; otherwise keep the tilde version
600
+ if (projectPath !== "/" + withoutTilde) {
601
+ result = projectPath;
602
+ }
603
+ }
604
+ // Step 10: Remove leading slash if present (prefer relative paths)
605
+ if (result.startsWith("/")) {
606
+ result = result.substring(1);
607
+ }
608
+ return result;
609
+ }
610
+ /**
611
+ * Recursively unwraps Error.cause chain and returns array of chained errors.
612
+ *
613
+ * Error.cause is a standard JavaScript feature that allows chaining errors:
614
+ * const cause = new Error("Root cause");
615
+ * const error = new Error("Wrapper error", { cause });
616
+ *
617
+ * This function extracts all errors in the cause chain up to MAX_EXCEPTION_CHAIN_DEPTH.
618
+ *
619
+ * @param error - Error object to unwrap
620
+ * @returns Array of ChainedErrorData objects representing the error chain
621
+ */
622
+ function unwrapErrorCauses(error) {
623
+ const chainedErrors = [];
624
+ const seenErrors = new Set();
625
+ let currentError = error.cause;
626
+ let depth = 0;
627
+ while (currentError && depth < MAX_EXCEPTION_CHAIN_DEPTH) {
628
+ // If cause is not an Error, stringify it and stop
629
+ if (!(currentError instanceof Error)) {
630
+ chainedErrors.push({
631
+ message: stringifyNonError(currentError),
632
+ type: undefined,
633
+ });
634
+ break;
635
+ }
636
+ // Check for circular reference
637
+ if (seenErrors.has(currentError)) {
638
+ break;
639
+ }
640
+ seenErrors.add(currentError);
641
+ const chainedErrorData = {
642
+ message: currentError.message || "",
643
+ type: currentError.name || currentError.constructor?.name || "Error",
644
+ };
645
+ if (currentError.stack) {
646
+ chainedErrorData.stack = currentError.stack;
647
+ chainedErrorData.frames = parseV8StackTrace(currentError.stack);
648
+ }
649
+ chainedErrors.push(chainedErrorData);
650
+ // Move to next cause in chain
651
+ currentError = currentError.cause;
652
+ depth++;
653
+ }
654
+ return chainedErrors;
655
+ }
656
+ /**
657
+ * Detects if a value is a CallToolResult object (SDK 1.21.0+ error format).
658
+ *
659
+ * SDK 1.21.0+ converts errors to CallToolResult format:
660
+ * { content: [{ type: "text", text: "error message" }], isError: true }
661
+ *
662
+ * @param value - Value to check
663
+ * @returns True if value is a CallToolResult object
664
+ */
665
+ function isCallToolResult(value) {
666
+ return (value !== null &&
667
+ typeof value === "object" &&
668
+ "isError" in value &&
669
+ "content" in value &&
670
+ Array.isArray(value.content));
671
+ }
672
+ /**
673
+ * Extracts error information from CallToolResult objects.
674
+ *
675
+ * SDK 1.21.0+ converts errors to CallToolResult, losing original stack traces.
676
+ * This extracts the error message from the content array.
677
+ *
678
+ * @param result - CallToolResult object with error
679
+ * @param _contextStack - Optional Error object for stack context (unused, kept for compatibility)
680
+ * @returns ErrorData with extracted message (no stack trace)
681
+ */
682
+ function captureCallToolResultError(result, _contextStack) {
683
+ // Extract message from content array
684
+ const message = result.content
685
+ ?.filter((c) => c.type === "text")
686
+ .map((c) => c.text)
687
+ .join(" ")
688
+ .trim() || "Unknown error";
689
+ const errorData = {
690
+ message,
691
+ type: undefined, // Can't determine actual type from CallToolResult
692
+ platform: "javascript",
693
+ // No stack or frames - SDK stripped the original error information
694
+ };
695
+ return errorData;
696
+ }
697
+ /**
698
+ * Converts non-Error objects to string representation for error messages.
699
+ *
700
+ * In JavaScript, anything can be thrown (not just Error objects):
701
+ * throw "string error";
702
+ * throw { code: 404 };
703
+ * throw null;
704
+ *
705
+ * This function handles these cases by converting them to meaningful strings.
706
+ *
707
+ * @param value - Non-Error value that was thrown
708
+ * @returns String representation of the value
709
+ */
710
+ function stringifyNonError(value) {
711
+ if (value === null) {
712
+ return "null";
713
+ }
714
+ if (value === undefined) {
715
+ return "undefined";
716
+ }
717
+ if (typeof value === "string") {
718
+ return value;
719
+ }
720
+ if (typeof value === "number" || typeof value === "boolean") {
721
+ return String(value);
722
+ }
723
+ // Try to stringify objects with fallback
724
+ try {
725
+ return JSON.stringify(value);
726
+ }
727
+ catch {
728
+ return String(value);
729
+ }
730
+ }