@bbradar/mcp 0.1.6 → 0.1.8
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 +512 -72
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -29,8 +29,9 @@ const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
|
|
|
29
29
|
const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
|
|
30
30
|
const MAX_LOCAL_EXPORT_RESOURCES = 25;
|
|
31
31
|
const EXPORT_PREVIEW_LIMIT = 25;
|
|
32
|
-
const STALE_PROGRAM_ID_RESOLVE_PAGES =
|
|
33
|
-
const STALE_PROGRAM_ID_RESOLVE_BUDGET =
|
|
32
|
+
const STALE_PROGRAM_ID_RESOLVE_PAGES = 1;
|
|
33
|
+
const STALE_PROGRAM_ID_RESOLVE_BUDGET = 1;
|
|
34
|
+
const PROGRAM_FALLBACK_CACHE_MAX_ENTRIES = 200;
|
|
34
35
|
const MAX_RESOLVE_SEARCH_QUERIES = 4;
|
|
35
36
|
const SDK_VERSION = "1.29.0";
|
|
36
37
|
const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
|
|
@@ -92,10 +93,9 @@ const filterValueSchema = z
|
|
|
92
93
|
message: "Filter values cannot contain commas or control characters."
|
|
93
94
|
});
|
|
94
95
|
const stringListSchema = z
|
|
95
|
-
.array(filterValueSchema)
|
|
96
|
-
.max(50)
|
|
97
|
-
.default([])
|
|
96
|
+
.preprocess(normalizeStringListInput, z.array(filterValueSchema).max(50).default([]))
|
|
98
97
|
.describe("Filter values.");
|
|
98
|
+
const acceptedTargetLimitSchema = z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM);
|
|
99
99
|
const searchTextSchema = z
|
|
100
100
|
.string()
|
|
101
101
|
.trim()
|
|
@@ -329,7 +329,7 @@ export function createBbradarServer(client, config) {
|
|
|
329
329
|
include_out_of_scope: z.boolean().default(false),
|
|
330
330
|
include_ineligible: z.boolean().default(false),
|
|
331
331
|
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
332
|
-
max_targets:
|
|
332
|
+
max_targets: acceptedTargetLimitSchema.default(25),
|
|
333
333
|
group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
|
|
334
334
|
target_list_mode: targetListModeSchema.default("identifiers"),
|
|
335
335
|
max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
@@ -486,7 +486,7 @@ export function createBbradarServer(client, config) {
|
|
|
486
486
|
include_out_of_scope: z.boolean().default(false),
|
|
487
487
|
include_ineligible: z.boolean().default(false),
|
|
488
488
|
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
489
|
-
max_targets:
|
|
489
|
+
max_targets: acceptedTargetLimitSchema.default(50),
|
|
490
490
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
491
491
|
},
|
|
492
492
|
outputSchema: {
|
|
@@ -498,7 +498,7 @@ export function createBbradarServer(client, config) {
|
|
|
498
498
|
meta: jsonRecordSchema.optional()
|
|
499
499
|
},
|
|
500
500
|
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 }))));
|
|
501
|
+
}, (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
502
|
server.registerTool("get_recent_target_activity", {
|
|
503
503
|
title: "Get Recent Target Activity",
|
|
504
504
|
description: "Recent target changes grouped by program.",
|
|
@@ -534,7 +534,7 @@ export function createBbradarServer(client, config) {
|
|
|
534
534
|
target_type: filterValueSchema.optional(),
|
|
535
535
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
536
536
|
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
537
|
-
max_targets:
|
|
537
|
+
max_targets: acceptedTargetLimitSchema.default(25),
|
|
538
538
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
539
539
|
},
|
|
540
540
|
outputSchema: {
|
|
@@ -1366,24 +1366,33 @@ export function createBbradarServer(client, config) {
|
|
|
1366
1366
|
},
|
|
1367
1367
|
outputSchema: {
|
|
1368
1368
|
...apiEnvelopeOutputShape,
|
|
1369
|
-
export: z.unknown().optional()
|
|
1369
|
+
export: z.unknown().optional(),
|
|
1370
|
+
meta: jsonRecordSchema.optional()
|
|
1370
1371
|
},
|
|
1371
1372
|
annotations: readOnlyAnnotations
|
|
1372
1373
|
}, (args) => runTool("export_targets", config, rateLimiter, async () => {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1374
|
+
try {
|
|
1375
|
+
const api = await client.exportTargets(compactRecord(args));
|
|
1376
|
+
const exportId = randomUUID();
|
|
1377
|
+
const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
|
|
1378
|
+
const resourceUri = exportResourceUri(exportId);
|
|
1379
|
+
rememberExport(exportStore, exportId, sanitizedExport);
|
|
1380
|
+
return withApiMetadata(api, {
|
|
1381
|
+
export: {
|
|
1382
|
+
export_id: exportId,
|
|
1383
|
+
resource_uri: resourceUri,
|
|
1384
|
+
preview: previewExportPayload(sanitizedExport),
|
|
1385
|
+
returned_preview_count: previewExportCount(sanitizedExport),
|
|
1386
|
+
limit: args.limit
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
catch (error) {
|
|
1391
|
+
if (!(error instanceof BBRadarApiError) || (error.status !== 404 && error.status !== 405)) {
|
|
1392
|
+
throw error;
|
|
1385
1393
|
}
|
|
1386
|
-
|
|
1394
|
+
return exportTargetsFromProgramTargetsFallback(client, config, exportStore, args, error);
|
|
1395
|
+
}
|
|
1387
1396
|
}));
|
|
1388
1397
|
registerPrompts(server);
|
|
1389
1398
|
registerResources(server, client, config, exportStore);
|
|
@@ -1480,7 +1489,19 @@ function registerResources(server, client, config, exportStore) {
|
|
|
1480
1489
|
}));
|
|
1481
1490
|
});
|
|
1482
1491
|
}
|
|
1492
|
+
const programFallbackCache = new Map();
|
|
1493
|
+
const programTargetsFallbackCaches = new WeakMap();
|
|
1483
1494
|
async function getProgramWithIdFallback(client, config, requestedProgramId) {
|
|
1495
|
+
const cachedFallback = getCachedProgramFallback(config, requestedProgramId);
|
|
1496
|
+
if (cachedFallback && !isProgramIndexFallback(cachedFallback)) {
|
|
1497
|
+
return {
|
|
1498
|
+
api: await client.getProgram(cachedFallback.resolvedProgramId),
|
|
1499
|
+
programId: cachedFallback.resolvedProgramId,
|
|
1500
|
+
sourceRequests: cachedFallback.sourceRequests,
|
|
1501
|
+
warnings: cachedFallback.warnings,
|
|
1502
|
+
fallback: cachedFallback
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1484
1505
|
try {
|
|
1485
1506
|
return {
|
|
1486
1507
|
api: await client.getProgram(requestedProgramId),
|
|
@@ -1497,6 +1518,7 @@ async function getProgramWithIdFallback(client, config, requestedProgramId) {
|
|
|
1497
1518
|
if (!fallback) {
|
|
1498
1519
|
throw error;
|
|
1499
1520
|
}
|
|
1521
|
+
rememberProgramFallback(config, fallback);
|
|
1500
1522
|
return {
|
|
1501
1523
|
api: await client.getProgram(fallback.resolvedProgramId),
|
|
1502
1524
|
programId: fallback.resolvedProgramId,
|
|
@@ -1507,6 +1529,30 @@ async function getProgramWithIdFallback(client, config, requestedProgramId) {
|
|
|
1507
1529
|
}
|
|
1508
1530
|
}
|
|
1509
1531
|
async function runProgramIdTool(client, config, requestedProgramId, callback, indexFallbackCallback) {
|
|
1532
|
+
const cachedFallback = getCachedProgramFallback(config, requestedProgramId);
|
|
1533
|
+
if (cachedFallback) {
|
|
1534
|
+
if (isProgramIndexFallback(cachedFallback) && indexFallbackCallback) {
|
|
1535
|
+
const payload = await indexFallbackCallback(cachedFallback);
|
|
1536
|
+
return withProgramIndexFallbackMetadata(payload, cachedFallback);
|
|
1537
|
+
}
|
|
1538
|
+
try {
|
|
1539
|
+
const payload = await callback(cachedFallback.resolvedProgramId);
|
|
1540
|
+
return withProgramIdFallbackMetadata(payload, cachedFallback);
|
|
1541
|
+
}
|
|
1542
|
+
catch (cachedFallbackError) {
|
|
1543
|
+
if (!isProgramNotFoundError(cachedFallbackError) || !indexFallbackCallback) {
|
|
1544
|
+
throw cachedFallbackError;
|
|
1545
|
+
}
|
|
1546
|
+
const indexFallback = createProgramIndexFallbackFromStaleResolution(cachedFallback, requestedProgramId, cachedFallbackError) ??
|
|
1547
|
+
(await resolveIndexedProgramId(client, config, cachedFallback.resolvedProgramId, cachedFallbackError));
|
|
1548
|
+
if (!indexFallback) {
|
|
1549
|
+
throw cachedFallbackError;
|
|
1550
|
+
}
|
|
1551
|
+
rememberProgramFallback(config, indexFallback);
|
|
1552
|
+
const payload = await indexFallbackCallback(indexFallback);
|
|
1553
|
+
return withProgramIndexFallbackMetadata(payload, indexFallback);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1510
1556
|
try {
|
|
1511
1557
|
return await callback(requestedProgramId);
|
|
1512
1558
|
}
|
|
@@ -1523,10 +1569,12 @@ async function runProgramIdTool(client, config, requestedProgramId, callback, in
|
|
|
1523
1569
|
if (!indexFallback) {
|
|
1524
1570
|
throw error;
|
|
1525
1571
|
}
|
|
1572
|
+
rememberProgramFallback(config, indexFallback);
|
|
1526
1573
|
const payload = await indexFallbackCallback(indexFallback);
|
|
1527
1574
|
return withProgramIndexFallbackMetadata(payload, indexFallback);
|
|
1528
1575
|
}
|
|
1529
1576
|
try {
|
|
1577
|
+
rememberProgramFallback(config, fallback);
|
|
1530
1578
|
const payload = await callback(fallback.resolvedProgramId);
|
|
1531
1579
|
return withProgramIdFallbackMetadata(payload, fallback);
|
|
1532
1580
|
}
|
|
@@ -1534,21 +1582,107 @@ async function runProgramIdTool(client, config, requestedProgramId, callback, in
|
|
|
1534
1582
|
if (!isProgramNotFoundError(fallbackError) || !indexFallbackCallback) {
|
|
1535
1583
|
throw fallbackError;
|
|
1536
1584
|
}
|
|
1537
|
-
const indexFallback =
|
|
1585
|
+
const indexFallback = createProgramIndexFallbackFromStaleResolution(fallback, requestedProgramId, fallbackError) ??
|
|
1586
|
+
(await resolveIndexedProgramId(client, config, fallback.resolvedProgramId, fallbackError));
|
|
1538
1587
|
if (!indexFallback) {
|
|
1539
1588
|
throw fallbackError;
|
|
1540
1589
|
}
|
|
1541
|
-
const combinedFallback =
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1590
|
+
const combinedFallback = indexFallback.requestedProgramId === requestedProgramId
|
|
1591
|
+
? indexFallback
|
|
1592
|
+
: {
|
|
1593
|
+
...indexFallback,
|
|
1594
|
+
requestedProgramId,
|
|
1595
|
+
sourceRequests: [...fallback.sourceRequests, ...indexFallback.sourceRequests],
|
|
1596
|
+
warnings: uniqueStrings([...fallback.warnings, ...indexFallback.warnings])
|
|
1597
|
+
};
|
|
1598
|
+
rememberProgramFallback(config, combinedFallback);
|
|
1547
1599
|
const payload = await indexFallbackCallback(combinedFallback);
|
|
1548
1600
|
return withProgramIndexFallbackMetadata(payload, combinedFallback);
|
|
1549
1601
|
}
|
|
1550
1602
|
}
|
|
1551
1603
|
}
|
|
1604
|
+
function getCachedProgramFallback(config, requestedProgramId) {
|
|
1605
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0) {
|
|
1606
|
+
return undefined;
|
|
1607
|
+
}
|
|
1608
|
+
const key = programFallbackCacheKey(config, requestedProgramId);
|
|
1609
|
+
const cached = programFallbackCache.get(key);
|
|
1610
|
+
if (!cached) {
|
|
1611
|
+
return undefined;
|
|
1612
|
+
}
|
|
1613
|
+
if (cached.expiresAt <= Date.now()) {
|
|
1614
|
+
programFallbackCache.delete(key);
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
return cloneProgramFallbackForRequest(cached.fallback, requestedProgramId, true);
|
|
1618
|
+
}
|
|
1619
|
+
function rememberProgramFallback(config, fallback) {
|
|
1620
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
pruneProgramFallbackCache();
|
|
1624
|
+
const key = programFallbackCacheKey(config, fallback.requestedProgramId);
|
|
1625
|
+
programFallbackCache.set(key, {
|
|
1626
|
+
expiresAt: Date.now() + config.cacheTtlMs,
|
|
1627
|
+
fallback: cloneProgramFallbackForRequest(fallback, fallback.requestedProgramId, false)
|
|
1628
|
+
});
|
|
1629
|
+
if (programFallbackCache.size > PROGRAM_FALLBACK_CACHE_MAX_ENTRIES) {
|
|
1630
|
+
const oldestKey = programFallbackCache.keys().next().value;
|
|
1631
|
+
if (oldestKey) {
|
|
1632
|
+
programFallbackCache.delete(oldestKey);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
function pruneProgramFallbackCache() {
|
|
1637
|
+
const now = Date.now();
|
|
1638
|
+
for (const [key, cached] of programFallbackCache) {
|
|
1639
|
+
if (cached.expiresAt <= now) {
|
|
1640
|
+
programFallbackCache.delete(key);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
function programFallbackCacheKey(config, requestedProgramId) {
|
|
1645
|
+
return `${config.apiBaseUrl}\n${normalizeTag(requestedProgramId)}`;
|
|
1646
|
+
}
|
|
1647
|
+
function cloneProgramFallbackForRequest(fallback, requestedProgramId, fromCache) {
|
|
1648
|
+
const warnings = fromCache
|
|
1649
|
+
? uniqueStrings([...fallback.warnings, `Reused cached program_id resolution for ${requestedProgramId}.`])
|
|
1650
|
+
: [...fallback.warnings];
|
|
1651
|
+
const cloned = {
|
|
1652
|
+
...fallback,
|
|
1653
|
+
requestedProgramId,
|
|
1654
|
+
warnings,
|
|
1655
|
+
sourceRequests: [...fallback.sourceRequests],
|
|
1656
|
+
resolution: { ...fallback.resolution }
|
|
1657
|
+
};
|
|
1658
|
+
if (isProgramIndexFallback(fallback)) {
|
|
1659
|
+
const indexClone = {
|
|
1660
|
+
...cloned,
|
|
1661
|
+
program: { ...fallback.program }
|
|
1662
|
+
};
|
|
1663
|
+
return indexClone;
|
|
1664
|
+
}
|
|
1665
|
+
return cloned;
|
|
1666
|
+
}
|
|
1667
|
+
function isProgramIndexFallback(fallback) {
|
|
1668
|
+
return readObject(fallback.program) !== undefined;
|
|
1669
|
+
}
|
|
1670
|
+
function createProgramIndexFallbackFromStaleResolution(fallback, requestedProgramId, detailError) {
|
|
1671
|
+
const match = selectProgramIndexFallbackMatch(fallback.resolvedProgramId, fallback.resolution);
|
|
1672
|
+
if (!match) {
|
|
1673
|
+
return undefined;
|
|
1674
|
+
}
|
|
1675
|
+
const detailApiError = detailError instanceof BBRadarApiError ? detailError : undefined;
|
|
1676
|
+
const indexWarning = `program_id ${fallback.resolvedProgramId} exists in the BBRadar program index, but the per-program detail endpoint returned 404.`;
|
|
1677
|
+
return {
|
|
1678
|
+
...fallback,
|
|
1679
|
+
requestedProgramId,
|
|
1680
|
+
staleRequestId: fallback.staleRequestId ?? detailApiError?.requestId,
|
|
1681
|
+
staleUpstreamRequestId: fallback.staleUpstreamRequestId ?? detailApiError?.upstreamRequestId,
|
|
1682
|
+
warnings: uniqueStrings([...fallback.warnings, indexWarning]),
|
|
1683
|
+
program: match.program
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1552
1686
|
async function resolveIndexedProgramId(client, config, requestedProgramId, staleError) {
|
|
1553
1687
|
const query = programSearchText(requestedProgramId);
|
|
1554
1688
|
if (query.length < 2) {
|
|
@@ -2827,7 +2961,7 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
2827
2961
|
});
|
|
2828
2962
|
}
|
|
2829
2963
|
async function getProgramTargetsExportFallbackPayload(client, input, programId) {
|
|
2830
|
-
const exportLookup = await
|
|
2964
|
+
const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, programId);
|
|
2831
2965
|
const sanitizedTargets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, input));
|
|
2832
2966
|
const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
|
|
2833
2967
|
return stripUndefined({
|
|
@@ -2853,6 +2987,123 @@ async function getProgramTargetsExportFallbackPayload(client, input, programId)
|
|
|
2853
2987
|
})
|
|
2854
2988
|
});
|
|
2855
2989
|
}
|
|
2990
|
+
async function fetchProgramTargetsWithEndpointFallback(client, programId) {
|
|
2991
|
+
const cachedLookup = getCachedProgramTargetsFallback(client, programId);
|
|
2992
|
+
if (cachedLookup) {
|
|
2993
|
+
return cachedLookup;
|
|
2994
|
+
}
|
|
2995
|
+
try {
|
|
2996
|
+
const api = await client.getProgramTargets(programId);
|
|
2997
|
+
const data = readObject(api.data);
|
|
2998
|
+
const rawTargets = readArray(data?.targets).map(sanitizeTarget);
|
|
2999
|
+
return {
|
|
3000
|
+
requestId: api.requestId,
|
|
3001
|
+
upstreamRequestId: api.upstreamRequestId,
|
|
3002
|
+
fetchedAt: api.fetchedAt,
|
|
3003
|
+
cache: readObject(stripUndefined({
|
|
3004
|
+
hit: api.cached,
|
|
3005
|
+
coalesced_live_request: api.coalesced,
|
|
3006
|
+
expires_at: api.cacheExpiresAt
|
|
3007
|
+
})),
|
|
3008
|
+
source: "program_targets",
|
|
3009
|
+
rawTargets,
|
|
3010
|
+
sourceRequests: [
|
|
3011
|
+
{
|
|
3012
|
+
source: "program_targets",
|
|
3013
|
+
program_id: programId,
|
|
3014
|
+
request_id: api.requestId,
|
|
3015
|
+
upstream_request_id: api.upstreamRequestId,
|
|
3016
|
+
...apiSourceMetadata(api)
|
|
3017
|
+
}
|
|
3018
|
+
],
|
|
3019
|
+
warnings: [],
|
|
3020
|
+
unavailable: false
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
catch (error) {
|
|
3024
|
+
if (!isProgramNotFoundError(error)) {
|
|
3025
|
+
throw error;
|
|
3026
|
+
}
|
|
3027
|
+
const lookup = await fetchProgramTargetsFromExportFallback(client, programId);
|
|
3028
|
+
rememberProgramTargetsFallback(client, programId, lookup);
|
|
3029
|
+
return lookup;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
function getCachedProgramTargetsFallback(client, programId) {
|
|
3033
|
+
const diagnostics = client.diagnostics();
|
|
3034
|
+
if (!diagnostics.response_cache.enabled) {
|
|
3035
|
+
return undefined;
|
|
3036
|
+
}
|
|
3037
|
+
const cache = programTargetsFallbackCaches.get(client);
|
|
3038
|
+
const key = programTargetsFallbackCacheKey(programId);
|
|
3039
|
+
const cached = cache?.get(key);
|
|
3040
|
+
if (!cached) {
|
|
3041
|
+
return undefined;
|
|
3042
|
+
}
|
|
3043
|
+
if (cached.expiresAt <= Date.now()) {
|
|
3044
|
+
cache?.delete(key);
|
|
3045
|
+
return undefined;
|
|
3046
|
+
}
|
|
3047
|
+
return {
|
|
3048
|
+
...cached.lookup,
|
|
3049
|
+
cache: readObject(stripUndefined({
|
|
3050
|
+
...(cached.lookup.cache ?? {}),
|
|
3051
|
+
hit: true,
|
|
3052
|
+
expires_at: new Date(cached.expiresAt).toISOString()
|
|
3053
|
+
})),
|
|
3054
|
+
sourceRequests: cached.lookup.sourceRequests.map((request) => ({
|
|
3055
|
+
...request,
|
|
3056
|
+
cache_hit: true,
|
|
3057
|
+
cache_expires_at: new Date(cached.expiresAt).toISOString()
|
|
3058
|
+
})),
|
|
3059
|
+
warnings: uniqueStrings([...cached.lookup.warnings, `Reused cached target fallback for ${programId}.`])
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
function rememberProgramTargetsFallback(client, programId, lookup) {
|
|
3063
|
+
const diagnostics = client.diagnostics();
|
|
3064
|
+
if (!diagnostics.response_cache.enabled || lookup.unavailable) {
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
const cache = programTargetsFallbackCacheForClient(client);
|
|
3068
|
+
pruneProgramTargetsFallbackCache(cache);
|
|
3069
|
+
const ttlMs = cacheTtlMsFromDiagnostics(diagnostics.response_cache);
|
|
3070
|
+
if (ttlMs <= 0) {
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
cache.set(programTargetsFallbackCacheKey(programId), {
|
|
3074
|
+
expiresAt: Date.now() + ttlMs,
|
|
3075
|
+
lookup
|
|
3076
|
+
});
|
|
3077
|
+
if (cache.size > PROGRAM_FALLBACK_CACHE_MAX_ENTRIES) {
|
|
3078
|
+
const oldestKey = cache.keys().next().value;
|
|
3079
|
+
if (oldestKey) {
|
|
3080
|
+
cache.delete(oldestKey);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function programTargetsFallbackCacheForClient(client) {
|
|
3085
|
+
const existing = programTargetsFallbackCaches.get(client);
|
|
3086
|
+
if (existing) {
|
|
3087
|
+
return existing;
|
|
3088
|
+
}
|
|
3089
|
+
const cache = new Map();
|
|
3090
|
+
programTargetsFallbackCaches.set(client, cache);
|
|
3091
|
+
return cache;
|
|
3092
|
+
}
|
|
3093
|
+
function pruneProgramTargetsFallbackCache(cache) {
|
|
3094
|
+
const now = Date.now();
|
|
3095
|
+
for (const [key, cached] of cache) {
|
|
3096
|
+
if (cached.expiresAt <= now) {
|
|
3097
|
+
cache.delete(key);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
function programTargetsFallbackCacheKey(programId) {
|
|
3102
|
+
return normalizeTag(programId);
|
|
3103
|
+
}
|
|
3104
|
+
function cacheTtlMsFromDiagnostics(cache) {
|
|
3105
|
+
return cache.enabled ? cache.ttl_ms : 0;
|
|
3106
|
+
}
|
|
2856
3107
|
async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
2857
3108
|
try {
|
|
2858
3109
|
const api = await client.exportTargets({
|
|
@@ -2908,6 +3159,116 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
|
2908
3159
|
};
|
|
2909
3160
|
}
|
|
2910
3161
|
}
|
|
3162
|
+
async function exportTargetsFromProgramTargetsFallback(client, config, exportStore, input, exportError) {
|
|
3163
|
+
const warnings = ["Target export endpoint is unavailable; assembled a fallback export from per-program target endpoints."];
|
|
3164
|
+
const sourceRequests = [
|
|
3165
|
+
{
|
|
3166
|
+
source: "target_export",
|
|
3167
|
+
failed: true,
|
|
3168
|
+
request_id: exportError.requestId,
|
|
3169
|
+
upstream_request_id: exportError.upstreamRequestId,
|
|
3170
|
+
status: exportError.status
|
|
3171
|
+
}
|
|
3172
|
+
];
|
|
3173
|
+
const programIds = input.program_ids.length > 0 ? input.program_ids : await discoverExportProgramIds(client, input, sourceRequests, warnings);
|
|
3174
|
+
const targets = [];
|
|
3175
|
+
const errors = [];
|
|
3176
|
+
for (const programId of programIds) {
|
|
3177
|
+
if (targets.length >= input.limit) {
|
|
3178
|
+
break;
|
|
3179
|
+
}
|
|
3180
|
+
try {
|
|
3181
|
+
const payload = await runProgramIdTool(client, config, programId, (resolvedProgramId) => getProgramTargetsPayload(client, {
|
|
3182
|
+
include_out_of_scope: input.include_out_of_scope,
|
|
3183
|
+
include_ineligible: input.include_ineligible,
|
|
3184
|
+
strict_scope_filter: false,
|
|
3185
|
+
offset: 0,
|
|
3186
|
+
limit: input.limit - targets.length,
|
|
3187
|
+
output_mode: "full"
|
|
3188
|
+
}, resolvedProgramId), (indexFallback) => getProgramTargetsExportFallbackPayload(client, {
|
|
3189
|
+
include_out_of_scope: input.include_out_of_scope,
|
|
3190
|
+
include_ineligible: input.include_ineligible,
|
|
3191
|
+
strict_scope_filter: false,
|
|
3192
|
+
offset: 0,
|
|
3193
|
+
limit: input.limit - targets.length,
|
|
3194
|
+
output_mode: "full"
|
|
3195
|
+
}, indexFallback.resolvedProgramId));
|
|
3196
|
+
const resolvedProgramId = stringField(payload, "program_id") ?? programId;
|
|
3197
|
+
sourceRequests.push(...readArray(payload.source_requests).filter((request) => readObject(request) !== undefined));
|
|
3198
|
+
warnings.push(...readArray(payload.warnings).filter((warning) => typeof warning === "string"));
|
|
3199
|
+
for (const target of readArray(payload.targets)) {
|
|
3200
|
+
const object = readObject(target);
|
|
3201
|
+
if (!object) {
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
targets.push({ program_id: resolvedProgramId, ...object });
|
|
3205
|
+
if (targets.length >= input.limit) {
|
|
3206
|
+
break;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
catch (error) {
|
|
3211
|
+
errors.push(apiOperationError(programId, error));
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
if (input.format === "csv") {
|
|
3215
|
+
warnings.push("Fallback target export returns JSON even when CSV was requested.");
|
|
3216
|
+
}
|
|
3217
|
+
const exportPayload = stripUndefined({
|
|
3218
|
+
targets,
|
|
3219
|
+
meta: {
|
|
3220
|
+
export_source: "program_targets_fallback",
|
|
3221
|
+
requested_program_count: programIds.length,
|
|
3222
|
+
returned: targets.length,
|
|
3223
|
+
limit: input.limit,
|
|
3224
|
+
format: "json"
|
|
3225
|
+
},
|
|
3226
|
+
generated_at: new Date().toISOString()
|
|
3227
|
+
});
|
|
3228
|
+
const exportId = randomUUID();
|
|
3229
|
+
const resourceUri = exportResourceUri(exportId);
|
|
3230
|
+
rememberExport(exportStore, exportId, exportPayload);
|
|
3231
|
+
return stripUndefined({
|
|
3232
|
+
request_id: randomUUID(),
|
|
3233
|
+
source_requests: sourceRequests,
|
|
3234
|
+
warnings: uniqueStrings(warnings),
|
|
3235
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
3236
|
+
export: {
|
|
3237
|
+
export_id: exportId,
|
|
3238
|
+
resource_uri: resourceUri,
|
|
3239
|
+
preview: previewExportPayload(exportPayload),
|
|
3240
|
+
returned_preview_count: previewExportCount(exportPayload),
|
|
3241
|
+
limit: input.limit
|
|
3242
|
+
},
|
|
3243
|
+
meta: stripUndefined({
|
|
3244
|
+
export_source: "program_targets_fallback",
|
|
3245
|
+
requested_program_count: programIds.length,
|
|
3246
|
+
returned: targets.length,
|
|
3247
|
+
failed_program_count: errors.length,
|
|
3248
|
+
fallback_error: apiOperationError("target_export", exportError)
|
|
3249
|
+
})
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
async function discoverExportProgramIds(client, input, sourceRequests, warnings) {
|
|
3253
|
+
const budget = {
|
|
3254
|
+
initial: 1,
|
|
3255
|
+
remaining: 1
|
|
3256
|
+
};
|
|
3257
|
+
const collected = await collectPrograms(client, {
|
|
3258
|
+
platforms: input.platforms,
|
|
3259
|
+
tags: input.tags,
|
|
3260
|
+
updated_since: undefined,
|
|
3261
|
+
opportunity_levels: input.opportunity_levels,
|
|
3262
|
+
max_pages: 1,
|
|
3263
|
+
budget,
|
|
3264
|
+
warnings
|
|
3265
|
+
});
|
|
3266
|
+
sourceRequests.push(...collected.sourceRequests);
|
|
3267
|
+
return uniqueStrings(collected.programs
|
|
3268
|
+
.map((program) => stringField(sanitizeProgram(program, ""), "id"))
|
|
3269
|
+
.filter((programId) => programId !== undefined)
|
|
3270
|
+
.slice(0, input.limit));
|
|
3271
|
+
}
|
|
2911
3272
|
function readExportTargetRows(data) {
|
|
2912
3273
|
if (Array.isArray(data)) {
|
|
2913
3274
|
return data;
|
|
@@ -2958,7 +3319,7 @@ async function getProgramScopeSummary(client, config, input) {
|
|
|
2958
3319
|
};
|
|
2959
3320
|
}
|
|
2960
3321
|
async function getProgramScopeSummaryFromIndexFallback(client, indexFallback, input) {
|
|
2961
|
-
const exportLookup = await
|
|
3322
|
+
const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, input.program_id);
|
|
2962
3323
|
const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
|
|
2963
3324
|
include_out_of_scope: input.include_out_of_scope,
|
|
2964
3325
|
include_ineligible: input.include_ineligible,
|
|
@@ -3031,7 +3392,7 @@ async function getProgramTargetBreakdown(client, config, input) {
|
|
|
3031
3392
|
};
|
|
3032
3393
|
}
|
|
3033
3394
|
async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback, input) {
|
|
3034
|
-
const exportLookup = await
|
|
3395
|
+
const exportLookup = await fetchProgramTargetsWithEndpointFallback(client, input.program_id);
|
|
3035
3396
|
const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
|
|
3036
3397
|
include_out_of_scope: input.include_out_of_scope,
|
|
3037
3398
|
include_ineligible: input.include_ineligible,
|
|
@@ -3069,19 +3430,8 @@ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback,
|
|
|
3069
3430
|
async function getProgramScopeDelta(client, config, input) {
|
|
3070
3431
|
const programApi = await client.getProgram(input.program_id);
|
|
3071
3432
|
const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
|
|
3072
|
-
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);
|
|
3073
|
-
const
|
|
3074
|
-
const filteredChanges = changesPayload.changes
|
|
3075
|
-
.filter((change) => !input.change_type || stringField(change, "change_type") === input.change_type)
|
|
3076
|
-
.filter((change) => sinceTimestamp === undefined || timestampField(change, "changed_at") >= sinceTimestamp)
|
|
3077
|
-
.filter((change) => {
|
|
3078
|
-
const target = readObject(change.target);
|
|
3079
|
-
return !input.target_type || (target !== undefined && targetMatchesRequestedType(target, input.target_type));
|
|
3080
|
-
})
|
|
3081
|
-
.filter((change) => {
|
|
3082
|
-
const target = readObject(change.target);
|
|
3083
|
-
return input.language_tags.length === 0 || (target !== undefined && targetMatchesAnySignal(target, input.language_tags));
|
|
3084
|
-
});
|
|
3433
|
+
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);
|
|
3434
|
+
const filteredChanges = filterProgramScopeDeltaChanges(changesPayload.changes, input);
|
|
3085
3435
|
const limitedChanges = filteredChanges.slice(0, input.max_targets);
|
|
3086
3436
|
return stripUndefined({
|
|
3087
3437
|
request_id: randomUUID(),
|
|
@@ -3106,10 +3456,55 @@ async function getProgramScopeDelta(client, config, input) {
|
|
|
3106
3456
|
change_type: input.change_type,
|
|
3107
3457
|
target_type: input.target_type,
|
|
3108
3458
|
language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
|
|
3109
|
-
target_list_mode: input.target_list_mode
|
|
3459
|
+
target_list_mode: input.target_list_mode,
|
|
3460
|
+
upstream_total_pages: readNumber(changesPayload.meta?.total_pages),
|
|
3461
|
+
pages_fetched: readNumber(changesPayload.meta?.pages_fetched),
|
|
3462
|
+
scan_may_be_incomplete: (readNumber(changesPayload.meta?.total_pages) ?? 1) > (readNumber(changesPayload.meta?.pages_fetched) ?? 1)
|
|
3110
3463
|
})
|
|
3111
3464
|
});
|
|
3112
3465
|
}
|
|
3466
|
+
async function getProgramScopeDeltaFromIndexedProgram(client, config, input, indexedProgram) {
|
|
3467
|
+
const program = addProgramResourceLinks(indexedProgram);
|
|
3468
|
+
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);
|
|
3469
|
+
const filteredChanges = filterProgramScopeDeltaChanges(changesPayload.changes, input);
|
|
3470
|
+
const limitedChanges = filteredChanges.slice(0, input.max_targets);
|
|
3471
|
+
return stripUndefined({
|
|
3472
|
+
request_id: randomUUID(),
|
|
3473
|
+
source_requests: changesPayload.sourceRequests,
|
|
3474
|
+
program: formatProgram(program, "compact"),
|
|
3475
|
+
delta: buildScopeDelta(limitedChanges, input.target_list_mode),
|
|
3476
|
+
changes: limitedChanges.map((change) => formatChange(change, "compact")),
|
|
3477
|
+
meta: stripUndefined({
|
|
3478
|
+
recent_changes_scanned: changesPayload.changes.length,
|
|
3479
|
+
changes_after_filters: filteredChanges.length,
|
|
3480
|
+
returned: limitedChanges.length,
|
|
3481
|
+
has_more: filteredChanges.length > limitedChanges.length,
|
|
3482
|
+
since: input.since,
|
|
3483
|
+
change_type: input.change_type,
|
|
3484
|
+
target_type: input.target_type,
|
|
3485
|
+
language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
|
|
3486
|
+
target_list_mode: input.target_list_mode,
|
|
3487
|
+
program_detail_source: "program_index",
|
|
3488
|
+
upstream_total_pages: readNumber(changesPayload.meta?.total_pages),
|
|
3489
|
+
pages_fetched: readNumber(changesPayload.meta?.pages_fetched),
|
|
3490
|
+
scan_may_be_incomplete: (readNumber(changesPayload.meta?.total_pages) ?? 1) > (readNumber(changesPayload.meta?.pages_fetched) ?? 1)
|
|
3491
|
+
})
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
function filterProgramScopeDeltaChanges(changes, input) {
|
|
3495
|
+
const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
|
|
3496
|
+
return changes
|
|
3497
|
+
.filter((change) => !input.change_type || stringField(change, "change_type") === input.change_type)
|
|
3498
|
+
.filter((change) => sinceTimestamp === undefined || timestampField(change, "changed_at") >= sinceTimestamp)
|
|
3499
|
+
.filter((change) => {
|
|
3500
|
+
const target = readObject(change.target);
|
|
3501
|
+
return !input.target_type || (target !== undefined && targetMatchesRequestedType(target, input.target_type));
|
|
3502
|
+
})
|
|
3503
|
+
.filter((change) => {
|
|
3504
|
+
const target = readObject(change.target);
|
|
3505
|
+
return input.language_tags.length === 0 || (target !== undefined && targetMatchesAnySignal(target, input.language_tags));
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3113
3508
|
async function getRecentTargetActivity(client, config, input) {
|
|
3114
3509
|
const query = toQuery({
|
|
3115
3510
|
change_type: input.change_type,
|
|
@@ -3517,32 +3912,45 @@ async function getProgramDelta(client, config, input) {
|
|
|
3517
3912
|
meta: changes.meta
|
|
3518
3913
|
};
|
|
3519
3914
|
}
|
|
3520
|
-
async function fetchProgramChanges(client, config, programId, pageSize, includeRemoved, includeIneligible, includeOutOfScope) {
|
|
3915
|
+
async function fetchProgramChanges(client, config, programId, pageSize, includeRemoved, includeIneligible, includeOutOfScope, maxChanges = pageSize) {
|
|
3521
3916
|
const handle = programSearchText(programId);
|
|
3522
|
-
const
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3917
|
+
const changes = [];
|
|
3918
|
+
const sourceRequests = [];
|
|
3919
|
+
let meta;
|
|
3920
|
+
const maxPages = Math.max(1, Math.ceil(maxChanges / pageSize));
|
|
3921
|
+
for (let page = 1; page <= maxPages && changes.length < maxChanges; page += 1) {
|
|
3922
|
+
const api = await client.getRecentChanges({
|
|
3923
|
+
search: handle.length >= 2 ? handle : programId,
|
|
3924
|
+
include_removed: includeRemoved,
|
|
3925
|
+
include_ineligible: includeIneligible,
|
|
3926
|
+
include_out_of_scope: includeOutOfScope,
|
|
3927
|
+
page,
|
|
3928
|
+
page_size: pageSize
|
|
3929
|
+
});
|
|
3930
|
+
const data = readObject(api.data);
|
|
3931
|
+
const pageMeta = readObject(sanitizeJson(data?.meta));
|
|
3932
|
+
const rows = readArray(data?.results);
|
|
3933
|
+
meta = {
|
|
3934
|
+
...(meta ?? {}),
|
|
3935
|
+
...(pageMeta ?? {}),
|
|
3936
|
+
pages_fetched: page
|
|
3937
|
+
};
|
|
3938
|
+
sourceRequests.push({
|
|
3939
|
+
source: "recent_changes",
|
|
3940
|
+
request_id: api.requestId,
|
|
3941
|
+
upstream_request_id: api.upstreamRequestId,
|
|
3942
|
+
page,
|
|
3943
|
+
...apiSourceMetadata(api)
|
|
3944
|
+
});
|
|
3945
|
+
changes.push(...rows
|
|
3946
|
+
.map((change) => sanitizeChange(change, config.webBaseUrl))
|
|
3947
|
+
.filter((change) => stringField(readObject(change.program), "id") === programId));
|
|
3948
|
+
const totalPages = readNumber(pageMeta?.total_pages);
|
|
3949
|
+
if (rows.length === 0 || (totalPages !== undefined && page >= totalPages)) {
|
|
3950
|
+
break;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
return { changes: changes.slice(0, maxChanges), meta, sourceRequests };
|
|
3546
3954
|
}
|
|
3547
3955
|
function addProgramResourceLinks(program) {
|
|
3548
3956
|
const id = stringField(program, "id");
|
|
@@ -4136,6 +4544,23 @@ function programFetchError(programId, error) {
|
|
|
4136
4544
|
suggested_fix: "Retry the program lookup or verify the BBRadar program id."
|
|
4137
4545
|
};
|
|
4138
4546
|
}
|
|
4547
|
+
function apiOperationError(operation, error) {
|
|
4548
|
+
if (error instanceof BBRadarApiError) {
|
|
4549
|
+
return stripUndefined({
|
|
4550
|
+
operation,
|
|
4551
|
+
request_id: error.requestId,
|
|
4552
|
+
upstream_request_id: error.upstreamRequestId,
|
|
4553
|
+
status: error.status,
|
|
4554
|
+
message: error.message,
|
|
4555
|
+
detail: sanitizeJson(error.detail),
|
|
4556
|
+
errors: sanitizeJson(error.errors)
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4559
|
+
return {
|
|
4560
|
+
operation,
|
|
4561
|
+
message: error instanceof Error ? error.message : String(error)
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4139
4564
|
function programSearchText(programId) {
|
|
4140
4565
|
const colonIndex = programId.indexOf(":");
|
|
4141
4566
|
const handle = colonIndex >= 0 ? programId.slice(colonIndex + 1) : programId;
|
|
@@ -4999,6 +5424,21 @@ function toRateLimitPayload(decision) {
|
|
|
4999
5424
|
function rateLimitForTool(toolName, config) {
|
|
5000
5425
|
return toolName === "export_targets" ? config.exportRateLimitPerMinute : config.defaultRateLimitPerMinute;
|
|
5001
5426
|
}
|
|
5427
|
+
function normalizeStringListInput(value) {
|
|
5428
|
+
if (typeof value === "string") {
|
|
5429
|
+
return splitFilterList(value);
|
|
5430
|
+
}
|
|
5431
|
+
if (Array.isArray(value)) {
|
|
5432
|
+
return value.flatMap((entry) => (typeof entry === "string" ? splitFilterList(entry) : [entry]));
|
|
5433
|
+
}
|
|
5434
|
+
return value;
|
|
5435
|
+
}
|
|
5436
|
+
function splitFilterList(value) {
|
|
5437
|
+
return value
|
|
5438
|
+
.split(",")
|
|
5439
|
+
.map((entry) => entry.trim())
|
|
5440
|
+
.filter((entry) => entry.length > 0);
|
|
5441
|
+
}
|
|
5002
5442
|
function toQuery(values) {
|
|
5003
5443
|
return compactRecord(values);
|
|
5004
5444
|
}
|