@bbradar/mcp 0.1.5 → 0.1.7

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
@@ -92,10 +92,9 @@ const filterValueSchema = z
92
92
  message: "Filter values cannot contain commas or control characters."
93
93
  });
94
94
  const stringListSchema = z
95
- .array(filterValueSchema)
96
- .max(50)
97
- .default([])
95
+ .preprocess(normalizeStringListInput, z.array(filterValueSchema).max(50).default([]))
98
96
  .describe("Filter values.");
97
+ const acceptedTargetLimitSchema = z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM);
99
98
  const searchTextSchema = z
100
99
  .string()
101
100
  .trim()
@@ -329,7 +328,7 @@ export function createBbradarServer(client, config) {
329
328
  include_out_of_scope: z.boolean().default(false),
330
329
  include_ineligible: z.boolean().default(false),
331
330
  page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
332
- max_targets: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(25),
331
+ max_targets: acceptedTargetLimitSchema.default(25),
333
332
  group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
334
333
  target_list_mode: targetListModeSchema.default("identifiers"),
335
334
  max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
@@ -486,7 +485,7 @@ export function createBbradarServer(client, config) {
486
485
  include_out_of_scope: z.boolean().default(false),
487
486
  include_ineligible: z.boolean().default(false),
488
487
  page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
489
- max_targets: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50),
488
+ max_targets: acceptedTargetLimitSchema.default(50),
490
489
  target_list_mode: targetListModeSchema.default("identifiers")
491
490
  },
492
491
  outputSchema: {
@@ -498,7 +497,7 @@ export function createBbradarServer(client, config) {
498
497
  meta: jsonRecordSchema.optional()
499
498
  },
500
499
  annotations: readOnlyAnnotations
501
- }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeDelta(client, config, { ...args, program_id: programId }))));
500
+ }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeDelta(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramScopeDeltaFromIndexedProgram(client, config, { ...args, program_id: indexFallback.resolvedProgramId }, indexFallback.program))));
502
501
  server.registerTool("get_recent_target_activity", {
503
502
  title: "Get Recent Target Activity",
504
503
  description: "Recent target changes grouped by program.",
@@ -534,7 +533,7 @@ export function createBbradarServer(client, config) {
534
533
  target_type: filterValueSchema.optional(),
535
534
  include_ineligible: z.boolean().default(false).describe("Include ineligible."),
536
535
  page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
537
- max_targets: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(25),
536
+ max_targets: acceptedTargetLimitSchema.default(25),
538
537
  target_list_mode: targetListModeSchema.default("identifiers")
539
538
  },
540
539
  outputSchema: {
@@ -1366,24 +1365,33 @@ export function createBbradarServer(client, config) {
1366
1365
  },
1367
1366
  outputSchema: {
1368
1367
  ...apiEnvelopeOutputShape,
1369
- export: z.unknown().optional()
1368
+ export: z.unknown().optional(),
1369
+ meta: jsonRecordSchema.optional()
1370
1370
  },
1371
1371
  annotations: readOnlyAnnotations
1372
1372
  }, (args) => runTool("export_targets", config, rateLimiter, async () => {
1373
- const api = await client.exportTargets(compactRecord(args));
1374
- const exportId = randomUUID();
1375
- const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
1376
- const resourceUri = exportResourceUri(exportId);
1377
- rememberExport(exportStore, exportId, sanitizedExport);
1378
- return withApiMetadata(api, {
1379
- export: {
1380
- export_id: exportId,
1381
- resource_uri: resourceUri,
1382
- preview: previewExportPayload(sanitizedExport),
1383
- returned_preview_count: previewExportCount(sanitizedExport),
1384
- limit: args.limit
1373
+ try {
1374
+ const api = await client.exportTargets(compactRecord(args));
1375
+ const exportId = randomUUID();
1376
+ const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
1377
+ const resourceUri = exportResourceUri(exportId);
1378
+ rememberExport(exportStore, exportId, sanitizedExport);
1379
+ return withApiMetadata(api, {
1380
+ export: {
1381
+ export_id: exportId,
1382
+ resource_uri: resourceUri,
1383
+ preview: previewExportPayload(sanitizedExport),
1384
+ returned_preview_count: previewExportCount(sanitizedExport),
1385
+ limit: args.limit
1386
+ }
1387
+ });
1388
+ }
1389
+ catch (error) {
1390
+ if (!(error instanceof BBRadarApiError) || (error.status !== 404 && error.status !== 405)) {
1391
+ throw error;
1385
1392
  }
1386
- });
1393
+ return exportTargetsFromProgramTargetsFallback(client, config, exportStore, args, error);
1394
+ }
1387
1395
  }));
1388
1396
  registerPrompts(server);
1389
1397
  registerResources(server, client, config, exportStore);
@@ -1526,8 +1534,27 @@ async function runProgramIdTool(client, config, requestedProgramId, callback, in
1526
1534
  const payload = await indexFallbackCallback(indexFallback);
1527
1535
  return withProgramIndexFallbackMetadata(payload, indexFallback);
1528
1536
  }
1529
- const payload = await callback(fallback.resolvedProgramId);
1530
- return withProgramIdFallbackMetadata(payload, fallback);
1537
+ try {
1538
+ const payload = await callback(fallback.resolvedProgramId);
1539
+ return withProgramIdFallbackMetadata(payload, fallback);
1540
+ }
1541
+ catch (fallbackError) {
1542
+ if (!isProgramNotFoundError(fallbackError) || !indexFallbackCallback) {
1543
+ throw fallbackError;
1544
+ }
1545
+ const indexFallback = await resolveIndexedProgramId(client, config, fallback.resolvedProgramId, fallbackError);
1546
+ if (!indexFallback) {
1547
+ throw fallbackError;
1548
+ }
1549
+ const combinedFallback = {
1550
+ ...indexFallback,
1551
+ requestedProgramId,
1552
+ sourceRequests: [...fallback.sourceRequests, ...indexFallback.sourceRequests],
1553
+ warnings: uniqueStrings([...fallback.warnings, ...indexFallback.warnings])
1554
+ };
1555
+ const payload = await indexFallbackCallback(combinedFallback);
1556
+ return withProgramIndexFallbackMetadata(payload, combinedFallback);
1557
+ }
1531
1558
  }
1532
1559
  }
1533
1560
  async function resolveIndexedProgramId(client, config, requestedProgramId, staleError) {
@@ -1602,6 +1629,7 @@ async function resolveStaleProgramId(client, config, requestedProgramId, staleEr
1602
1629
  function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1603
1630
  const requestedHandle = normalizeTag(programHandleText(requestedProgramId));
1604
1631
  const requestedSearchText = normalizeTag(programSearchText(requestedProgramId));
1632
+ const requestedPlatform = normalizeTag(programPlatformText(requestedProgramId));
1605
1633
  const candidates = readArray(resolution.matches)
1606
1634
  .map((entry) => readObject(entry))
1607
1635
  .filter((entry) => entry !== undefined)
@@ -1610,17 +1638,29 @@ function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1610
1638
  const resolvedProgramId = stringField(entry, "program_id") ?? stringField(program, "id");
1611
1639
  const candidateHandle = normalizeTag(stringField(program, "handle") ?? "");
1612
1640
  const candidateSearchText = resolvedProgramId ? normalizeTag(programSearchText(resolvedProgramId)) : "";
1641
+ const candidatePlatform = normalizeTag(stringField(program, "platform") ?? (resolvedProgramId ? programPlatformText(resolvedProgramId) : ""));
1613
1642
  const score = readNumber(entry.score) ?? 0;
1614
1643
  const exactHandleMatch = candidateHandle === requestedHandle ||
1615
1644
  candidateHandle === requestedSearchText ||
1616
1645
  candidateSearchText === requestedSearchText;
1646
+ const partialHandleMatch = requestedHandle.length >= 4 &&
1647
+ requestedPlatform.length > 0 &&
1648
+ candidatePlatform === requestedPlatform &&
1649
+ (candidateHandle.startsWith(`${requestedHandle}_`) ||
1650
+ candidateHandle.startsWith(`${requestedHandle}-`) ||
1651
+ candidateSearchText.startsWith(`${requestedSearchText}_`) ||
1652
+ candidateSearchText.startsWith(`${requestedSearchText}-`));
1617
1653
  return {
1618
1654
  resolvedProgramId,
1619
1655
  score,
1620
- exactHandleMatch
1656
+ exactHandleMatch,
1657
+ partialHandleMatch
1621
1658
  };
1622
1659
  })
1623
- .filter((entry) => entry.resolvedProgramId !== undefined && entry.resolvedProgramId !== requestedProgramId && entry.exactHandleMatch && entry.score >= 80)
1660
+ .filter((entry) => entry.resolvedProgramId !== undefined &&
1661
+ normalizeTag(entry.resolvedProgramId) !== normalizeTag(requestedProgramId) &&
1662
+ (entry.exactHandleMatch || entry.partialHandleMatch) &&
1663
+ entry.score >= 80)
1624
1664
  .sort((left, right) => right.score - left.score || left.resolvedProgramId.localeCompare(right.resolvedProgramId));
1625
1665
  if (candidates.length === 0) {
1626
1666
  return undefined;
@@ -1650,11 +1690,15 @@ function selectProgramIndexFallbackMatch(requestedProgramId, resolution) {
1650
1690
  const platformHandleMatch = requestedPlatform.length > 0 &&
1651
1691
  candidatePlatform === requestedPlatform &&
1652
1692
  candidateHandle === requestedHandle;
1693
+ const partialPlatformHandleMatch = requestedHandle.length >= 4 &&
1694
+ requestedPlatform.length > 0 &&
1695
+ candidatePlatform === requestedPlatform &&
1696
+ (candidateHandle.startsWith(`${requestedHandle}_`) || candidateHandle.startsWith(`${requestedHandle}-`));
1653
1697
  return {
1654
1698
  resolvedProgramId,
1655
1699
  program,
1656
1700
  score,
1657
- exactMatch: idMatch || platformHandleMatch
1701
+ exactMatch: idMatch || platformHandleMatch || partialPlatformHandleMatch
1658
1702
  };
1659
1703
  })
1660
1704
  .filter((entry) => entry.resolvedProgramId !== undefined && entry.program !== undefined && entry.exactMatch && entry.score >= 80)
@@ -2791,7 +2835,7 @@ async function getProgramTargetsPayload(client, input, programId) {
2791
2835
  });
2792
2836
  }
2793
2837
  async function getProgramTargetsExportFallbackPayload(client, input, programId) {
2794
- const exportLookup = await fetchProgramTargetsFromExportFallback(client, programId);
2838
+ const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, programId);
2795
2839
  const sanitizedTargets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, input));
2796
2840
  const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
2797
2841
  return stripUndefined({
@@ -2817,6 +2861,42 @@ async function getProgramTargetsExportFallbackPayload(client, input, programId)
2817
2861
  })
2818
2862
  });
2819
2863
  }
2864
+ async function fetchProgramTargetsWithEndpointFallback(client, programId) {
2865
+ try {
2866
+ const api = await client.getProgramTargets(programId);
2867
+ const data = readObject(api.data);
2868
+ const rawTargets = readArray(data?.targets).map(sanitizeTarget);
2869
+ return {
2870
+ requestId: api.requestId,
2871
+ upstreamRequestId: api.upstreamRequestId,
2872
+ fetchedAt: api.fetchedAt,
2873
+ cache: readObject(stripUndefined({
2874
+ hit: api.cached,
2875
+ coalesced_live_request: api.coalesced,
2876
+ expires_at: api.cacheExpiresAt
2877
+ })),
2878
+ source: "program_targets",
2879
+ rawTargets,
2880
+ sourceRequests: [
2881
+ {
2882
+ source: "program_targets",
2883
+ program_id: programId,
2884
+ request_id: api.requestId,
2885
+ upstream_request_id: api.upstreamRequestId,
2886
+ ...apiSourceMetadata(api)
2887
+ }
2888
+ ],
2889
+ warnings: [],
2890
+ unavailable: false
2891
+ };
2892
+ }
2893
+ catch (error) {
2894
+ if (!isProgramNotFoundError(error)) {
2895
+ throw error;
2896
+ }
2897
+ return fetchProgramTargetsFromExportFallback(client, programId);
2898
+ }
2899
+ }
2820
2900
  async function fetchProgramTargetsFromExportFallback(client, programId) {
2821
2901
  try {
2822
2902
  const api = await client.exportTargets({
@@ -2872,6 +2952,116 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
2872
2952
  };
2873
2953
  }
2874
2954
  }
2955
+ async function exportTargetsFromProgramTargetsFallback(client, config, exportStore, input, exportError) {
2956
+ const warnings = ["Target export endpoint is unavailable; assembled a fallback export from per-program target endpoints."];
2957
+ const sourceRequests = [
2958
+ {
2959
+ source: "target_export",
2960
+ failed: true,
2961
+ request_id: exportError.requestId,
2962
+ upstream_request_id: exportError.upstreamRequestId,
2963
+ status: exportError.status
2964
+ }
2965
+ ];
2966
+ const programIds = input.program_ids.length > 0 ? input.program_ids : await discoverExportProgramIds(client, input, sourceRequests, warnings);
2967
+ const targets = [];
2968
+ const errors = [];
2969
+ for (const programId of programIds) {
2970
+ if (targets.length >= input.limit) {
2971
+ break;
2972
+ }
2973
+ try {
2974
+ const payload = await runProgramIdTool(client, config, programId, (resolvedProgramId) => getProgramTargetsPayload(client, {
2975
+ include_out_of_scope: input.include_out_of_scope,
2976
+ include_ineligible: input.include_ineligible,
2977
+ strict_scope_filter: false,
2978
+ offset: 0,
2979
+ limit: input.limit - targets.length,
2980
+ output_mode: "full"
2981
+ }, resolvedProgramId), (indexFallback) => getProgramTargetsExportFallbackPayload(client, {
2982
+ include_out_of_scope: input.include_out_of_scope,
2983
+ include_ineligible: input.include_ineligible,
2984
+ strict_scope_filter: false,
2985
+ offset: 0,
2986
+ limit: input.limit - targets.length,
2987
+ output_mode: "full"
2988
+ }, indexFallback.resolvedProgramId));
2989
+ const resolvedProgramId = stringField(payload, "program_id") ?? programId;
2990
+ sourceRequests.push(...readArray(payload.source_requests).filter((request) => readObject(request) !== undefined));
2991
+ warnings.push(...readArray(payload.warnings).filter((warning) => typeof warning === "string"));
2992
+ for (const target of readArray(payload.targets)) {
2993
+ const object = readObject(target);
2994
+ if (!object) {
2995
+ continue;
2996
+ }
2997
+ targets.push({ program_id: resolvedProgramId, ...object });
2998
+ if (targets.length >= input.limit) {
2999
+ break;
3000
+ }
3001
+ }
3002
+ }
3003
+ catch (error) {
3004
+ errors.push(apiOperationError(programId, error));
3005
+ }
3006
+ }
3007
+ if (input.format === "csv") {
3008
+ warnings.push("Fallback target export returns JSON even when CSV was requested.");
3009
+ }
3010
+ const exportPayload = stripUndefined({
3011
+ targets,
3012
+ meta: {
3013
+ export_source: "program_targets_fallback",
3014
+ requested_program_count: programIds.length,
3015
+ returned: targets.length,
3016
+ limit: input.limit,
3017
+ format: "json"
3018
+ },
3019
+ generated_at: new Date().toISOString()
3020
+ });
3021
+ const exportId = randomUUID();
3022
+ const resourceUri = exportResourceUri(exportId);
3023
+ rememberExport(exportStore, exportId, exportPayload);
3024
+ return stripUndefined({
3025
+ request_id: randomUUID(),
3026
+ source_requests: sourceRequests,
3027
+ warnings: uniqueStrings(warnings),
3028
+ errors: errors.length > 0 ? errors : undefined,
3029
+ export: {
3030
+ export_id: exportId,
3031
+ resource_uri: resourceUri,
3032
+ preview: previewExportPayload(exportPayload),
3033
+ returned_preview_count: previewExportCount(exportPayload),
3034
+ limit: input.limit
3035
+ },
3036
+ meta: stripUndefined({
3037
+ export_source: "program_targets_fallback",
3038
+ requested_program_count: programIds.length,
3039
+ returned: targets.length,
3040
+ failed_program_count: errors.length,
3041
+ fallback_error: apiOperationError("target_export", exportError)
3042
+ })
3043
+ });
3044
+ }
3045
+ async function discoverExportProgramIds(client, input, sourceRequests, warnings) {
3046
+ const budget = {
3047
+ initial: 1,
3048
+ remaining: 1
3049
+ };
3050
+ const collected = await collectPrograms(client, {
3051
+ platforms: input.platforms,
3052
+ tags: input.tags,
3053
+ updated_since: undefined,
3054
+ opportunity_levels: input.opportunity_levels,
3055
+ max_pages: 1,
3056
+ budget,
3057
+ warnings
3058
+ });
3059
+ sourceRequests.push(...collected.sourceRequests);
3060
+ return uniqueStrings(collected.programs
3061
+ .map((program) => stringField(sanitizeProgram(program, ""), "id"))
3062
+ .filter((programId) => programId !== undefined)
3063
+ .slice(0, input.limit));
3064
+ }
2875
3065
  function readExportTargetRows(data) {
2876
3066
  if (Array.isArray(data)) {
2877
3067
  return data;
@@ -2922,7 +3112,7 @@ async function getProgramScopeSummary(client, config, input) {
2922
3112
  };
2923
3113
  }
2924
3114
  async function getProgramScopeSummaryFromIndexFallback(client, indexFallback, input) {
2925
- const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
3115
+ const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, input.program_id);
2926
3116
  const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
2927
3117
  include_out_of_scope: input.include_out_of_scope,
2928
3118
  include_ineligible: input.include_ineligible,
@@ -2995,7 +3185,7 @@ async function getProgramTargetBreakdown(client, config, input) {
2995
3185
  };
2996
3186
  }
2997
3187
  async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback, input) {
2998
- const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
3188
+ const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, input.program_id);
2999
3189
  const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
3000
3190
  include_out_of_scope: input.include_out_of_scope,
3001
3191
  include_ineligible: input.include_ineligible,
@@ -3033,19 +3223,8 @@ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback,
3033
3223
  async function getProgramScopeDelta(client, config, input) {
3034
3224
  const programApi = await client.getProgram(input.program_id);
3035
3225
  const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
3036
- const changesPayload = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope);
3037
- const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
3038
- const filteredChanges = changesPayload.changes
3039
- .filter((change) => !input.change_type || stringField(change, "change_type") === input.change_type)
3040
- .filter((change) => sinceTimestamp === undefined || timestampField(change, "changed_at") >= sinceTimestamp)
3041
- .filter((change) => {
3042
- const target = readObject(change.target);
3043
- return !input.target_type || (target !== undefined && targetMatchesRequestedType(target, input.target_type));
3044
- })
3045
- .filter((change) => {
3046
- const target = readObject(change.target);
3047
- return input.language_tags.length === 0 || (target !== undefined && targetMatchesAnySignal(target, input.language_tags));
3048
- });
3226
+ const changesPayload = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope, input.max_targets);
3227
+ const filteredChanges = filterProgramScopeDeltaChanges(changesPayload.changes, input);
3049
3228
  const limitedChanges = filteredChanges.slice(0, input.max_targets);
3050
3229
  return stripUndefined({
3051
3230
  request_id: randomUUID(),
@@ -3070,10 +3249,55 @@ async function getProgramScopeDelta(client, config, input) {
3070
3249
  change_type: input.change_type,
3071
3250
  target_type: input.target_type,
3072
3251
  language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
3073
- target_list_mode: input.target_list_mode
3252
+ target_list_mode: input.target_list_mode,
3253
+ upstream_total_pages: readNumber(changesPayload.meta?.total_pages),
3254
+ pages_fetched: readNumber(changesPayload.meta?.pages_fetched),
3255
+ scan_may_be_incomplete: (readNumber(changesPayload.meta?.total_pages) ?? 1) > (readNumber(changesPayload.meta?.pages_fetched) ?? 1)
3074
3256
  })
3075
3257
  });
3076
3258
  }
3259
+ async function getProgramScopeDeltaFromIndexedProgram(client, config, input, indexedProgram) {
3260
+ const program = addProgramResourceLinks(indexedProgram);
3261
+ const changesPayload = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope, input.max_targets);
3262
+ const filteredChanges = filterProgramScopeDeltaChanges(changesPayload.changes, input);
3263
+ const limitedChanges = filteredChanges.slice(0, input.max_targets);
3264
+ return stripUndefined({
3265
+ request_id: randomUUID(),
3266
+ source_requests: changesPayload.sourceRequests,
3267
+ program: formatProgram(program, "compact"),
3268
+ delta: buildScopeDelta(limitedChanges, input.target_list_mode),
3269
+ changes: limitedChanges.map((change) => formatChange(change, "compact")),
3270
+ meta: stripUndefined({
3271
+ recent_changes_scanned: changesPayload.changes.length,
3272
+ changes_after_filters: filteredChanges.length,
3273
+ returned: limitedChanges.length,
3274
+ has_more: filteredChanges.length > limitedChanges.length,
3275
+ since: input.since,
3276
+ change_type: input.change_type,
3277
+ target_type: input.target_type,
3278
+ language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
3279
+ target_list_mode: input.target_list_mode,
3280
+ program_detail_source: "program_index",
3281
+ upstream_total_pages: readNumber(changesPayload.meta?.total_pages),
3282
+ pages_fetched: readNumber(changesPayload.meta?.pages_fetched),
3283
+ scan_may_be_incomplete: (readNumber(changesPayload.meta?.total_pages) ?? 1) > (readNumber(changesPayload.meta?.pages_fetched) ?? 1)
3284
+ })
3285
+ });
3286
+ }
3287
+ function filterProgramScopeDeltaChanges(changes, input) {
3288
+ const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
3289
+ return changes
3290
+ .filter((change) => !input.change_type || stringField(change, "change_type") === input.change_type)
3291
+ .filter((change) => sinceTimestamp === undefined || timestampField(change, "changed_at") >= sinceTimestamp)
3292
+ .filter((change) => {
3293
+ const target = readObject(change.target);
3294
+ return !input.target_type || (target !== undefined && targetMatchesRequestedType(target, input.target_type));
3295
+ })
3296
+ .filter((change) => {
3297
+ const target = readObject(change.target);
3298
+ return input.language_tags.length === 0 || (target !== undefined && targetMatchesAnySignal(target, input.language_tags));
3299
+ });
3300
+ }
3077
3301
  async function getRecentTargetActivity(client, config, input) {
3078
3302
  const query = toQuery({
3079
3303
  change_type: input.change_type,
@@ -3481,32 +3705,45 @@ async function getProgramDelta(client, config, input) {
3481
3705
  meta: changes.meta
3482
3706
  };
3483
3707
  }
3484
- async function fetchProgramChanges(client, config, programId, pageSize, includeRemoved, includeIneligible, includeOutOfScope) {
3708
+ async function fetchProgramChanges(client, config, programId, pageSize, includeRemoved, includeIneligible, includeOutOfScope, maxChanges = pageSize) {
3485
3709
  const handle = programSearchText(programId);
3486
- const api = await client.getRecentChanges({
3487
- search: handle.length >= 2 ? handle : programId,
3488
- include_removed: includeRemoved,
3489
- include_ineligible: includeIneligible,
3490
- include_out_of_scope: includeOutOfScope,
3491
- page: 1,
3492
- page_size: pageSize
3493
- });
3494
- const data = readObject(api.data);
3495
- const changes = readArray(data?.results)
3496
- .map((change) => sanitizeChange(change, config.webBaseUrl))
3497
- .filter((change) => stringField(readObject(change.program), "id") === programId);
3498
- return {
3499
- changes,
3500
- meta: sanitizeJson(data?.meta),
3501
- sourceRequests: [
3502
- {
3503
- source: "recent_changes",
3504
- request_id: api.requestId,
3505
- upstream_request_id: api.upstreamRequestId,
3506
- ...apiSourceMetadata(api)
3507
- }
3508
- ]
3509
- };
3710
+ const changes = [];
3711
+ const sourceRequests = [];
3712
+ let meta;
3713
+ const maxPages = Math.max(1, Math.ceil(maxChanges / pageSize));
3714
+ for (let page = 1; page <= maxPages && changes.length < maxChanges; page += 1) {
3715
+ const api = await client.getRecentChanges({
3716
+ search: handle.length >= 2 ? handle : programId,
3717
+ include_removed: includeRemoved,
3718
+ include_ineligible: includeIneligible,
3719
+ include_out_of_scope: includeOutOfScope,
3720
+ page,
3721
+ page_size: pageSize
3722
+ });
3723
+ const data = readObject(api.data);
3724
+ const pageMeta = readObject(sanitizeJson(data?.meta));
3725
+ const rows = readArray(data?.results);
3726
+ meta = {
3727
+ ...(meta ?? {}),
3728
+ ...(pageMeta ?? {}),
3729
+ pages_fetched: page
3730
+ };
3731
+ sourceRequests.push({
3732
+ source: "recent_changes",
3733
+ request_id: api.requestId,
3734
+ upstream_request_id: api.upstreamRequestId,
3735
+ page,
3736
+ ...apiSourceMetadata(api)
3737
+ });
3738
+ changes.push(...rows
3739
+ .map((change) => sanitizeChange(change, config.webBaseUrl))
3740
+ .filter((change) => stringField(readObject(change.program), "id") === programId));
3741
+ const totalPages = readNumber(pageMeta?.total_pages);
3742
+ if (rows.length === 0 || (totalPages !== undefined && page >= totalPages)) {
3743
+ break;
3744
+ }
3745
+ }
3746
+ return { changes: changes.slice(0, maxChanges), meta, sourceRequests };
3510
3747
  }
3511
3748
  function addProgramResourceLinks(program) {
3512
3749
  const id = stringField(program, "id");
@@ -4100,6 +4337,23 @@ function programFetchError(programId, error) {
4100
4337
  suggested_fix: "Retry the program lookup or verify the BBRadar program id."
4101
4338
  };
4102
4339
  }
4340
+ function apiOperationError(operation, error) {
4341
+ if (error instanceof BBRadarApiError) {
4342
+ return stripUndefined({
4343
+ operation,
4344
+ request_id: error.requestId,
4345
+ upstream_request_id: error.upstreamRequestId,
4346
+ status: error.status,
4347
+ message: error.message,
4348
+ detail: sanitizeJson(error.detail),
4349
+ errors: sanitizeJson(error.errors)
4350
+ });
4351
+ }
4352
+ return {
4353
+ operation,
4354
+ message: error instanceof Error ? error.message : String(error)
4355
+ };
4356
+ }
4103
4357
  function programSearchText(programId) {
4104
4358
  const colonIndex = programId.indexOf(":");
4105
4359
  const handle = colonIndex >= 0 ? programId.slice(colonIndex + 1) : programId;
@@ -4963,6 +5217,21 @@ function toRateLimitPayload(decision) {
4963
5217
  function rateLimitForTool(toolName, config) {
4964
5218
  return toolName === "export_targets" ? config.exportRateLimitPerMinute : config.defaultRateLimitPerMinute;
4965
5219
  }
5220
+ function normalizeStringListInput(value) {
5221
+ if (typeof value === "string") {
5222
+ return splitFilterList(value);
5223
+ }
5224
+ if (Array.isArray(value)) {
5225
+ return value.flatMap((entry) => (typeof entry === "string" ? splitFilterList(entry) : [entry]));
5226
+ }
5227
+ return value;
5228
+ }
5229
+ function splitFilterList(value) {
5230
+ return value
5231
+ .split(",")
5232
+ .map((entry) => entry.trim())
5233
+ .filter((entry) => entry.length > 0);
5234
+ }
4966
5235
  function toQuery(values) {
4967
5236
  return compactRecord(values);
4968
5237
  }