@bbradar/mcp 0.1.4 → 0.1.5

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
@@ -31,17 +31,51 @@ const MAX_LOCAL_EXPORT_RESOURCES = 25;
31
31
  const EXPORT_PREVIEW_LIMIT = 25;
32
32
  const STALE_PROGRAM_ID_RESOLVE_PAGES = 5;
33
33
  const STALE_PROGRAM_ID_RESOLVE_BUDGET = 5;
34
+ const MAX_RESOLVE_SEARCH_QUERIES = 4;
34
35
  const SDK_VERSION = "1.29.0";
35
36
  const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
36
37
  const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
37
38
  const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
39
+ const PROGRAM_RESOLUTION_STOP_WORDS = new Set([
40
+ "program",
41
+ "programs",
42
+ "target",
43
+ "targets",
44
+ "scope",
45
+ "scopes",
46
+ "new",
47
+ "latest",
48
+ "recent",
49
+ "recently",
50
+ "added",
51
+ "additions",
52
+ "for",
53
+ "from",
54
+ "the",
55
+ "a",
56
+ "an",
57
+ "of",
58
+ "on",
59
+ "in",
60
+ "with",
61
+ "to",
62
+ "get",
63
+ "find",
64
+ "show",
65
+ "list",
66
+ "check",
67
+ "does",
68
+ "has",
69
+ "have",
70
+ "any"
71
+ ]);
38
72
  const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
39
73
  const changeListModeSchema = z.enum(["compact", "full"]);
40
74
  const programListModeSchema = z.enum(["compact", "full"]);
41
75
  const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
42
76
  const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
43
77
  const targetMatchModeSchema = z.enum(["contains", "exact", "suffix", "wildcard"]);
44
- const programNameActionSchema = z.enum(["new_targets", "scope_delta", "brief", "target_breakdown"]);
78
+ const programNameActionSchema = z.enum(["new_targets", "targets", "scope_delta", "brief", "target_breakdown"]);
45
79
  const readOnlyAnnotations = {
46
80
  readOnlyHint: true,
47
81
  destructiveHint: false,
@@ -192,7 +226,8 @@ export function createBbradarServer(client, config) {
192
226
  instructions: [
193
227
  "Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
194
228
  "For BBRadar asks, use these tools first and avoid external skills unless methodology is explicitly requested.",
195
- "Name + action: run_program_name_action. Name only: resolve_program.",
229
+ "Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
230
+ "Default to bounty-eligible in-scope data. Include out-of-scope, ineligible, VDP, disclosure-only, or no-bounty data only when the user explicitly asks.",
196
231
  "Freshness: get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
197
232
  "Discovery: find_program_candidates, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, find_low_noise_programs.",
198
233
  "Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
@@ -208,15 +243,20 @@ export function createBbradarServer(client, config) {
208
243
  platforms: stringListSchema,
209
244
  tags: stringListSchema,
210
245
  updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
246
+ vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
211
247
  page: pageSchema,
212
248
  page_size: programsPageSizeSchema
213
249
  },
214
250
  outputSchema: programListOutputShape,
215
251
  annotations: readOnlyAnnotations
216
252
  }, (args) => runTool("search_programs", config, rateLimiter, async () => {
217
- const api = await client.listPrograms(toQuery(args));
253
+ const { vdp_mode, ...filters } = args;
254
+ const api = await client.listPrograms(toQuery(filters));
218
255
  const data = readObject(api.data);
219
- const programs = readArray(data?.programs).map((program) => addProgramResourceLinks(sanitizeProgram(program, config.webBaseUrl)));
256
+ const programs = readArray(data?.programs)
257
+ .map((program) => toProgramCandidate(program, config.webBaseUrl))
258
+ .filter((candidate) => candidateMatchesVdpMode(candidate, vdp_mode))
259
+ .map((candidate) => candidate.program);
220
260
  return withApiMetadata(api, {
221
261
  programs,
222
262
  meta: sanitizeJson(data?.meta)
@@ -230,7 +270,8 @@ export function createBbradarServer(client, config) {
230
270
  },
231
271
  outputSchema: {
232
272
  ...apiEnvelopeOutputShape,
233
- program: programOutputSchema.optional()
273
+ program: programOutputSchema.optional(),
274
+ meta: jsonRecordSchema.optional()
234
275
  },
235
276
  annotations: readOnlyAnnotations
236
277
  }, (args) => runTool("get_program", config, rateLimiter, async () => {
@@ -239,11 +280,19 @@ export function createBbradarServer(client, config) {
239
280
  return withApiMetadata(api, {
240
281
  program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
241
282
  });
242
- });
283
+ }, async (indexFallback) => ({
284
+ request_id: randomUUID(),
285
+ warnings: [`Returned program metadata from the BBRadar program index because the per-program detail endpoint returned 404 for ${indexFallback.resolvedProgramId}.`],
286
+ program: indexFallback.program,
287
+ meta: {
288
+ detail_source: "program_index",
289
+ detail_endpoint_unavailable: true
290
+ }
291
+ }));
243
292
  }));
244
293
  server.registerTool("resolve_program", {
245
294
  title: "Resolve Program",
246
- description: "Resolve name/handle text to likely program_ids.",
295
+ description: "Resolve partial name/handle text to likely program_ids. Use this when the user gives a program name fragment like Nado.",
247
296
  inputSchema: {
248
297
  query: searchTextSchema,
249
298
  platforms: stringListSchema,
@@ -267,7 +316,7 @@ export function createBbradarServer(client, config) {
267
316
  }, (args) => runTool("resolve_program", config, rateLimiter, async () => resolveProgram(client, config, args)));
268
317
  server.registerTool("run_program_name_action", {
269
318
  title: "Run Program Name Action",
270
- description: "Resolve a name, then run new_targets, scope_delta, brief, or target_breakdown.",
319
+ description: "Resolve a partial program name, then run targets, new_targets, scope_delta, brief, or target_breakdown.",
271
320
  inputSchema: {
272
321
  query: searchTextSchema,
273
322
  action: programNameActionSchema.default("new_targets"),
@@ -319,26 +368,7 @@ export function createBbradarServer(client, config) {
319
368
  },
320
369
  annotations: readOnlyAnnotations
321
370
  }, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
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
- });
341
- });
371
+ return runProgramIdTool(client, config, args.program_id, async (programId) => getProgramTargetsPayload(client, args, programId), async (indexFallback) => getProgramTargetsExportFallbackPayload(client, args, indexFallback.resolvedProgramId));
342
372
  }));
343
373
  server.registerTool("get_recent_changes", {
344
374
  title: "Get Recent Target Changes",
@@ -390,8 +420,8 @@ export function createBbradarServer(client, config) {
390
420
  include_full_target_list: z.boolean().default(true),
391
421
  full_target_list_mode: targetListModeSchema.default("identifiers"),
392
422
  full_target_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
393
- full_list_include_out_of_scope: z.boolean().default(true),
394
- full_list_include_ineligible: z.boolean().default(true)
423
+ full_list_include_out_of_scope: z.boolean().default(false),
424
+ full_list_include_ineligible: z.boolean().default(false)
395
425
  },
396
426
  outputSchema: {
397
427
  ...apiEnvelopeOutputShape,
@@ -412,8 +442,8 @@ export function createBbradarServer(client, config) {
412
442
  program_id: programIdSchema,
413
443
  target_list_mode: targetListModeSchema.default("identifiers"),
414
444
  group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
415
- include_out_of_scope: z.boolean().default(true),
416
- include_ineligible: z.boolean().default(true)
445
+ include_out_of_scope: z.boolean().default(false),
446
+ include_ineligible: z.boolean().default(false)
417
447
  },
418
448
  outputSchema: {
419
449
  ...apiEnvelopeOutputShape,
@@ -423,7 +453,7 @@ export function createBbradarServer(client, config) {
423
453
  meta: jsonRecordSchema.optional()
424
454
  },
425
455
  annotations: readOnlyAnnotations
426
- }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeSummary(client, config, { ...args, program_id: programId }))));
456
+ }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeSummary(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramScopeSummaryFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
427
457
  server.registerTool("get_program_target_breakdown", {
428
458
  title: "Get Program Target Breakdown",
429
459
  description: "Target type, scope tag, language tag, and bucket counts for one program.",
@@ -431,8 +461,8 @@ export function createBbradarServer(client, config) {
431
461
  program_id: programIdSchema,
432
462
  target_list_mode: targetListModeSchema.default("identifiers"),
433
463
  group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
434
- include_out_of_scope: z.boolean().default(true),
435
- include_ineligible: z.boolean().default(true)
464
+ include_out_of_scope: z.boolean().default(false),
465
+ include_ineligible: z.boolean().default(false)
436
466
  },
437
467
  outputSchema: {
438
468
  ...apiEnvelopeOutputShape,
@@ -442,7 +472,7 @@ export function createBbradarServer(client, config) {
442
472
  meta: jsonRecordSchema.optional()
443
473
  },
444
474
  annotations: readOnlyAnnotations
445
- }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramTargetBreakdown(client, config, { ...args, program_id: programId }))));
475
+ }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramTargetBreakdown(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramTargetBreakdownFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
446
476
  server.registerTool("get_program_scope_delta", {
447
477
  title: "Get Program Scope Delta",
448
478
  description: "Compact recent scope changes for one program.",
@@ -519,7 +549,7 @@ export function createBbradarServer(client, config) {
519
549
  meta: jsonRecordSchema.optional()
520
550
  },
521
551
  annotations: readOnlyAnnotations
522
- }, (args) => runTool("check_program_new_targets", config, rateLimiter, async () => checkProgramNewTargets(client, config, args)));
552
+ }, (args) => runTool("check_program_new_targets", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => checkProgramNewTargets(client, config, { ...args, program_id: programId }), (indexFallback) => checkProgramNewTargetsFromIndexedProgram(client, config, { ...args, program_id: indexFallback.resolvedProgramId }, indexFallback.program))));
523
553
  server.registerTool("check_watchlist_new_targets", {
524
554
  title: "Check Watchlist New Targets",
525
555
  description: "New in-scope targets across many known program_ids.",
@@ -549,6 +579,7 @@ export function createBbradarServer(client, config) {
549
579
  platforms: stringListSchema,
550
580
  tags: stringListSchema,
551
581
  updated_since: isoDateTimeSchema.optional(),
582
+ vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
552
583
  page: pageSchema,
553
584
  page_size: programsPageSizeSchema
554
585
  },
@@ -558,10 +589,13 @@ export function createBbradarServer(client, config) {
558
589
  },
559
590
  annotations: readOnlyAnnotations
560
591
  }, (args) => runTool("get_opportunities", config, rateLimiter, async () => {
561
- const { level, ...filters } = args;
592
+ const { level, vdp_mode, ...filters } = args;
562
593
  const api = await client.getOpportunities(level, toQuery(filters));
563
594
  const data = readObject(api.data);
564
- const programs = readArray(data?.programs).map((program) => addProgramResourceLinks(sanitizeProgram(program, config.webBaseUrl)));
595
+ const programs = readArray(data?.programs)
596
+ .map((program) => toProgramCandidate(program, config.webBaseUrl))
597
+ .filter((candidate) => candidateMatchesVdpMode(candidate, vdp_mode))
598
+ .map((candidate) => candidate.program);
565
599
  return withApiMetadata(api, {
566
600
  level,
567
601
  programs,
@@ -592,7 +626,7 @@ export function createBbradarServer(client, config) {
592
626
  exclude_non_web: z.boolean().default(false),
593
627
  has_api_scope: z.boolean().default(false),
594
628
  contest_like: z.boolean().default(false).describe("Require contest/audit signals."),
595
- vdp_mode: vdpModeSchema.default("include").describe("VDP/no-bounty filter mode."),
629
+ vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
596
630
  fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
597
631
  min_wildcards: z.number().int().min(0).max(1000).default(0),
598
632
  min_eligible_targets: z.number().int().min(0).optional(),
@@ -800,7 +834,7 @@ export function createBbradarServer(client, config) {
800
834
  min_reward: z.number().int().min(0).optional(),
801
835
  reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
802
836
  require_bounty: z.boolean().default(false),
803
- exclude_vdp: z.boolean().default(false),
837
+ exclude_vdp: z.boolean().default(true),
804
838
  max_public_report_count: z.number().int().min(0).optional(),
805
839
  require_known_report_count: z.boolean().default(false),
806
840
  include_unknown_report_count: z.boolean().default(true),
@@ -830,7 +864,7 @@ export function createBbradarServer(client, config) {
830
864
  require_bounty: z.boolean().default(false),
831
865
  fresh_only: z.boolean().default(false),
832
866
  min_added_7d: z.number().int().min(0).optional(),
833
- exclude_vdp: z.boolean().default(false),
867
+ exclude_vdp: z.boolean().default(true),
834
868
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
835
869
  max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
836
870
  include_target_samples: z.boolean().default(false),
@@ -899,7 +933,7 @@ export function createBbradarServer(client, config) {
899
933
  web3: z.boolean().default(false),
900
934
  contest_like: z.boolean().default(false),
901
935
  require_bounty: z.boolean().default(false),
902
- exclude_vdp: z.boolean().default(false),
936
+ exclude_vdp: z.boolean().default(true),
903
937
  max_public_report_count: z.number().int().min(0).optional(),
904
938
  min_eligible_targets: z.number().int().min(0).optional(),
905
939
  fresh_only: z.boolean().default(false),
@@ -978,8 +1012,8 @@ export function createBbradarServer(client, config) {
978
1012
  use_api_search: z.boolean().default(true),
979
1013
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(2),
980
1014
  max_results: z.number().int().min(1).max(25).default(10),
981
- include_out_of_scope: z.boolean().default(true),
982
- include_ineligible: z.boolean().default(true),
1015
+ include_out_of_scope: z.boolean().default(false),
1016
+ include_ineligible: z.boolean().default(false),
983
1017
  max_targets_per_program: z.number().int().min(1).max(50).default(10),
984
1018
  target_list_mode: targetListModeSchema.default("identifiers"),
985
1019
  upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
@@ -1472,7 +1506,7 @@ async function getProgramWithIdFallback(client, config, requestedProgramId) {
1472
1506
  };
1473
1507
  }
1474
1508
  }
1475
- async function runProgramIdTool(client, config, requestedProgramId, callback) {
1509
+ async function runProgramIdTool(client, config, requestedProgramId, callback, indexFallbackCallback) {
1476
1510
  try {
1477
1511
  return await callback(requestedProgramId);
1478
1512
  }
@@ -1482,12 +1516,55 @@ async function runProgramIdTool(client, config, requestedProgramId, callback) {
1482
1516
  }
1483
1517
  const fallback = await resolveStaleProgramId(client, config, requestedProgramId, error);
1484
1518
  if (!fallback) {
1485
- throw error;
1519
+ if (!indexFallbackCallback) {
1520
+ throw error;
1521
+ }
1522
+ const indexFallback = await resolveIndexedProgramId(client, config, requestedProgramId, error);
1523
+ if (!indexFallback) {
1524
+ throw error;
1525
+ }
1526
+ const payload = await indexFallbackCallback(indexFallback);
1527
+ return withProgramIndexFallbackMetadata(payload, indexFallback);
1486
1528
  }
1487
1529
  const payload = await callback(fallback.resolvedProgramId);
1488
1530
  return withProgramIdFallbackMetadata(payload, fallback);
1489
1531
  }
1490
1532
  }
1533
+ async function resolveIndexedProgramId(client, config, requestedProgramId, staleError) {
1534
+ const query = programSearchText(requestedProgramId);
1535
+ if (query.length < 2) {
1536
+ return undefined;
1537
+ }
1538
+ const resolution = await resolveProgram(client, config, {
1539
+ query,
1540
+ platforms: [],
1541
+ tags: [],
1542
+ opportunity_levels: [],
1543
+ max_pages: STALE_PROGRAM_ID_RESOLVE_PAGES,
1544
+ max_results: 5,
1545
+ output_mode: "full",
1546
+ upstream_request_budget: STALE_PROGRAM_ID_RESOLVE_BUDGET
1547
+ });
1548
+ const match = selectProgramIndexFallbackMatch(requestedProgramId, resolution);
1549
+ if (!match) {
1550
+ return undefined;
1551
+ }
1552
+ const sourceRequests = readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined);
1553
+ const warnings = readArray(resolution.warnings).filter((warning) => typeof warning === "string");
1554
+ const staleApiError = staleError instanceof BBRadarApiError ? staleError : undefined;
1555
+ const indexWarning = `program_id ${requestedProgramId} exists in the BBRadar program index, but the per-program detail endpoint returned 404.`;
1556
+ return {
1557
+ requestedProgramId,
1558
+ resolvedProgramId: match.resolvedProgramId,
1559
+ query,
1560
+ staleRequestId: staleApiError?.requestId,
1561
+ staleUpstreamRequestId: staleApiError?.upstreamRequestId,
1562
+ sourceRequests,
1563
+ warnings: uniqueStrings([...warnings, indexWarning]),
1564
+ resolution,
1565
+ program: match.program
1566
+ };
1567
+ }
1491
1568
  async function resolveStaleProgramId(client, config, requestedProgramId, staleError) {
1492
1569
  const query = programSearchText(requestedProgramId);
1493
1570
  if (query.length < 2) {
@@ -1556,6 +1633,44 @@ function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1556
1633
  resolvedProgramId: best.resolvedProgramId
1557
1634
  };
1558
1635
  }
1636
+ function selectProgramIndexFallbackMatch(requestedProgramId, resolution) {
1637
+ const requestedId = normalizeTag(requestedProgramId);
1638
+ const requestedHandle = normalizeTag(programHandleText(requestedProgramId));
1639
+ const requestedPlatform = normalizeTag(programPlatformText(requestedProgramId));
1640
+ const candidates = readArray(resolution.matches)
1641
+ .map((entry) => readObject(entry))
1642
+ .filter((entry) => entry !== undefined)
1643
+ .map((entry) => {
1644
+ const program = readObject(entry.program);
1645
+ const resolvedProgramId = stringField(entry, "program_id") ?? stringField(program, "id");
1646
+ const candidateHandle = normalizeTag(stringField(program, "handle") ?? (resolvedProgramId ? programHandleText(resolvedProgramId) : ""));
1647
+ const candidatePlatform = normalizeTag(stringField(program, "platform") ?? (resolvedProgramId ? programPlatformText(resolvedProgramId) : ""));
1648
+ const score = readNumber(entry.score) ?? 0;
1649
+ const idMatch = resolvedProgramId !== undefined && normalizeTag(resolvedProgramId) === requestedId;
1650
+ const platformHandleMatch = requestedPlatform.length > 0 &&
1651
+ candidatePlatform === requestedPlatform &&
1652
+ candidateHandle === requestedHandle;
1653
+ return {
1654
+ resolvedProgramId,
1655
+ program,
1656
+ score,
1657
+ exactMatch: idMatch || platformHandleMatch
1658
+ };
1659
+ })
1660
+ .filter((entry) => entry.resolvedProgramId !== undefined && entry.program !== undefined && entry.exactMatch && entry.score >= 80)
1661
+ .sort((left, right) => right.score - left.score || left.resolvedProgramId.localeCompare(right.resolvedProgramId));
1662
+ if (candidates.length === 0) {
1663
+ return undefined;
1664
+ }
1665
+ const [best, next] = candidates;
1666
+ if (!best || (next && best.score === next.score && normalizeTag(next.resolvedProgramId) !== normalizeTag(best.resolvedProgramId))) {
1667
+ return undefined;
1668
+ }
1669
+ return {
1670
+ resolvedProgramId: best.resolvedProgramId,
1671
+ program: addProgramResourceLinks(best.program)
1672
+ };
1673
+ }
1559
1674
  function withProgramIdFallbackMetadata(payload, fallback) {
1560
1675
  const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
1561
1676
  const warnings = readArray(payload.warnings).filter((warning) => typeof warning === "string");
@@ -1576,6 +1691,17 @@ function withProgramIdFallbackMetadata(payload, fallback) {
1576
1691
  program_id_resolution: resolution
1577
1692
  });
1578
1693
  }
1694
+ function withProgramIndexFallbackMetadata(payload, fallback) {
1695
+ const enriched = withProgramIdFallbackMetadata(payload, fallback);
1696
+ const resolution = readObject(enriched.program_id_resolution);
1697
+ return {
1698
+ ...enriched,
1699
+ program_id_resolution: stripUndefined({
1700
+ ...(resolution ?? {}),
1701
+ reason: "program_index_detail_404"
1702
+ })
1703
+ };
1704
+ }
1579
1705
  async function resolveProgram(client, config, input) {
1580
1706
  const warnings = [];
1581
1707
  const budget = {
@@ -1583,10 +1709,11 @@ async function resolveProgram(client, config, input) {
1583
1709
  remaining: input.upstream_request_budget
1584
1710
  };
1585
1711
  const maxPages = clampLimit("max_pages", input.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
1586
- const collected = await collectPrograms(client, {
1712
+ const searchQueries = programResolutionSearchQueries(input.query);
1713
+ const collected = await collectProgramsForResolution(client, {
1714
+ searchQueries,
1587
1715
  platforms: input.platforms,
1588
1716
  tags: input.tags,
1589
- updated_since: undefined,
1590
1717
  opportunity_levels: input.opportunity_levels,
1591
1718
  max_pages: maxPages,
1592
1719
  budget,
@@ -1608,6 +1735,7 @@ async function resolveProgram(client, config, input) {
1608
1735
  warnings: warnings.length > 0 ? warnings : undefined,
1609
1736
  query: {
1610
1737
  text: input.query,
1738
+ search_queries: searchQueries,
1611
1739
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
1612
1740
  tags: input.tags.length > 0 ? input.tags : undefined
1613
1741
  },
@@ -1715,6 +1843,31 @@ async function runResolvedProgramAction(client, config, programId, input) {
1715
1843
  include_ineligible: input.include_ineligible
1716
1844
  });
1717
1845
  }
1846
+ if (input.action === "targets") {
1847
+ const api = await client.getProgramTargets(programId);
1848
+ const data = readObject(api.data);
1849
+ const rawTargets = readArray(data?.targets);
1850
+ const sanitizedTargets = rawTargets
1851
+ .map(sanitizeTarget)
1852
+ .filter((target) => targetHasAllowedScope(target, {
1853
+ include_out_of_scope: input.include_out_of_scope,
1854
+ include_ineligible: input.include_ineligible,
1855
+ strict_scope_filter: true
1856
+ }));
1857
+ const targets = sanitizedTargets.slice(0, input.max_targets);
1858
+ return withApiMetadata(api, {
1859
+ program_id: programId,
1860
+ targets: formatTargetList(targets, input.target_list_mode),
1861
+ meta: {
1862
+ limit: input.max_targets,
1863
+ output_mode: input.target_list_mode,
1864
+ total_active_targets: rawTargets.length,
1865
+ total_after_filters: sanitizedTargets.length,
1866
+ returned: targets.length,
1867
+ has_more: sanitizedTargets.length > targets.length
1868
+ }
1869
+ });
1870
+ }
1718
1871
  return checkProgramNewTargets(client, config, {
1719
1872
  program_id: programId,
1720
1873
  since: input.since,
@@ -2080,7 +2233,7 @@ function findLowNoisePrograms(client, config, input) {
2080
2233
  exclude_mobile_only: false,
2081
2234
  exclude_non_web: false,
2082
2235
  has_api_scope: false,
2083
- vdp_mode: input.require_bounty ? "exclude" : "include",
2236
+ vdp_mode: "exclude",
2084
2237
  fresh_launch_days: undefined,
2085
2238
  min_wildcards: 0,
2086
2239
  min_eligible_targets: input.min_eligible_targets,
@@ -2616,11 +2769,132 @@ async function getLatestAddedTargets(client, config, input) {
2616
2769
  })
2617
2770
  });
2618
2771
  }
2772
+ async function getProgramTargetsPayload(client, input, programId) {
2773
+ const api = await client.getProgramTargets(programId);
2774
+ const data = readObject(api.data);
2775
+ const rawTargets = readArray(data?.targets);
2776
+ const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
2777
+ const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
2778
+ return withApiMetadata(api, {
2779
+ program_id: programId,
2780
+ targets: formatTargetList(targets, input.output_mode),
2781
+ meta: {
2782
+ offset: input.offset,
2783
+ limit: input.limit,
2784
+ output_mode: input.output_mode,
2785
+ total_active_targets: rawTargets.length,
2786
+ total_after_filters: sanitizedTargets.length,
2787
+ returned: targets.length,
2788
+ has_more: input.offset + input.limit < sanitizedTargets.length,
2789
+ target_source: "program_targets"
2790
+ }
2791
+ });
2792
+ }
2793
+ async function getProgramTargetsExportFallbackPayload(client, input, programId) {
2794
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, programId);
2795
+ const sanitizedTargets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, input));
2796
+ const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
2797
+ return stripUndefined({
2798
+ request_id: exportLookup.requestId,
2799
+ upstream_request_id: exportLookup.upstreamRequestId,
2800
+ fetched_at: exportLookup.fetchedAt,
2801
+ cache: exportLookup.cache,
2802
+ source_requests: exportLookup.sourceRequests,
2803
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
2804
+ program_id: programId,
2805
+ targets: formatTargetList(targets, input.output_mode),
2806
+ meta: stripUndefined({
2807
+ offset: input.offset,
2808
+ limit: input.limit,
2809
+ output_mode: input.output_mode,
2810
+ total_active_targets: exportLookup.rawTargets.length,
2811
+ total_after_filters: sanitizedTargets.length,
2812
+ returned: targets.length,
2813
+ has_more: input.offset + input.limit < sanitizedTargets.length,
2814
+ target_source: exportLookup.source,
2815
+ target_list_unavailable: exportLookup.unavailable,
2816
+ fallback_error: exportLookup.error
2817
+ })
2818
+ });
2819
+ }
2820
+ async function fetchProgramTargetsFromExportFallback(client, programId) {
2821
+ try {
2822
+ const api = await client.exportTargets({
2823
+ program_ids: [programId],
2824
+ include_out_of_scope: true,
2825
+ include_ineligible: true,
2826
+ format: "json",
2827
+ limit: MAX_TARGET_EXPORT
2828
+ });
2829
+ const rawTargets = readExportTargetRows(api.data).map((row) => sanitizeTarget(readObject(row)?.target ?? row));
2830
+ return {
2831
+ requestId: api.requestId,
2832
+ upstreamRequestId: api.upstreamRequestId,
2833
+ fetchedAt: api.fetchedAt,
2834
+ cache: readObject(stripUndefined({
2835
+ hit: api.cached,
2836
+ coalesced_live_request: api.coalesced,
2837
+ expires_at: api.cacheExpiresAt
2838
+ })),
2839
+ source: "target_export",
2840
+ rawTargets,
2841
+ sourceRequests: [
2842
+ {
2843
+ source: "target_export",
2844
+ program_id: programId,
2845
+ request_id: api.requestId,
2846
+ upstream_request_id: api.upstreamRequestId,
2847
+ ...apiSourceMetadata(api)
2848
+ }
2849
+ ],
2850
+ warnings: [`Used target export fallback because the per-program targets endpoint returned 404 for ${programId}.`],
2851
+ unavailable: false
2852
+ };
2853
+ }
2854
+ catch (error) {
2855
+ return {
2856
+ requestId: randomUUID(),
2857
+ source: "unavailable",
2858
+ rawTargets: [],
2859
+ sourceRequests: [
2860
+ {
2861
+ source: "target_export",
2862
+ program_id: programId,
2863
+ failed: true,
2864
+ error: programFetchError(programId, error)
2865
+ }
2866
+ ],
2867
+ warnings: [
2868
+ `The BBRadar program index contains ${programId}, but both the per-program targets endpoint and target export fallback were unavailable. Exact targets cannot be returned from the current API response.`
2869
+ ],
2870
+ unavailable: true,
2871
+ error: programFetchError(programId, error)
2872
+ };
2873
+ }
2874
+ }
2875
+ function readExportTargetRows(data) {
2876
+ if (Array.isArray(data)) {
2877
+ return data;
2878
+ }
2879
+ const object = readObject(data);
2880
+ if (!object) {
2881
+ return [];
2882
+ }
2883
+ return [object.targets, object.results, object.data]
2884
+ .map(readArray)
2885
+ .find((rows) => rows.length > 0) ?? [];
2886
+ }
2619
2887
  async function getProgramScopeSummary(client, config, input) {
2620
2888
  const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2621
2889
  const targetsData = readObject(targetsApi.data);
2622
2890
  const rawTargets = readArray(targetsData?.targets);
2623
- const targets = rawTargets.map(sanitizeTarget);
2891
+ const targets = rawTargets
2892
+ .map(sanitizeTarget)
2893
+ .filter((target) => targetHasAllowedScope(target, {
2894
+ include_out_of_scope: input.include_out_of_scope,
2895
+ include_ineligible: input.include_ineligible,
2896
+ strict_scope_filter: false
2897
+ }));
2624
2898
  return {
2625
2899
  request_id: randomUUID(),
2626
2900
  source_requests: [
@@ -2641,11 +2915,39 @@ async function getProgramScopeSummary(client, config, input) {
2641
2915
  scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
2642
2916
  meta: {
2643
2917
  total_active_targets: rawTargets.length,
2918
+ total_after_filters: targets.length,
2644
2919
  target_list_mode: input.target_list_mode,
2645
2920
  group_limit: input.group_limit
2646
2921
  }
2647
2922
  };
2648
2923
  }
2924
+ async function getProgramScopeSummaryFromIndexFallback(client, indexFallback, input) {
2925
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
2926
+ const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
2927
+ include_out_of_scope: input.include_out_of_scope,
2928
+ include_ineligible: input.include_ineligible,
2929
+ strict_scope_filter: false
2930
+ }));
2931
+ return stripUndefined({
2932
+ request_id: exportLookup.requestId,
2933
+ upstream_request_id: exportLookup.upstreamRequestId,
2934
+ fetched_at: exportLookup.fetchedAt,
2935
+ cache: exportLookup.cache,
2936
+ source_requests: exportLookup.sourceRequests,
2937
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
2938
+ program: formatProgram(indexFallback.program, "compact"),
2939
+ scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
2940
+ meta: stripUndefined({
2941
+ total_active_targets: exportLookup.rawTargets.length,
2942
+ total_after_filters: targets.length,
2943
+ target_list_mode: input.target_list_mode,
2944
+ group_limit: input.group_limit,
2945
+ target_source: exportLookup.source,
2946
+ target_list_unavailable: exportLookup.unavailable,
2947
+ fallback_error: exportLookup.error
2948
+ })
2949
+ });
2950
+ }
2649
2951
  async function getProgramTargetBreakdown(client, config, input) {
2650
2952
  const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2651
2953
  const targetsData = readObject(targetsApi.data);
@@ -2692,6 +2994,42 @@ async function getProgramTargetBreakdown(client, config, input) {
2692
2994
  }
2693
2995
  };
2694
2996
  }
2997
+ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback, input) {
2998
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
2999
+ const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
3000
+ include_out_of_scope: input.include_out_of_scope,
3001
+ include_ineligible: input.include_ineligible,
3002
+ strict_scope_filter: false
3003
+ }));
3004
+ const scope = buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible);
3005
+ return stripUndefined({
3006
+ request_id: exportLookup.requestId,
3007
+ upstream_request_id: exportLookup.upstreamRequestId,
3008
+ fetched_at: exportLookup.fetchedAt,
3009
+ cache: exportLookup.cache,
3010
+ source_requests: exportLookup.sourceRequests,
3011
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
3012
+ program: formatProgram(indexFallback.program, "compact"),
3013
+ breakdown: {
3014
+ target_counts: readObject(scope.target_counts),
3015
+ by_target_type: countTargetValues(targets, (target) => stringField(target, "target_type")),
3016
+ by_scope_tag: countTargetValues(targets, (target) => readStringArrayField(target, "scope_tags")),
3017
+ by_language_tag: countTargetValues(targets, (target) => readStringArrayField(target, "language_tags")),
3018
+ group_counts: readObject(scope.group_counts),
3019
+ group_has_more: readObject(scope.group_has_more),
3020
+ groups: readObject(scope.groups)
3021
+ },
3022
+ meta: stripUndefined({
3023
+ total_active_targets: exportLookup.rawTargets.length,
3024
+ total_after_filters: targets.length,
3025
+ target_list_mode: input.target_list_mode,
3026
+ group_limit: input.group_limit,
3027
+ target_source: exportLookup.source,
3028
+ target_list_unavailable: exportLookup.unavailable,
3029
+ fallback_error: exportLookup.error
3030
+ })
3031
+ });
3032
+ }
2695
3033
  async function getProgramScopeDelta(client, config, input) {
2696
3034
  const programApi = await client.getProgram(input.program_id);
2697
3035
  const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
@@ -2903,6 +3241,77 @@ async function checkProgramNewTargets(client, config, input) {
2903
3241
  });
2904
3242
  return programLookup.fallback ? withProgramIdFallbackMetadata(payload, programLookup.fallback) : payload;
2905
3243
  }
3244
+ async function checkProgramNewTargetsFromIndexedProgram(client, config, input, indexedProgram) {
3245
+ const programId = input.program_id;
3246
+ const handle = programSearchText(programId);
3247
+ const api = await client.getRecentChanges({
3248
+ change_type: "added",
3249
+ include_removed: false,
3250
+ include_ineligible: input.include_ineligible,
3251
+ include_out_of_scope: false,
3252
+ search: handle.length >= 2 ? handle : programId,
3253
+ tags: input.target_type ? [input.target_type] : [],
3254
+ page: 1,
3255
+ page_size: input.page_size
3256
+ });
3257
+ const data = readObject(api.data);
3258
+ const upstreamMeta = readObject(sanitizeJson(data?.meta));
3259
+ const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
3260
+ const changes = readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
3261
+ const matchingChanges = changes
3262
+ .filter((change) => {
3263
+ const target = readObject(change.target);
3264
+ const changedAt = timestampField(change, "changed_at");
3265
+ return (stringField(change, "change_type") === "added" &&
3266
+ stringField(readObject(change.program), "id") === programId &&
3267
+ target !== undefined &&
3268
+ (sinceTimestamp === undefined || changedAt >= sinceTimestamp) &&
3269
+ (!input.target_type || targetMatchesRequestedType(target, input.target_type)) &&
3270
+ targetHasAllowedScope(target, {
3271
+ include_out_of_scope: false,
3272
+ include_ineligible: input.include_ineligible,
3273
+ strict_scope_filter: true
3274
+ }));
3275
+ })
3276
+ .sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
3277
+ const limitedChanges = matchingChanges.slice(0, input.max_targets);
3278
+ const targets = limitedChanges
3279
+ .map((change) => readObject(change.target))
3280
+ .filter((target) => target !== undefined);
3281
+ const latest = matchingChanges[0];
3282
+ return stripUndefined({
3283
+ request_id: randomUUID(),
3284
+ source_requests: [
3285
+ {
3286
+ source: "recent_changes",
3287
+ request_id: api.requestId,
3288
+ upstream_request_id: api.upstreamRequestId,
3289
+ ...apiSourceMetadata(api)
3290
+ }
3291
+ ],
3292
+ program_id: programId,
3293
+ program: latest
3294
+ ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact")
3295
+ : formatProgram(indexedProgram, "compact"),
3296
+ has_new_targets: matchingChanges.length > 0,
3297
+ new_target_count: matchingChanges.length,
3298
+ latest_added_at: latest ? stringField(latest, "changed_at") : undefined,
3299
+ new_targets: formatTargetList(targets, input.target_list_mode),
3300
+ meta: stripUndefined({
3301
+ recent_changes_scanned: changes.length,
3302
+ returned: targets.length,
3303
+ has_more: matchingChanges.length > targets.length,
3304
+ since: input.since,
3305
+ target_type: input.target_type,
3306
+ in_scope_only: true,
3307
+ include_ineligible: input.include_ineligible,
3308
+ target_list_mode: input.target_list_mode,
3309
+ program_detail_source: "program_index",
3310
+ upstream_total_pages: readNumber(upstreamMeta?.total_pages),
3311
+ scan_may_be_incomplete: (readNumber(upstreamMeta?.total_pages) ?? 1) > 1
3312
+ })
3313
+ });
3314
+ }
2906
3315
  async function checkWatchlistNewTargets(client, config, input) {
2907
3316
  const watchlist = new Set(input.program_ids);
2908
3317
  const api = await client.getRecentChanges({
@@ -3573,8 +3982,9 @@ function candidateLooksLikeVdp(candidate) {
3573
3982
  return candidateHasKnownNoBounty(candidate) || tagIncludesAnyNormalized(candidate.normalizedSearchSignals, VDP_SIGNALS);
3574
3983
  }
3575
3984
  function programResolutionMatch(candidate, query) {
3576
- const normalizedQuery = normalizeTag(query);
3577
- const queryTokens = tokenSet(normalizedQuery);
3985
+ const queryVariants = programResolutionQueryVariants(query);
3986
+ const primaryQuery = queryVariants[0] ?? normalizeTag(query);
3987
+ const queryTokens = meaningfulProgramQueryTokens(query);
3578
3988
  const fields = [
3579
3989
  { name: "id", value: stringField(candidate.program, "id") },
3580
3990
  { name: "handle", value: stringField(candidate.program, "handle") },
@@ -3592,16 +4002,23 @@ function programResolutionMatch(candidate, query) {
3592
4002
  }
3593
4003
  const normalizedValue = normalizeTag(field.value);
3594
4004
  const fieldTokens = tokenSet(normalizedValue);
3595
- if (normalizedValue === normalizedQuery) {
3596
- score = Math.max(score, field.name === "id" || field.name === "handle" ? 100 : 95);
3597
- matchedFields.add(field.name);
3598
- reasons.add(`${field.name} exact match`);
3599
- continue;
3600
- }
3601
- if (normalizedValue.includes(normalizedQuery)) {
3602
- score = Math.max(score, field.name === "name" || field.name === "handle" ? 80 : 65);
3603
- matchedFields.add(field.name);
3604
- reasons.add(`${field.name} contains query`);
4005
+ for (const queryVariant of queryVariants) {
4006
+ if (normalizedValue === queryVariant) {
4007
+ score = Math.max(score, field.name === "id" || field.name === "handle" ? 100 : 95);
4008
+ matchedFields.add(field.name);
4009
+ reasons.add(queryVariant === primaryQuery ? `${field.name} exact match` : `${field.name} exact meaningful-query match`);
4010
+ continue;
4011
+ }
4012
+ if (normalizedValue.includes(queryVariant)) {
4013
+ const containsScore = field.name === "name" || field.name === "handle"
4014
+ ? queryVariant.includes("-") || queryVariant.includes(" ")
4015
+ ? 90
4016
+ : 85
4017
+ : 70;
4018
+ matchedFields.add(field.name);
4019
+ score = Math.max(score, containsScore);
4020
+ reasons.add(queryVariant === primaryQuery ? `${field.name} contains query` : `${field.name} contains meaningful query`);
4021
+ }
3605
4022
  }
3606
4023
  const tokenOverlap = [...queryTokens].filter((token) => fieldTokens.has(token)).length;
3607
4024
  if (tokenOverlap > 0) {
@@ -3611,10 +4028,12 @@ function programResolutionMatch(candidate, query) {
3611
4028
  reasons.add(`${field.name} token match`);
3612
4029
  }
3613
4030
  }
3614
- if (candidate.normalizedSearchSignals.some((signal) => signal.includes(normalizedQuery))) {
3615
- score = Math.max(score, 55);
3616
- matchedFields.add("search_signals");
3617
- reasons.add("program search signals contain query");
4031
+ for (const queryVariant of queryVariants) {
4032
+ if (candidate.normalizedSearchSignals.some((signal) => signal.includes(queryVariant))) {
4033
+ score = Math.max(score, queryVariant === primaryQuery ? 55 : 65);
4034
+ matchedFields.add("search_signals");
4035
+ reasons.add(queryVariant === primaryQuery ? "program search signals contain query" : "program search signals contain meaningful query");
4036
+ }
3618
4037
  }
3619
4038
  return {
3620
4039
  score,
@@ -3622,6 +4041,25 @@ function programResolutionMatch(candidate, query) {
3622
4041
  reasons: [...reasons]
3623
4042
  };
3624
4043
  }
4044
+ function programResolutionSearchQueries(query) {
4045
+ return programResolutionQueryVariants(query).slice(0, MAX_RESOLVE_SEARCH_QUERIES);
4046
+ }
4047
+ function programResolutionQueryVariants(query) {
4048
+ const normalized = normalizeTag(query);
4049
+ const meaningfulTokens = [...meaningfulProgramQueryTokens(query)];
4050
+ const meaningfulPhrase = meaningfulTokens.join(" ");
4051
+ const values = [
4052
+ normalized,
4053
+ meaningfulPhrase,
4054
+ meaningfulTokens.join("-"),
4055
+ ...meaningfulTokens
4056
+ ];
4057
+ return uniqueStrings(values.filter((value) => value.length >= 2));
4058
+ }
4059
+ function meaningfulProgramQueryTokens(query) {
4060
+ const tokens = [...tokenSet(normalizeTag(query))].filter((token) => !PROGRAM_RESOLUTION_STOP_WORDS.has(token));
4061
+ return new Set(tokens.length > 0 ? tokens : [...tokenSet(normalizeTag(query))]);
4062
+ }
3625
4063
  function normalizedCandidateValues(program, normalizedScopeTags) {
3626
4064
  return [
3627
4065
  stringField(program, "id"),
@@ -3672,9 +4110,14 @@ function programHandleText(programId) {
3672
4110
  const colonIndex = programId.indexOf(":");
3673
4111
  return colonIndex >= 0 ? programId.slice(colonIndex + 1) : programId;
3674
4112
  }
4113
+ function programPlatformText(programId) {
4114
+ const colonIndex = programId.indexOf(":");
4115
+ return colonIndex >= 0 ? programId.slice(0, colonIndex) : "";
4116
+ }
3675
4117
  function normalizeFindProgramsInput(input) {
3676
4118
  const warnings = [];
3677
4119
  const normalized = { ...input };
4120
+ normalized.vdp_mode ??= "exclude";
3678
4121
  normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
3679
4122
  normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
3680
4123
  normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
@@ -3805,6 +4248,55 @@ async function collectPrograms(client, options) {
3805
4248
  totalPagesBySource
3806
4249
  };
3807
4250
  }
4251
+ async function collectProgramsForResolution(client, options) {
4252
+ const collections = [];
4253
+ const queries = options.searchQueries.length > 0 ? options.searchQueries : [undefined];
4254
+ for (const search of queries) {
4255
+ if (options.budget.remaining <= 0) {
4256
+ addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all program name search variants could be checked.");
4257
+ break;
4258
+ }
4259
+ collections.push(await collectPrograms(client, {
4260
+ search,
4261
+ platforms: options.platforms,
4262
+ tags: options.tags,
4263
+ updated_since: undefined,
4264
+ opportunity_levels: options.opportunity_levels,
4265
+ max_pages: options.max_pages,
4266
+ budget: options.budget,
4267
+ warnings: options.warnings
4268
+ }));
4269
+ }
4270
+ if (collections.length === 0) {
4271
+ return {
4272
+ programs: [],
4273
+ sourceRequests: [],
4274
+ requestsScanned: 0,
4275
+ programsScanned: 0,
4276
+ totalPagesBySource: {}
4277
+ };
4278
+ }
4279
+ return mergeProgramCollections(collections);
4280
+ }
4281
+ function mergeProgramCollections(collections) {
4282
+ const programs = new Map();
4283
+ const totalPagesBySource = {};
4284
+ for (const collection of collections) {
4285
+ for (const program of collection.programs) {
4286
+ programs.set(programKey(program, programs.size), program);
4287
+ }
4288
+ for (const [sourceName, totalPages] of Object.entries(collection.totalPagesBySource)) {
4289
+ totalPagesBySource[sourceName] = totalPages;
4290
+ }
4291
+ }
4292
+ return {
4293
+ programs: [...programs.values()],
4294
+ sourceRequests: collections.flatMap((collection) => collection.sourceRequests),
4295
+ requestsScanned: collections.reduce((total, collection) => total + collection.requestsScanned, 0),
4296
+ programsScanned: programs.size,
4297
+ totalPagesBySource
4298
+ };
4299
+ }
3808
4300
  async function collectProgramSources(client, sources, options) {
3809
4301
  const pages = [];
3810
4302
  await mapWithConcurrency(sources.map((source, sourceIndex) => ({ source, sourceIndex })), PROGRAM_COLLECTION_CONCURRENCY, async ({ source, sourceIndex }) => {
@@ -3849,6 +4341,7 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
3849
4341
  const runWithSlot = options.runWithProgramPageSlot ?? runImmediately;
3850
4342
  return runWithSlot(async () => {
3851
4343
  const query = toQuery({
4344
+ search: options.search,
3852
4345
  platforms: options.platforms,
3853
4346
  tags: options.tags,
3854
4347
  updated_since: options.updated_since,
@@ -3864,14 +4357,16 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
3864
4357
  const data = readObject(api.data);
3865
4358
  const meta = readObject(data?.meta);
3866
4359
  const sourceName = source.kind === "opportunity" ? `opportunities:${source.level}` : "programs";
4360
+ const labeledSourceName = options.search ? `${sourceName}:search:${normalizeTag(options.search)}` : sourceName;
3867
4361
  return {
3868
- sourceName,
4362
+ sourceName: labeledSourceName,
3869
4363
  sourceIndex,
3870
4364
  page,
3871
4365
  programs: readArray(data?.programs),
3872
4366
  totalPages: readNumber(meta?.total_pages),
3873
4367
  sourceRequest: stripUndefined({
3874
- source: sourceName,
4368
+ source: labeledSourceName,
4369
+ search: options.search,
3875
4370
  request_id: api.requestId,
3876
4371
  upstream_request_id: api.upstreamRequestId,
3877
4372
  ...apiSourceMetadata(api),
@@ -3994,13 +4489,7 @@ function programCandidateMatches(candidate, filters) {
3994
4489
  if (filters.contest_like === true && !candidateLooksContestLike(candidate)) {
3995
4490
  return false;
3996
4491
  }
3997
- if (filters.vdp_mode === "exclude" && candidateLooksLikeVdp(candidate)) {
3998
- return false;
3999
- }
4000
- if (filters.vdp_mode === "only_likely" && !candidateLooksLikeVdp(candidate)) {
4001
- return false;
4002
- }
4003
- if (filters.vdp_mode === "only_known_no_bounty" && !candidateHasKnownNoBounty(candidate)) {
4492
+ if (!candidateMatchesVdpMode(candidate, filters.vdp_mode ?? "exclude")) {
4004
4493
  return false;
4005
4494
  }
4006
4495
  if (filters.fresh_launch_days !== undefined) {
@@ -4061,6 +4550,18 @@ function compareProgramCandidates(left, right, sortBy) {
4061
4550
  right.firstSeenTime - left.firstSeenTime ||
4062
4551
  right.lastUpdatedTime - left.lastUpdatedTime);
4063
4552
  }
4553
+ function candidateMatchesVdpMode(candidate, vdpMode) {
4554
+ if (vdpMode === "exclude") {
4555
+ return !candidateLooksLikeVdp(candidate);
4556
+ }
4557
+ if (vdpMode === "only_likely") {
4558
+ return candidateLooksLikeVdp(candidate);
4559
+ }
4560
+ if (vdpMode === "only_known_no_bounty") {
4561
+ return candidateHasKnownNoBounty(candidate);
4562
+ }
4563
+ return true;
4564
+ }
4064
4565
  async function collectTargetSamples(client, candidates, input, budget) {
4065
4566
  const samples = {};
4066
4567
  const sampleCandidates = candidates.slice(0, input.target_sample_programs);
@@ -4263,7 +4764,7 @@ function registerPrompts(server) {
4263
4764
 
4264
4765
  Find the best BBRadar program candidates.
4265
4766
  - Start with find_program_candidates unless the user needs custom filters; pass opportunity_levels from this JSON array unless the user asked for a broader search: ${promptJson([args.opportunity_level ?? "elite"])}.
4266
- - Use find_language_programs for language asks, find_target_type_programs for API/mobile/source-code/domain asks, find_reward_programs or find_paid_programs for reward asks, find_vdp_programs for VDP/no-bounty asks, find_web3_contests for Web3 contest/audit asks, find_web3_programs for general Web3 requests, and find_wildcard_programs for wildcard-specific requests.
4767
+ - Use find_language_programs for language asks, find_target_type_programs for API/mobile/source-code/domain asks, find_reward_programs or find_paid_programs for reward asks, find_vdp_programs only for explicit VDP/no-bounty asks, find_web3_contests for Web3 contest/audit asks, find_web3_programs for general Web3 requests, and find_wildcard_programs for wildcard-specific requests.
4267
4768
  - Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
4268
4769
  - Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
4269
4770
  - Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.
@@ -4273,16 +4774,16 @@ Find the best BBRadar program candidates.
4273
4774
  - Stay passive-only; do not recommend scanning, probing, exploit attempts, or direct contact with targets.`));
4274
4775
  server.registerPrompt("summarize_program_scope", {
4275
4776
  title: "Summarize Program Scope",
4276
- description: "Summarize in-scope and out-of-scope BBRadar target data for one program.",
4777
+ description: "Summarize BBRadar target data for one program.",
4277
4778
  argsSchema: {
4278
4779
  program_id: programIdSchema
4279
4780
  }
4280
- }, (args) => promptResult(`Use get_program for the program_id in this JSON string: ${promptJson(args.program_id)}. Then use get_program_targets with include_out_of_scope=true, include_ineligible=true, and limit=${MAX_TARGETS_PER_PROGRAM}.
4781
+ }, (args) => promptResult(`Use get_program for the program_id in this JSON string: ${promptJson(args.program_id)}. Then use get_program_targets with include_out_of_scope=false, include_ineligible=false, and limit=${MAX_TARGETS_PER_PROGRAM}. Only request out-of-scope or ineligible targets if the user explicitly asks for them.
4281
4782
 
4282
4783
  Summarize the program scope:
4283
4784
  - Program name, platform, reward range, public report count, first seen date, last updated date, and BBRadar URL.
4284
4785
  - In-scope bounty-eligible target categories.
4285
- - Out-of-scope or ineligible target categories.
4786
+ - Out-of-scope or ineligible target categories only if explicitly requested.
4286
4787
  - Wildcards, high-severity target labels, and notable language or scope tags.
4287
4788
  - Recent target update signals if present.
4288
4789
  - Do not contact, scan, probe, or validate any listed target.`));