@bbradar/mcp 0.1.4 → 0.1.6

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,74 @@ 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);
1528
+ }
1529
+ try {
1530
+ const payload = await callback(fallback.resolvedProgramId);
1531
+ return withProgramIdFallbackMetadata(payload, fallback);
1532
+ }
1533
+ catch (fallbackError) {
1534
+ if (!isProgramNotFoundError(fallbackError) || !indexFallbackCallback) {
1535
+ throw fallbackError;
1536
+ }
1537
+ const indexFallback = await resolveIndexedProgramId(client, config, fallback.resolvedProgramId, fallbackError);
1538
+ if (!indexFallback) {
1539
+ throw fallbackError;
1540
+ }
1541
+ const combinedFallback = {
1542
+ ...indexFallback,
1543
+ requestedProgramId,
1544
+ sourceRequests: [...fallback.sourceRequests, ...indexFallback.sourceRequests],
1545
+ warnings: uniqueStrings([...fallback.warnings, ...indexFallback.warnings])
1546
+ };
1547
+ const payload = await indexFallbackCallback(combinedFallback);
1548
+ return withProgramIndexFallbackMetadata(payload, combinedFallback);
1486
1549
  }
1487
- const payload = await callback(fallback.resolvedProgramId);
1488
- return withProgramIdFallbackMetadata(payload, fallback);
1489
1550
  }
1490
1551
  }
1552
+ async function resolveIndexedProgramId(client, config, requestedProgramId, staleError) {
1553
+ const query = programSearchText(requestedProgramId);
1554
+ if (query.length < 2) {
1555
+ return undefined;
1556
+ }
1557
+ const resolution = await resolveProgram(client, config, {
1558
+ query,
1559
+ platforms: [],
1560
+ tags: [],
1561
+ opportunity_levels: [],
1562
+ max_pages: STALE_PROGRAM_ID_RESOLVE_PAGES,
1563
+ max_results: 5,
1564
+ output_mode: "full",
1565
+ upstream_request_budget: STALE_PROGRAM_ID_RESOLVE_BUDGET
1566
+ });
1567
+ const match = selectProgramIndexFallbackMatch(requestedProgramId, resolution);
1568
+ if (!match) {
1569
+ return undefined;
1570
+ }
1571
+ const sourceRequests = readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined);
1572
+ const warnings = readArray(resolution.warnings).filter((warning) => typeof warning === "string");
1573
+ const staleApiError = staleError instanceof BBRadarApiError ? staleError : undefined;
1574
+ const indexWarning = `program_id ${requestedProgramId} exists in the BBRadar program index, but the per-program detail endpoint returned 404.`;
1575
+ return {
1576
+ requestedProgramId,
1577
+ resolvedProgramId: match.resolvedProgramId,
1578
+ query,
1579
+ staleRequestId: staleApiError?.requestId,
1580
+ staleUpstreamRequestId: staleApiError?.upstreamRequestId,
1581
+ sourceRequests,
1582
+ warnings: uniqueStrings([...warnings, indexWarning]),
1583
+ resolution,
1584
+ program: match.program
1585
+ };
1586
+ }
1491
1587
  async function resolveStaleProgramId(client, config, requestedProgramId, staleError) {
1492
1588
  const query = programSearchText(requestedProgramId);
1493
1589
  if (query.length < 2) {
@@ -1525,6 +1621,7 @@ async function resolveStaleProgramId(client, config, requestedProgramId, staleEr
1525
1621
  function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1526
1622
  const requestedHandle = normalizeTag(programHandleText(requestedProgramId));
1527
1623
  const requestedSearchText = normalizeTag(programSearchText(requestedProgramId));
1624
+ const requestedPlatform = normalizeTag(programPlatformText(requestedProgramId));
1528
1625
  const candidates = readArray(resolution.matches)
1529
1626
  .map((entry) => readObject(entry))
1530
1627
  .filter((entry) => entry !== undefined)
@@ -1533,17 +1630,29 @@ function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1533
1630
  const resolvedProgramId = stringField(entry, "program_id") ?? stringField(program, "id");
1534
1631
  const candidateHandle = normalizeTag(stringField(program, "handle") ?? "");
1535
1632
  const candidateSearchText = resolvedProgramId ? normalizeTag(programSearchText(resolvedProgramId)) : "";
1633
+ const candidatePlatform = normalizeTag(stringField(program, "platform") ?? (resolvedProgramId ? programPlatformText(resolvedProgramId) : ""));
1536
1634
  const score = readNumber(entry.score) ?? 0;
1537
1635
  const exactHandleMatch = candidateHandle === requestedHandle ||
1538
1636
  candidateHandle === requestedSearchText ||
1539
1637
  candidateSearchText === requestedSearchText;
1638
+ const partialHandleMatch = requestedHandle.length >= 4 &&
1639
+ requestedPlatform.length > 0 &&
1640
+ candidatePlatform === requestedPlatform &&
1641
+ (candidateHandle.startsWith(`${requestedHandle}_`) ||
1642
+ candidateHandle.startsWith(`${requestedHandle}-`) ||
1643
+ candidateSearchText.startsWith(`${requestedSearchText}_`) ||
1644
+ candidateSearchText.startsWith(`${requestedSearchText}-`));
1540
1645
  return {
1541
1646
  resolvedProgramId,
1542
1647
  score,
1543
- exactHandleMatch
1648
+ exactHandleMatch,
1649
+ partialHandleMatch
1544
1650
  };
1545
1651
  })
1546
- .filter((entry) => entry.resolvedProgramId !== undefined && entry.resolvedProgramId !== requestedProgramId && entry.exactHandleMatch && entry.score >= 80)
1652
+ .filter((entry) => entry.resolvedProgramId !== undefined &&
1653
+ normalizeTag(entry.resolvedProgramId) !== normalizeTag(requestedProgramId) &&
1654
+ (entry.exactHandleMatch || entry.partialHandleMatch) &&
1655
+ entry.score >= 80)
1547
1656
  .sort((left, right) => right.score - left.score || left.resolvedProgramId.localeCompare(right.resolvedProgramId));
1548
1657
  if (candidates.length === 0) {
1549
1658
  return undefined;
@@ -1556,6 +1665,48 @@ function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1556
1665
  resolvedProgramId: best.resolvedProgramId
1557
1666
  };
1558
1667
  }
1668
+ function selectProgramIndexFallbackMatch(requestedProgramId, resolution) {
1669
+ const requestedId = normalizeTag(requestedProgramId);
1670
+ const requestedHandle = normalizeTag(programHandleText(requestedProgramId));
1671
+ const requestedPlatform = normalizeTag(programPlatformText(requestedProgramId));
1672
+ const candidates = readArray(resolution.matches)
1673
+ .map((entry) => readObject(entry))
1674
+ .filter((entry) => entry !== undefined)
1675
+ .map((entry) => {
1676
+ const program = readObject(entry.program);
1677
+ const resolvedProgramId = stringField(entry, "program_id") ?? stringField(program, "id");
1678
+ const candidateHandle = normalizeTag(stringField(program, "handle") ?? (resolvedProgramId ? programHandleText(resolvedProgramId) : ""));
1679
+ const candidatePlatform = normalizeTag(stringField(program, "platform") ?? (resolvedProgramId ? programPlatformText(resolvedProgramId) : ""));
1680
+ const score = readNumber(entry.score) ?? 0;
1681
+ const idMatch = resolvedProgramId !== undefined && normalizeTag(resolvedProgramId) === requestedId;
1682
+ const platformHandleMatch = requestedPlatform.length > 0 &&
1683
+ candidatePlatform === requestedPlatform &&
1684
+ candidateHandle === requestedHandle;
1685
+ const partialPlatformHandleMatch = requestedHandle.length >= 4 &&
1686
+ requestedPlatform.length > 0 &&
1687
+ candidatePlatform === requestedPlatform &&
1688
+ (candidateHandle.startsWith(`${requestedHandle}_`) || candidateHandle.startsWith(`${requestedHandle}-`));
1689
+ return {
1690
+ resolvedProgramId,
1691
+ program,
1692
+ score,
1693
+ exactMatch: idMatch || platformHandleMatch || partialPlatformHandleMatch
1694
+ };
1695
+ })
1696
+ .filter((entry) => entry.resolvedProgramId !== undefined && entry.program !== undefined && entry.exactMatch && entry.score >= 80)
1697
+ .sort((left, right) => right.score - left.score || left.resolvedProgramId.localeCompare(right.resolvedProgramId));
1698
+ if (candidates.length === 0) {
1699
+ return undefined;
1700
+ }
1701
+ const [best, next] = candidates;
1702
+ if (!best || (next && best.score === next.score && normalizeTag(next.resolvedProgramId) !== normalizeTag(best.resolvedProgramId))) {
1703
+ return undefined;
1704
+ }
1705
+ return {
1706
+ resolvedProgramId: best.resolvedProgramId,
1707
+ program: addProgramResourceLinks(best.program)
1708
+ };
1709
+ }
1559
1710
  function withProgramIdFallbackMetadata(payload, fallback) {
1560
1711
  const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
1561
1712
  const warnings = readArray(payload.warnings).filter((warning) => typeof warning === "string");
@@ -1576,6 +1727,17 @@ function withProgramIdFallbackMetadata(payload, fallback) {
1576
1727
  program_id_resolution: resolution
1577
1728
  });
1578
1729
  }
1730
+ function withProgramIndexFallbackMetadata(payload, fallback) {
1731
+ const enriched = withProgramIdFallbackMetadata(payload, fallback);
1732
+ const resolution = readObject(enriched.program_id_resolution);
1733
+ return {
1734
+ ...enriched,
1735
+ program_id_resolution: stripUndefined({
1736
+ ...(resolution ?? {}),
1737
+ reason: "program_index_detail_404"
1738
+ })
1739
+ };
1740
+ }
1579
1741
  async function resolveProgram(client, config, input) {
1580
1742
  const warnings = [];
1581
1743
  const budget = {
@@ -1583,10 +1745,11 @@ async function resolveProgram(client, config, input) {
1583
1745
  remaining: input.upstream_request_budget
1584
1746
  };
1585
1747
  const maxPages = clampLimit("max_pages", input.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
1586
- const collected = await collectPrograms(client, {
1748
+ const searchQueries = programResolutionSearchQueries(input.query);
1749
+ const collected = await collectProgramsForResolution(client, {
1750
+ searchQueries,
1587
1751
  platforms: input.platforms,
1588
1752
  tags: input.tags,
1589
- updated_since: undefined,
1590
1753
  opportunity_levels: input.opportunity_levels,
1591
1754
  max_pages: maxPages,
1592
1755
  budget,
@@ -1608,6 +1771,7 @@ async function resolveProgram(client, config, input) {
1608
1771
  warnings: warnings.length > 0 ? warnings : undefined,
1609
1772
  query: {
1610
1773
  text: input.query,
1774
+ search_queries: searchQueries,
1611
1775
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
1612
1776
  tags: input.tags.length > 0 ? input.tags : undefined
1613
1777
  },
@@ -1715,6 +1879,31 @@ async function runResolvedProgramAction(client, config, programId, input) {
1715
1879
  include_ineligible: input.include_ineligible
1716
1880
  });
1717
1881
  }
1882
+ if (input.action === "targets") {
1883
+ const api = await client.getProgramTargets(programId);
1884
+ const data = readObject(api.data);
1885
+ const rawTargets = readArray(data?.targets);
1886
+ const sanitizedTargets = rawTargets
1887
+ .map(sanitizeTarget)
1888
+ .filter((target) => targetHasAllowedScope(target, {
1889
+ include_out_of_scope: input.include_out_of_scope,
1890
+ include_ineligible: input.include_ineligible,
1891
+ strict_scope_filter: true
1892
+ }));
1893
+ const targets = sanitizedTargets.slice(0, input.max_targets);
1894
+ return withApiMetadata(api, {
1895
+ program_id: programId,
1896
+ targets: formatTargetList(targets, input.target_list_mode),
1897
+ meta: {
1898
+ limit: input.max_targets,
1899
+ output_mode: input.target_list_mode,
1900
+ total_active_targets: rawTargets.length,
1901
+ total_after_filters: sanitizedTargets.length,
1902
+ returned: targets.length,
1903
+ has_more: sanitizedTargets.length > targets.length
1904
+ }
1905
+ });
1906
+ }
1718
1907
  return checkProgramNewTargets(client, config, {
1719
1908
  program_id: programId,
1720
1909
  since: input.since,
@@ -2080,7 +2269,7 @@ function findLowNoisePrograms(client, config, input) {
2080
2269
  exclude_mobile_only: false,
2081
2270
  exclude_non_web: false,
2082
2271
  has_api_scope: false,
2083
- vdp_mode: input.require_bounty ? "exclude" : "include",
2272
+ vdp_mode: "exclude",
2084
2273
  fresh_launch_days: undefined,
2085
2274
  min_wildcards: 0,
2086
2275
  min_eligible_targets: input.min_eligible_targets,
@@ -2616,11 +2805,132 @@ async function getLatestAddedTargets(client, config, input) {
2616
2805
  })
2617
2806
  });
2618
2807
  }
2808
+ async function getProgramTargetsPayload(client, input, programId) {
2809
+ const api = await client.getProgramTargets(programId);
2810
+ const data = readObject(api.data);
2811
+ const rawTargets = readArray(data?.targets);
2812
+ const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
2813
+ const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
2814
+ return withApiMetadata(api, {
2815
+ program_id: programId,
2816
+ targets: formatTargetList(targets, input.output_mode),
2817
+ meta: {
2818
+ offset: input.offset,
2819
+ limit: input.limit,
2820
+ output_mode: input.output_mode,
2821
+ total_active_targets: rawTargets.length,
2822
+ total_after_filters: sanitizedTargets.length,
2823
+ returned: targets.length,
2824
+ has_more: input.offset + input.limit < sanitizedTargets.length,
2825
+ target_source: "program_targets"
2826
+ }
2827
+ });
2828
+ }
2829
+ async function getProgramTargetsExportFallbackPayload(client, input, programId) {
2830
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, programId);
2831
+ const sanitizedTargets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, input));
2832
+ const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
2833
+ return stripUndefined({
2834
+ request_id: exportLookup.requestId,
2835
+ upstream_request_id: exportLookup.upstreamRequestId,
2836
+ fetched_at: exportLookup.fetchedAt,
2837
+ cache: exportLookup.cache,
2838
+ source_requests: exportLookup.sourceRequests,
2839
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
2840
+ program_id: programId,
2841
+ targets: formatTargetList(targets, input.output_mode),
2842
+ meta: stripUndefined({
2843
+ offset: input.offset,
2844
+ limit: input.limit,
2845
+ output_mode: input.output_mode,
2846
+ total_active_targets: exportLookup.rawTargets.length,
2847
+ total_after_filters: sanitizedTargets.length,
2848
+ returned: targets.length,
2849
+ has_more: input.offset + input.limit < sanitizedTargets.length,
2850
+ target_source: exportLookup.source,
2851
+ target_list_unavailable: exportLookup.unavailable,
2852
+ fallback_error: exportLookup.error
2853
+ })
2854
+ });
2855
+ }
2856
+ async function fetchProgramTargetsFromExportFallback(client, programId) {
2857
+ try {
2858
+ const api = await client.exportTargets({
2859
+ program_ids: [programId],
2860
+ include_out_of_scope: true,
2861
+ include_ineligible: true,
2862
+ format: "json",
2863
+ limit: MAX_TARGET_EXPORT
2864
+ });
2865
+ const rawTargets = readExportTargetRows(api.data).map((row) => sanitizeTarget(readObject(row)?.target ?? row));
2866
+ return {
2867
+ requestId: api.requestId,
2868
+ upstreamRequestId: api.upstreamRequestId,
2869
+ fetchedAt: api.fetchedAt,
2870
+ cache: readObject(stripUndefined({
2871
+ hit: api.cached,
2872
+ coalesced_live_request: api.coalesced,
2873
+ expires_at: api.cacheExpiresAt
2874
+ })),
2875
+ source: "target_export",
2876
+ rawTargets,
2877
+ sourceRequests: [
2878
+ {
2879
+ source: "target_export",
2880
+ program_id: programId,
2881
+ request_id: api.requestId,
2882
+ upstream_request_id: api.upstreamRequestId,
2883
+ ...apiSourceMetadata(api)
2884
+ }
2885
+ ],
2886
+ warnings: [`Used target export fallback because the per-program targets endpoint returned 404 for ${programId}.`],
2887
+ unavailable: false
2888
+ };
2889
+ }
2890
+ catch (error) {
2891
+ return {
2892
+ requestId: randomUUID(),
2893
+ source: "unavailable",
2894
+ rawTargets: [],
2895
+ sourceRequests: [
2896
+ {
2897
+ source: "target_export",
2898
+ program_id: programId,
2899
+ failed: true,
2900
+ error: programFetchError(programId, error)
2901
+ }
2902
+ ],
2903
+ warnings: [
2904
+ `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.`
2905
+ ],
2906
+ unavailable: true,
2907
+ error: programFetchError(programId, error)
2908
+ };
2909
+ }
2910
+ }
2911
+ function readExportTargetRows(data) {
2912
+ if (Array.isArray(data)) {
2913
+ return data;
2914
+ }
2915
+ const object = readObject(data);
2916
+ if (!object) {
2917
+ return [];
2918
+ }
2919
+ return [object.targets, object.results, object.data]
2920
+ .map(readArray)
2921
+ .find((rows) => rows.length > 0) ?? [];
2922
+ }
2619
2923
  async function getProgramScopeSummary(client, config, input) {
2620
2924
  const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2621
2925
  const targetsData = readObject(targetsApi.data);
2622
2926
  const rawTargets = readArray(targetsData?.targets);
2623
- const targets = rawTargets.map(sanitizeTarget);
2927
+ const targets = rawTargets
2928
+ .map(sanitizeTarget)
2929
+ .filter((target) => targetHasAllowedScope(target, {
2930
+ include_out_of_scope: input.include_out_of_scope,
2931
+ include_ineligible: input.include_ineligible,
2932
+ strict_scope_filter: false
2933
+ }));
2624
2934
  return {
2625
2935
  request_id: randomUUID(),
2626
2936
  source_requests: [
@@ -2641,11 +2951,39 @@ async function getProgramScopeSummary(client, config, input) {
2641
2951
  scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
2642
2952
  meta: {
2643
2953
  total_active_targets: rawTargets.length,
2954
+ total_after_filters: targets.length,
2644
2955
  target_list_mode: input.target_list_mode,
2645
2956
  group_limit: input.group_limit
2646
2957
  }
2647
2958
  };
2648
2959
  }
2960
+ async function getProgramScopeSummaryFromIndexFallback(client, indexFallback, input) {
2961
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
2962
+ const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
2963
+ include_out_of_scope: input.include_out_of_scope,
2964
+ include_ineligible: input.include_ineligible,
2965
+ strict_scope_filter: false
2966
+ }));
2967
+ return stripUndefined({
2968
+ request_id: exportLookup.requestId,
2969
+ upstream_request_id: exportLookup.upstreamRequestId,
2970
+ fetched_at: exportLookup.fetchedAt,
2971
+ cache: exportLookup.cache,
2972
+ source_requests: exportLookup.sourceRequests,
2973
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
2974
+ program: formatProgram(indexFallback.program, "compact"),
2975
+ scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
2976
+ meta: stripUndefined({
2977
+ total_active_targets: exportLookup.rawTargets.length,
2978
+ total_after_filters: targets.length,
2979
+ target_list_mode: input.target_list_mode,
2980
+ group_limit: input.group_limit,
2981
+ target_source: exportLookup.source,
2982
+ target_list_unavailable: exportLookup.unavailable,
2983
+ fallback_error: exportLookup.error
2984
+ })
2985
+ });
2986
+ }
2649
2987
  async function getProgramTargetBreakdown(client, config, input) {
2650
2988
  const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2651
2989
  const targetsData = readObject(targetsApi.data);
@@ -2692,6 +3030,42 @@ async function getProgramTargetBreakdown(client, config, input) {
2692
3030
  }
2693
3031
  };
2694
3032
  }
3033
+ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback, input) {
3034
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
3035
+ const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
3036
+ include_out_of_scope: input.include_out_of_scope,
3037
+ include_ineligible: input.include_ineligible,
3038
+ strict_scope_filter: false
3039
+ }));
3040
+ const scope = buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible);
3041
+ return stripUndefined({
3042
+ request_id: exportLookup.requestId,
3043
+ upstream_request_id: exportLookup.upstreamRequestId,
3044
+ fetched_at: exportLookup.fetchedAt,
3045
+ cache: exportLookup.cache,
3046
+ source_requests: exportLookup.sourceRequests,
3047
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
3048
+ program: formatProgram(indexFallback.program, "compact"),
3049
+ breakdown: {
3050
+ target_counts: readObject(scope.target_counts),
3051
+ by_target_type: countTargetValues(targets, (target) => stringField(target, "target_type")),
3052
+ by_scope_tag: countTargetValues(targets, (target) => readStringArrayField(target, "scope_tags")),
3053
+ by_language_tag: countTargetValues(targets, (target) => readStringArrayField(target, "language_tags")),
3054
+ group_counts: readObject(scope.group_counts),
3055
+ group_has_more: readObject(scope.group_has_more),
3056
+ groups: readObject(scope.groups)
3057
+ },
3058
+ meta: stripUndefined({
3059
+ total_active_targets: exportLookup.rawTargets.length,
3060
+ total_after_filters: targets.length,
3061
+ target_list_mode: input.target_list_mode,
3062
+ group_limit: input.group_limit,
3063
+ target_source: exportLookup.source,
3064
+ target_list_unavailable: exportLookup.unavailable,
3065
+ fallback_error: exportLookup.error
3066
+ })
3067
+ });
3068
+ }
2695
3069
  async function getProgramScopeDelta(client, config, input) {
2696
3070
  const programApi = await client.getProgram(input.program_id);
2697
3071
  const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
@@ -2903,6 +3277,77 @@ async function checkProgramNewTargets(client, config, input) {
2903
3277
  });
2904
3278
  return programLookup.fallback ? withProgramIdFallbackMetadata(payload, programLookup.fallback) : payload;
2905
3279
  }
3280
+ async function checkProgramNewTargetsFromIndexedProgram(client, config, input, indexedProgram) {
3281
+ const programId = input.program_id;
3282
+ const handle = programSearchText(programId);
3283
+ const api = await client.getRecentChanges({
3284
+ change_type: "added",
3285
+ include_removed: false,
3286
+ include_ineligible: input.include_ineligible,
3287
+ include_out_of_scope: false,
3288
+ search: handle.length >= 2 ? handle : programId,
3289
+ tags: input.target_type ? [input.target_type] : [],
3290
+ page: 1,
3291
+ page_size: input.page_size
3292
+ });
3293
+ const data = readObject(api.data);
3294
+ const upstreamMeta = readObject(sanitizeJson(data?.meta));
3295
+ const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
3296
+ const changes = readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
3297
+ const matchingChanges = changes
3298
+ .filter((change) => {
3299
+ const target = readObject(change.target);
3300
+ const changedAt = timestampField(change, "changed_at");
3301
+ return (stringField(change, "change_type") === "added" &&
3302
+ stringField(readObject(change.program), "id") === programId &&
3303
+ target !== undefined &&
3304
+ (sinceTimestamp === undefined || changedAt >= sinceTimestamp) &&
3305
+ (!input.target_type || targetMatchesRequestedType(target, input.target_type)) &&
3306
+ targetHasAllowedScope(target, {
3307
+ include_out_of_scope: false,
3308
+ include_ineligible: input.include_ineligible,
3309
+ strict_scope_filter: true
3310
+ }));
3311
+ })
3312
+ .sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
3313
+ const limitedChanges = matchingChanges.slice(0, input.max_targets);
3314
+ const targets = limitedChanges
3315
+ .map((change) => readObject(change.target))
3316
+ .filter((target) => target !== undefined);
3317
+ const latest = matchingChanges[0];
3318
+ return stripUndefined({
3319
+ request_id: randomUUID(),
3320
+ source_requests: [
3321
+ {
3322
+ source: "recent_changes",
3323
+ request_id: api.requestId,
3324
+ upstream_request_id: api.upstreamRequestId,
3325
+ ...apiSourceMetadata(api)
3326
+ }
3327
+ ],
3328
+ program_id: programId,
3329
+ program: latest
3330
+ ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact")
3331
+ : formatProgram(indexedProgram, "compact"),
3332
+ has_new_targets: matchingChanges.length > 0,
3333
+ new_target_count: matchingChanges.length,
3334
+ latest_added_at: latest ? stringField(latest, "changed_at") : undefined,
3335
+ new_targets: formatTargetList(targets, input.target_list_mode),
3336
+ meta: stripUndefined({
3337
+ recent_changes_scanned: changes.length,
3338
+ returned: targets.length,
3339
+ has_more: matchingChanges.length > targets.length,
3340
+ since: input.since,
3341
+ target_type: input.target_type,
3342
+ in_scope_only: true,
3343
+ include_ineligible: input.include_ineligible,
3344
+ target_list_mode: input.target_list_mode,
3345
+ program_detail_source: "program_index",
3346
+ upstream_total_pages: readNumber(upstreamMeta?.total_pages),
3347
+ scan_may_be_incomplete: (readNumber(upstreamMeta?.total_pages) ?? 1) > 1
3348
+ })
3349
+ });
3350
+ }
2906
3351
  async function checkWatchlistNewTargets(client, config, input) {
2907
3352
  const watchlist = new Set(input.program_ids);
2908
3353
  const api = await client.getRecentChanges({
@@ -3573,8 +4018,9 @@ function candidateLooksLikeVdp(candidate) {
3573
4018
  return candidateHasKnownNoBounty(candidate) || tagIncludesAnyNormalized(candidate.normalizedSearchSignals, VDP_SIGNALS);
3574
4019
  }
3575
4020
  function programResolutionMatch(candidate, query) {
3576
- const normalizedQuery = normalizeTag(query);
3577
- const queryTokens = tokenSet(normalizedQuery);
4021
+ const queryVariants = programResolutionQueryVariants(query);
4022
+ const primaryQuery = queryVariants[0] ?? normalizeTag(query);
4023
+ const queryTokens = meaningfulProgramQueryTokens(query);
3578
4024
  const fields = [
3579
4025
  { name: "id", value: stringField(candidate.program, "id") },
3580
4026
  { name: "handle", value: stringField(candidate.program, "handle") },
@@ -3592,16 +4038,23 @@ function programResolutionMatch(candidate, query) {
3592
4038
  }
3593
4039
  const normalizedValue = normalizeTag(field.value);
3594
4040
  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`);
4041
+ for (const queryVariant of queryVariants) {
4042
+ if (normalizedValue === queryVariant) {
4043
+ score = Math.max(score, field.name === "id" || field.name === "handle" ? 100 : 95);
4044
+ matchedFields.add(field.name);
4045
+ reasons.add(queryVariant === primaryQuery ? `${field.name} exact match` : `${field.name} exact meaningful-query match`);
4046
+ continue;
4047
+ }
4048
+ if (normalizedValue.includes(queryVariant)) {
4049
+ const containsScore = field.name === "name" || field.name === "handle"
4050
+ ? queryVariant.includes("-") || queryVariant.includes(" ")
4051
+ ? 90
4052
+ : 85
4053
+ : 70;
4054
+ matchedFields.add(field.name);
4055
+ score = Math.max(score, containsScore);
4056
+ reasons.add(queryVariant === primaryQuery ? `${field.name} contains query` : `${field.name} contains meaningful query`);
4057
+ }
3605
4058
  }
3606
4059
  const tokenOverlap = [...queryTokens].filter((token) => fieldTokens.has(token)).length;
3607
4060
  if (tokenOverlap > 0) {
@@ -3611,10 +4064,12 @@ function programResolutionMatch(candidate, query) {
3611
4064
  reasons.add(`${field.name} token match`);
3612
4065
  }
3613
4066
  }
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");
4067
+ for (const queryVariant of queryVariants) {
4068
+ if (candidate.normalizedSearchSignals.some((signal) => signal.includes(queryVariant))) {
4069
+ score = Math.max(score, queryVariant === primaryQuery ? 55 : 65);
4070
+ matchedFields.add("search_signals");
4071
+ reasons.add(queryVariant === primaryQuery ? "program search signals contain query" : "program search signals contain meaningful query");
4072
+ }
3618
4073
  }
3619
4074
  return {
3620
4075
  score,
@@ -3622,6 +4077,25 @@ function programResolutionMatch(candidate, query) {
3622
4077
  reasons: [...reasons]
3623
4078
  };
3624
4079
  }
4080
+ function programResolutionSearchQueries(query) {
4081
+ return programResolutionQueryVariants(query).slice(0, MAX_RESOLVE_SEARCH_QUERIES);
4082
+ }
4083
+ function programResolutionQueryVariants(query) {
4084
+ const normalized = normalizeTag(query);
4085
+ const meaningfulTokens = [...meaningfulProgramQueryTokens(query)];
4086
+ const meaningfulPhrase = meaningfulTokens.join(" ");
4087
+ const values = [
4088
+ normalized,
4089
+ meaningfulPhrase,
4090
+ meaningfulTokens.join("-"),
4091
+ ...meaningfulTokens
4092
+ ];
4093
+ return uniqueStrings(values.filter((value) => value.length >= 2));
4094
+ }
4095
+ function meaningfulProgramQueryTokens(query) {
4096
+ const tokens = [...tokenSet(normalizeTag(query))].filter((token) => !PROGRAM_RESOLUTION_STOP_WORDS.has(token));
4097
+ return new Set(tokens.length > 0 ? tokens : [...tokenSet(normalizeTag(query))]);
4098
+ }
3625
4099
  function normalizedCandidateValues(program, normalizedScopeTags) {
3626
4100
  return [
3627
4101
  stringField(program, "id"),
@@ -3672,9 +4146,14 @@ function programHandleText(programId) {
3672
4146
  const colonIndex = programId.indexOf(":");
3673
4147
  return colonIndex >= 0 ? programId.slice(colonIndex + 1) : programId;
3674
4148
  }
4149
+ function programPlatformText(programId) {
4150
+ const colonIndex = programId.indexOf(":");
4151
+ return colonIndex >= 0 ? programId.slice(0, colonIndex) : "";
4152
+ }
3675
4153
  function normalizeFindProgramsInput(input) {
3676
4154
  const warnings = [];
3677
4155
  const normalized = { ...input };
4156
+ normalized.vdp_mode ??= "exclude";
3678
4157
  normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
3679
4158
  normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
3680
4159
  normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
@@ -3805,6 +4284,55 @@ async function collectPrograms(client, options) {
3805
4284
  totalPagesBySource
3806
4285
  };
3807
4286
  }
4287
+ async function collectProgramsForResolution(client, options) {
4288
+ const collections = [];
4289
+ const queries = options.searchQueries.length > 0 ? options.searchQueries : [undefined];
4290
+ for (const search of queries) {
4291
+ if (options.budget.remaining <= 0) {
4292
+ addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all program name search variants could be checked.");
4293
+ break;
4294
+ }
4295
+ collections.push(await collectPrograms(client, {
4296
+ search,
4297
+ platforms: options.platforms,
4298
+ tags: options.tags,
4299
+ updated_since: undefined,
4300
+ opportunity_levels: options.opportunity_levels,
4301
+ max_pages: options.max_pages,
4302
+ budget: options.budget,
4303
+ warnings: options.warnings
4304
+ }));
4305
+ }
4306
+ if (collections.length === 0) {
4307
+ return {
4308
+ programs: [],
4309
+ sourceRequests: [],
4310
+ requestsScanned: 0,
4311
+ programsScanned: 0,
4312
+ totalPagesBySource: {}
4313
+ };
4314
+ }
4315
+ return mergeProgramCollections(collections);
4316
+ }
4317
+ function mergeProgramCollections(collections) {
4318
+ const programs = new Map();
4319
+ const totalPagesBySource = {};
4320
+ for (const collection of collections) {
4321
+ for (const program of collection.programs) {
4322
+ programs.set(programKey(program, programs.size), program);
4323
+ }
4324
+ for (const [sourceName, totalPages] of Object.entries(collection.totalPagesBySource)) {
4325
+ totalPagesBySource[sourceName] = totalPages;
4326
+ }
4327
+ }
4328
+ return {
4329
+ programs: [...programs.values()],
4330
+ sourceRequests: collections.flatMap((collection) => collection.sourceRequests),
4331
+ requestsScanned: collections.reduce((total, collection) => total + collection.requestsScanned, 0),
4332
+ programsScanned: programs.size,
4333
+ totalPagesBySource
4334
+ };
4335
+ }
3808
4336
  async function collectProgramSources(client, sources, options) {
3809
4337
  const pages = [];
3810
4338
  await mapWithConcurrency(sources.map((source, sourceIndex) => ({ source, sourceIndex })), PROGRAM_COLLECTION_CONCURRENCY, async ({ source, sourceIndex }) => {
@@ -3849,6 +4377,7 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
3849
4377
  const runWithSlot = options.runWithProgramPageSlot ?? runImmediately;
3850
4378
  return runWithSlot(async () => {
3851
4379
  const query = toQuery({
4380
+ search: options.search,
3852
4381
  platforms: options.platforms,
3853
4382
  tags: options.tags,
3854
4383
  updated_since: options.updated_since,
@@ -3864,14 +4393,16 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
3864
4393
  const data = readObject(api.data);
3865
4394
  const meta = readObject(data?.meta);
3866
4395
  const sourceName = source.kind === "opportunity" ? `opportunities:${source.level}` : "programs";
4396
+ const labeledSourceName = options.search ? `${sourceName}:search:${normalizeTag(options.search)}` : sourceName;
3867
4397
  return {
3868
- sourceName,
4398
+ sourceName: labeledSourceName,
3869
4399
  sourceIndex,
3870
4400
  page,
3871
4401
  programs: readArray(data?.programs),
3872
4402
  totalPages: readNumber(meta?.total_pages),
3873
4403
  sourceRequest: stripUndefined({
3874
- source: sourceName,
4404
+ source: labeledSourceName,
4405
+ search: options.search,
3875
4406
  request_id: api.requestId,
3876
4407
  upstream_request_id: api.upstreamRequestId,
3877
4408
  ...apiSourceMetadata(api),
@@ -3994,13 +4525,7 @@ function programCandidateMatches(candidate, filters) {
3994
4525
  if (filters.contest_like === true && !candidateLooksContestLike(candidate)) {
3995
4526
  return false;
3996
4527
  }
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)) {
4528
+ if (!candidateMatchesVdpMode(candidate, filters.vdp_mode ?? "exclude")) {
4004
4529
  return false;
4005
4530
  }
4006
4531
  if (filters.fresh_launch_days !== undefined) {
@@ -4061,6 +4586,18 @@ function compareProgramCandidates(left, right, sortBy) {
4061
4586
  right.firstSeenTime - left.firstSeenTime ||
4062
4587
  right.lastUpdatedTime - left.lastUpdatedTime);
4063
4588
  }
4589
+ function candidateMatchesVdpMode(candidate, vdpMode) {
4590
+ if (vdpMode === "exclude") {
4591
+ return !candidateLooksLikeVdp(candidate);
4592
+ }
4593
+ if (vdpMode === "only_likely") {
4594
+ return candidateLooksLikeVdp(candidate);
4595
+ }
4596
+ if (vdpMode === "only_known_no_bounty") {
4597
+ return candidateHasKnownNoBounty(candidate);
4598
+ }
4599
+ return true;
4600
+ }
4064
4601
  async function collectTargetSamples(client, candidates, input, budget) {
4065
4602
  const samples = {};
4066
4603
  const sampleCandidates = candidates.slice(0, input.target_sample_programs);
@@ -4263,7 +4800,7 @@ function registerPrompts(server) {
4263
4800
 
4264
4801
  Find the best BBRadar program candidates.
4265
4802
  - 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.
4803
+ - 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
4804
  - Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
4268
4805
  - Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
4269
4806
  - Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.
@@ -4273,16 +4810,16 @@ Find the best BBRadar program candidates.
4273
4810
  - Stay passive-only; do not recommend scanning, probing, exploit attempts, or direct contact with targets.`));
4274
4811
  server.registerPrompt("summarize_program_scope", {
4275
4812
  title: "Summarize Program Scope",
4276
- description: "Summarize in-scope and out-of-scope BBRadar target data for one program.",
4813
+ description: "Summarize BBRadar target data for one program.",
4277
4814
  argsSchema: {
4278
4815
  program_id: programIdSchema
4279
4816
  }
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}.
4817
+ }, (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
4818
 
4282
4819
  Summarize the program scope:
4283
4820
  - Program name, platform, reward range, public report count, first seen date, last updated date, and BBRadar URL.
4284
4821
  - In-scope bounty-eligible target categories.
4285
- - Out-of-scope or ineligible target categories.
4822
+ - Out-of-scope or ineligible target categories only if explicitly requested.
4286
4823
  - Wildcards, high-severity target labels, and notable language or scope tags.
4287
4824
  - Recent target update signals if present.
4288
4825
  - Do not contact, scan, probe, or validate any listed target.`));