@bbradar/mcp 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -29,6 +29,8 @@ const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
29
29
  const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
30
30
  const MAX_LOCAL_EXPORT_RESOURCES = 25;
31
31
  const EXPORT_PREVIEW_LIMIT = 25;
32
+ const STALE_PROGRAM_ID_RESOLVE_PAGES = 5;
33
+ const STALE_PROGRAM_ID_RESOLVE_BUDGET = 5;
32
34
  const SDK_VERSION = "1.29.0";
33
35
  const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
34
36
  const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
@@ -124,8 +126,11 @@ const apiEnvelopeOutputShape = {
124
126
  upstream_request_id: z.string().optional(),
125
127
  fetched_at: z.string().optional(),
126
128
  cache: cacheOutputSchema.optional(),
129
+ source_requests: z.array(jsonRecordSchema).optional(),
127
130
  mcp_rate_limit: rateLimitOutputSchema.optional(),
128
131
  mcp_timing: toolTimingOutputSchema.optional(),
132
+ warnings: z.array(z.string()).optional(),
133
+ program_id_resolution: jsonRecordSchema.optional(),
129
134
  error: errorOutputSchema.optional()
130
135
  };
131
136
  const programListOutputShape = {
@@ -229,9 +234,11 @@ export function createBbradarServer(client, config) {
229
234
  },
230
235
  annotations: readOnlyAnnotations
231
236
  }, (args) => runTool("get_program", config, rateLimiter, async () => {
232
- const api = await client.getProgram(args.program_id);
233
- return withApiMetadata(api, {
234
- program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
237
+ return runProgramIdTool(client, config, args.program_id, async (programId) => {
238
+ const api = await client.getProgram(programId);
239
+ return withApiMetadata(api, {
240
+ program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
241
+ });
235
242
  });
236
243
  }));
237
244
  server.registerTool("resolve_program", {
@@ -312,23 +319,25 @@ export function createBbradarServer(client, config) {
312
319
  },
313
320
  annotations: readOnlyAnnotations
314
321
  }, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
315
- const api = await client.getProgramTargets(args.program_id);
316
- const data = readObject(api.data);
317
- const rawTargets = readArray(data?.targets);
318
- const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, args));
319
- const targets = sanitizedTargets.slice(args.offset, args.offset + args.limit);
320
- return withApiMetadata(api, {
321
- program_id: args.program_id,
322
- targets: formatTargetList(targets, args.output_mode),
323
- meta: {
324
- offset: args.offset,
325
- limit: args.limit,
326
- output_mode: args.output_mode,
327
- total_active_targets: rawTargets.length,
328
- total_after_filters: sanitizedTargets.length,
329
- returned: targets.length,
330
- has_more: args.offset + args.limit < sanitizedTargets.length
331
- }
322
+ return runProgramIdTool(client, config, args.program_id, async (programId) => {
323
+ const api = await client.getProgramTargets(programId);
324
+ const data = readObject(api.data);
325
+ const rawTargets = readArray(data?.targets);
326
+ const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, args));
327
+ const targets = sanitizedTargets.slice(args.offset, args.offset + args.limit);
328
+ return withApiMetadata(api, {
329
+ program_id: programId,
330
+ targets: formatTargetList(targets, args.output_mode),
331
+ meta: {
332
+ offset: args.offset,
333
+ limit: args.limit,
334
+ output_mode: args.output_mode,
335
+ total_active_targets: rawTargets.length,
336
+ total_after_filters: sanitizedTargets.length,
337
+ returned: targets.length,
338
+ has_more: args.offset + args.limit < sanitizedTargets.length
339
+ }
340
+ });
332
341
  });
333
342
  }));
334
343
  server.registerTool("get_recent_changes", {
@@ -414,7 +423,7 @@ export function createBbradarServer(client, config) {
414
423
  meta: jsonRecordSchema.optional()
415
424
  },
416
425
  annotations: readOnlyAnnotations
417
- }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => getProgramScopeSummary(client, config, args)));
426
+ }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeSummary(client, config, { ...args, program_id: programId }))));
418
427
  server.registerTool("get_program_target_breakdown", {
419
428
  title: "Get Program Target Breakdown",
420
429
  description: "Target type, scope tag, language tag, and bucket counts for one program.",
@@ -433,7 +442,7 @@ export function createBbradarServer(client, config) {
433
442
  meta: jsonRecordSchema.optional()
434
443
  },
435
444
  annotations: readOnlyAnnotations
436
- }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => getProgramTargetBreakdown(client, config, args)));
445
+ }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramTargetBreakdown(client, config, { ...args, program_id: programId }))));
437
446
  server.registerTool("get_program_scope_delta", {
438
447
  title: "Get Program Scope Delta",
439
448
  description: "Compact recent scope changes for one program.",
@@ -459,7 +468,7 @@ export function createBbradarServer(client, config) {
459
468
  meta: jsonRecordSchema.optional()
460
469
  },
461
470
  annotations: readOnlyAnnotations
462
- }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => getProgramScopeDelta(client, config, args)));
471
+ }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeDelta(client, config, { ...args, program_id: programId }))));
463
472
  server.registerTool("get_recent_target_activity", {
464
473
  title: "Get Recent Target Activity",
465
474
  description: "Recent target changes grouped by program.",
@@ -1155,7 +1164,7 @@ export function createBbradarServer(client, config) {
1155
1164
  changes: z.array(jsonRecordSchema).optional()
1156
1165
  },
1157
1166
  annotations: readOnlyAnnotations
1158
- }, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => summarizeProgramActivity(client, config, args.program_id, args.recent_changes_limit)));
1167
+ }, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => summarizeProgramActivity(client, config, programId, args.recent_changes_limit))));
1159
1168
  server.registerTool("get_program_brief", {
1160
1169
  title: "Get Program Brief",
1161
1170
  description: "Compact worth-hunting brief for one program.",
@@ -1178,7 +1187,7 @@ export function createBbradarServer(client, config) {
1178
1187
  meta: jsonRecordSchema.optional()
1179
1188
  },
1180
1189
  annotations: readOnlyAnnotations
1181
- }, (args) => runTool("get_program_brief", config, rateLimiter, async () => getProgramBrief(client, config, args)));
1190
+ }, (args) => runTool("get_program_brief", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramBrief(client, config, { ...args, program_id: programId }))));
1182
1191
  server.registerTool("find_recently_added_wildcards", {
1183
1192
  title: "Find Recently Added Wildcards",
1184
1193
  description: "Recently added wildcard programs.",
@@ -1307,7 +1316,7 @@ export function createBbradarServer(client, config) {
1307
1316
  meta: jsonRecordSchema.optional()
1308
1317
  },
1309
1318
  annotations: readOnlyAnnotations
1310
- }, (args) => runTool("get_program_delta", config, rateLimiter, async () => getProgramDelta(client, config, args)));
1319
+ }, (args) => runTool("get_program_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramDelta(client, config, { ...args, program_id: programId }))));
1311
1320
  server.registerTool("export_targets", {
1312
1321
  title: "Export BBRadar Targets",
1313
1322
  description: `Read-only target export. limit max ${MAX_TARGET_EXPORT}.`,
@@ -1374,9 +1383,11 @@ function registerResources(server, client, config, exportStore) {
1374
1383
  mimeType: "application/json"
1375
1384
  }, async (uri) => {
1376
1385
  const programId = parseProgramIdFromResourceUri(uri);
1377
- const api = await client.getProgram(programId);
1378
- const payload = withApiMetadata(api, {
1379
- program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
1386
+ const payload = await runProgramIdTool(client, config, programId, async (resolvedProgramId) => {
1387
+ const api = await client.getProgram(resolvedProgramId);
1388
+ return withApiMetadata(api, {
1389
+ program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
1390
+ });
1380
1391
  });
1381
1392
  return jsonResource(uri.toString(), payload);
1382
1393
  });
@@ -1386,15 +1397,17 @@ function registerResources(server, client, config, exportStore) {
1386
1397
  mimeType: "application/json"
1387
1398
  }, async (uri) => {
1388
1399
  const programId = parseProgramIdFromResourceUri(uri);
1389
- const api = await client.getProgramTargets(programId);
1390
- const data = readObject(api.data);
1391
- const targets = readArray(data?.targets).map(sanitizeTarget);
1392
- const payload = withApiMetadata(api, {
1393
- program_id: programId,
1394
- targets,
1395
- meta: {
1396
- total_active_targets: targets.length
1397
- }
1400
+ const payload = await runProgramIdTool(client, config, programId, async (resolvedProgramId) => {
1401
+ const api = await client.getProgramTargets(resolvedProgramId);
1402
+ const data = readObject(api.data);
1403
+ const targets = readArray(data?.targets).map(sanitizeTarget);
1404
+ return withApiMetadata(api, {
1405
+ program_id: resolvedProgramId,
1406
+ targets,
1407
+ meta: {
1408
+ total_active_targets: targets.length
1409
+ }
1410
+ });
1398
1411
  });
1399
1412
  return jsonResource(uri.toString(), payload);
1400
1413
  });
@@ -1433,6 +1446,136 @@ function registerResources(server, client, config, exportStore) {
1433
1446
  }));
1434
1447
  });
1435
1448
  }
1449
+ async function getProgramWithIdFallback(client, config, requestedProgramId) {
1450
+ try {
1451
+ return {
1452
+ api: await client.getProgram(requestedProgramId),
1453
+ programId: requestedProgramId,
1454
+ sourceRequests: [],
1455
+ warnings: []
1456
+ };
1457
+ }
1458
+ catch (error) {
1459
+ if (!isProgramNotFoundError(error)) {
1460
+ throw error;
1461
+ }
1462
+ const fallback = await resolveStaleProgramId(client, config, requestedProgramId, error);
1463
+ if (!fallback) {
1464
+ throw error;
1465
+ }
1466
+ return {
1467
+ api: await client.getProgram(fallback.resolvedProgramId),
1468
+ programId: fallback.resolvedProgramId,
1469
+ sourceRequests: fallback.sourceRequests,
1470
+ warnings: fallback.warnings,
1471
+ fallback
1472
+ };
1473
+ }
1474
+ }
1475
+ async function runProgramIdTool(client, config, requestedProgramId, callback) {
1476
+ try {
1477
+ return await callback(requestedProgramId);
1478
+ }
1479
+ catch (error) {
1480
+ if (!isProgramNotFoundError(error)) {
1481
+ throw error;
1482
+ }
1483
+ const fallback = await resolveStaleProgramId(client, config, requestedProgramId, error);
1484
+ if (!fallback) {
1485
+ throw error;
1486
+ }
1487
+ const payload = await callback(fallback.resolvedProgramId);
1488
+ return withProgramIdFallbackMetadata(payload, fallback);
1489
+ }
1490
+ }
1491
+ async function resolveStaleProgramId(client, config, requestedProgramId, staleError) {
1492
+ const query = programSearchText(requestedProgramId);
1493
+ if (query.length < 2) {
1494
+ return undefined;
1495
+ }
1496
+ const resolution = await resolveProgram(client, config, {
1497
+ query,
1498
+ platforms: [],
1499
+ tags: [],
1500
+ opportunity_levels: [],
1501
+ max_pages: STALE_PROGRAM_ID_RESOLVE_PAGES,
1502
+ max_results: 5,
1503
+ output_mode: "compact",
1504
+ upstream_request_budget: STALE_PROGRAM_ID_RESOLVE_BUDGET
1505
+ });
1506
+ const match = selectProgramIdFallbackMatch(requestedProgramId, resolution);
1507
+ if (!match) {
1508
+ return undefined;
1509
+ }
1510
+ const sourceRequests = readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined);
1511
+ const warnings = readArray(resolution.warnings).filter((warning) => typeof warning === "string");
1512
+ const staleApiError = staleError instanceof BBRadarApiError ? staleError : undefined;
1513
+ const fallbackWarning = `program_id ${requestedProgramId} was stale; automatically retried with resolved program_id ${match.resolvedProgramId}.`;
1514
+ return {
1515
+ requestedProgramId,
1516
+ resolvedProgramId: match.resolvedProgramId,
1517
+ query,
1518
+ staleRequestId: staleApiError?.requestId,
1519
+ staleUpstreamRequestId: staleApiError?.upstreamRequestId,
1520
+ sourceRequests,
1521
+ warnings: uniqueStrings([...warnings, fallbackWarning]),
1522
+ resolution
1523
+ };
1524
+ }
1525
+ function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1526
+ const requestedHandle = normalizeTag(programHandleText(requestedProgramId));
1527
+ const requestedSearchText = normalizeTag(programSearchText(requestedProgramId));
1528
+ const candidates = readArray(resolution.matches)
1529
+ .map((entry) => readObject(entry))
1530
+ .filter((entry) => entry !== undefined)
1531
+ .map((entry) => {
1532
+ const program = readObject(entry.program);
1533
+ const resolvedProgramId = stringField(entry, "program_id") ?? stringField(program, "id");
1534
+ const candidateHandle = normalizeTag(stringField(program, "handle") ?? "");
1535
+ const candidateSearchText = resolvedProgramId ? normalizeTag(programSearchText(resolvedProgramId)) : "";
1536
+ const score = readNumber(entry.score) ?? 0;
1537
+ const exactHandleMatch = candidateHandle === requestedHandle ||
1538
+ candidateHandle === requestedSearchText ||
1539
+ candidateSearchText === requestedSearchText;
1540
+ return {
1541
+ resolvedProgramId,
1542
+ score,
1543
+ exactHandleMatch
1544
+ };
1545
+ })
1546
+ .filter((entry) => entry.resolvedProgramId !== undefined && entry.resolvedProgramId !== requestedProgramId && entry.exactHandleMatch && entry.score >= 80)
1547
+ .sort((left, right) => right.score - left.score || left.resolvedProgramId.localeCompare(right.resolvedProgramId));
1548
+ if (candidates.length === 0) {
1549
+ return undefined;
1550
+ }
1551
+ const [best, next] = candidates;
1552
+ if (!best || (next && best.score === next.score)) {
1553
+ return undefined;
1554
+ }
1555
+ return {
1556
+ resolvedProgramId: best.resolvedProgramId
1557
+ };
1558
+ }
1559
+ function withProgramIdFallbackMetadata(payload, fallback) {
1560
+ const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
1561
+ const warnings = readArray(payload.warnings).filter((warning) => typeof warning === "string");
1562
+ const resolution = stripUndefined({
1563
+ requested_program_id: fallback.requestedProgramId,
1564
+ resolved_program_id: fallback.resolvedProgramId,
1565
+ query: fallback.query,
1566
+ reason: "stale_program_id",
1567
+ stale_status: 404,
1568
+ stale_request_id: fallback.staleRequestId,
1569
+ stale_upstream_request_id: fallback.staleUpstreamRequestId,
1570
+ resolution_request_id: stringField(fallback.resolution, "request_id")
1571
+ });
1572
+ return stripUndefined({
1573
+ ...payload,
1574
+ source_requests: [...fallback.sourceRequests, ...sourceRequests],
1575
+ warnings: uniqueStrings([...fallback.warnings, ...warnings]),
1576
+ program_id_resolution: resolution
1577
+ });
1578
+ }
1436
1579
  async function resolveProgram(client, config, input) {
1437
1580
  const warnings = [];
1438
1581
  const budget = {
@@ -2268,17 +2411,22 @@ async function comparePrograms(client, config, input) {
2268
2411
  const candidates = [];
2269
2412
  const sourceRequests = [];
2270
2413
  const errors = [];
2414
+ const warnings = [];
2271
2415
  await mapWithConcurrency(input.program_ids, TARGET_SAMPLE_CONCURRENCY, async (programId) => {
2272
2416
  try {
2273
- const api = await client.getProgram(programId);
2274
- candidates.push(toProgramCandidate(api.data, config.webBaseUrl));
2275
- sourceRequests.push({
2417
+ const lookup = await getProgramWithIdFallback(client, config, programId);
2418
+ const api = lookup.api;
2419
+ const requestSource = stripUndefined({
2276
2420
  source: "program",
2277
- program_id: programId,
2421
+ program_id: lookup.programId,
2422
+ requested_program_id: lookup.programId === programId ? undefined : programId,
2278
2423
  request_id: api.requestId,
2279
2424
  upstream_request_id: api.upstreamRequestId,
2280
2425
  ...apiSourceMetadata(api)
2281
2426
  });
2427
+ candidates.push(toProgramCandidate(api.data, config.webBaseUrl));
2428
+ sourceRequests.push(...lookup.sourceRequests, requestSource);
2429
+ warnings.push(...lookup.warnings);
2282
2430
  }
2283
2431
  catch (error) {
2284
2432
  errors.push(programFetchError(programId, error));
@@ -2333,6 +2481,7 @@ async function comparePrograms(client, config, input) {
2333
2481
  partial_success: errors.length > 0,
2334
2482
  program_ids_requested: input.program_ids,
2335
2483
  failed_program_ids: errors.map((error) => error.program_id).filter((id) => typeof id === "string"),
2484
+ warnings: warnings.length > 0 ? uniqueStrings(warnings) : undefined,
2336
2485
  errors: errors.length > 0 ? errors : undefined,
2337
2486
  source_requests: sourceRequests,
2338
2487
  compared_programs: candidates.map((candidate, index) => candidateOutput(candidate, index, targetSamples, input.output_mode ?? "compact")),
@@ -2673,13 +2822,15 @@ async function getRecentTargetActivity(client, config, input) {
2673
2822
  };
2674
2823
  }
2675
2824
  async function checkProgramNewTargets(client, config, input) {
2676
- const handle = programSearchText(input.program_id);
2825
+ const programLookup = await getProgramWithIdFallback(client, config, input.program_id);
2826
+ const programId = programLookup.programId;
2827
+ const handle = programSearchText(programId);
2677
2828
  const api = await client.getRecentChanges({
2678
2829
  change_type: "added",
2679
2830
  include_removed: false,
2680
2831
  include_ineligible: input.include_ineligible,
2681
2832
  include_out_of_scope: false,
2682
- search: handle.length >= 2 ? handle : input.program_id,
2833
+ search: handle.length >= 2 ? handle : programId,
2683
2834
  tags: input.target_type ? [input.target_type] : [],
2684
2835
  page: 1,
2685
2836
  page_size: input.page_size
@@ -2693,7 +2844,7 @@ async function checkProgramNewTargets(client, config, input) {
2693
2844
  const target = readObject(change.target);
2694
2845
  const changedAt = timestampField(change, "changed_at");
2695
2846
  return (stringField(change, "change_type") === "added" &&
2696
- stringField(readObject(change.program), "id") === input.program_id &&
2847
+ stringField(readObject(change.program), "id") === programId &&
2697
2848
  target !== undefined &&
2698
2849
  (sinceTimestamp === undefined || changedAt >= sinceTimestamp) &&
2699
2850
  (!input.target_type || targetMatchesRequestedType(target, input.target_type)) &&
@@ -2709,18 +2860,30 @@ async function checkProgramNewTargets(client, config, input) {
2709
2860
  .map((change) => readObject(change.target))
2710
2861
  .filter((target) => target !== undefined);
2711
2862
  const latest = matchingChanges[0];
2712
- return stripUndefined({
2863
+ const sourceRequests = [
2864
+ {
2865
+ source: "program",
2866
+ program_id: programId,
2867
+ requested_program_id: programId === input.program_id ? undefined : input.program_id,
2868
+ request_id: programLookup.api.requestId,
2869
+ upstream_request_id: programLookup.api.upstreamRequestId,
2870
+ ...apiSourceMetadata(programLookup.api)
2871
+ },
2872
+ {
2873
+ source: "recent_changes",
2874
+ request_id: api.requestId,
2875
+ upstream_request_id: api.upstreamRequestId,
2876
+ ...apiSourceMetadata(api)
2877
+ }
2878
+ ];
2879
+ const payload = stripUndefined({
2713
2880
  request_id: randomUUID(),
2714
- source_requests: [
2715
- {
2716
- source: "recent_changes",
2717
- request_id: api.requestId,
2718
- upstream_request_id: api.upstreamRequestId,
2719
- ...apiSourceMetadata(api)
2720
- }
2721
- ],
2722
- program_id: input.program_id,
2723
- program: latest ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact") : undefined,
2881
+ source_requests: sourceRequests,
2882
+ warnings: !programLookup.fallback && programLookup.warnings.length > 0 ? uniqueStrings(programLookup.warnings) : undefined,
2883
+ program_id: programId,
2884
+ program: latest
2885
+ ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact")
2886
+ : formatProgram(addProgramResourceLinks(sanitizeProgram(programLookup.api.data, config.webBaseUrl)), "compact"),
2724
2887
  has_new_targets: matchingChanges.length > 0,
2725
2888
  new_target_count: matchingChanges.length,
2726
2889
  latest_added_at: latest ? stringField(latest, "changed_at") : undefined,
@@ -2738,6 +2901,7 @@ async function checkProgramNewTargets(client, config, input) {
2738
2901
  scan_may_be_incomplete: (readNumber(upstreamMeta?.total_pages) ?? 1) > 1
2739
2902
  })
2740
2903
  });
2904
+ return programLookup.fallback ? withProgramIdFallbackMetadata(payload, programLookup.fallback) : payload;
2741
2905
  }
2742
2906
  async function checkWatchlistNewTargets(client, config, input) {
2743
2907
  const watchlist = new Set(input.program_ids);
@@ -3504,6 +3668,10 @@ function programSearchText(programId) {
3504
3668
  const lastPathSegment = handle.split("/").filter(Boolean).at(-1) ?? handle;
3505
3669
  return lastPathSegment.length >= 2 ? lastPathSegment : handle;
3506
3670
  }
3671
+ function programHandleText(programId) {
3672
+ const colonIndex = programId.indexOf(":");
3673
+ return colonIndex >= 0 ? programId.slice(colonIndex + 1) : programId;
3674
+ }
3507
3675
  function normalizeFindProgramsInput(input) {
3508
3676
  const warnings = [];
3509
3677
  const normalized = { ...input };
@@ -3984,6 +4152,9 @@ function addUniqueWarning(warnings, warning) {
3984
4152
  warnings.push(warning);
3985
4153
  }
3986
4154
  }
4155
+ function isProgramNotFoundError(error) {
4156
+ return error instanceof BBRadarApiError && error.status === 404;
4157
+ }
3987
4158
  function runImmediately(callback) {
3988
4159
  return callback();
3989
4160
  }