peak6-x-intelligence-plugin 0.1.8 → 0.1.9
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/README.md +1 -1
- package/dist/worker.js +231 -189
- package/dist/worker.js.map +4 -4
- package/package.json +16 -10
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ POST /api/plugins/{pluginId}/bridge/data — {"key": "dashboard-summary", "par
|
|
|
41
41
|
POST /api/plugins/{pluginId}/bridge/action — {"key": "trigger-discovery", "params": {}}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Skills in [attention-machine](https://github.com/peak6-labs/attention-machine) teach agents how to use these tools — `x-research` for ad-hoc queries, `daily-intelligence` for the corpus workflow.
|
|
45
45
|
|
|
46
46
|
## Deployment
|
|
47
47
|
|
package/dist/worker.js
CHANGED
|
@@ -1034,6 +1034,19 @@ var DEFAULT_CONFIG = {
|
|
|
1034
1034
|
alert_agents: []
|
|
1035
1035
|
};
|
|
1036
1036
|
|
|
1037
|
+
// src/pipeline/fetch-utils.ts
|
|
1038
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
1039
|
+
async function fetchWithTimeout(http, url, init = {}) {
|
|
1040
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, ...fetchInit } = init;
|
|
1041
|
+
const controller = new AbortController();
|
|
1042
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1043
|
+
try {
|
|
1044
|
+
return await http.fetch(url, { ...fetchInit, signal: controller.signal });
|
|
1045
|
+
} finally {
|
|
1046
|
+
clearTimeout(timeoutId);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1037
1050
|
// src/pipeline/xai-client.ts
|
|
1038
1051
|
var XAI_RESPONSES_URL = "https://api.x.ai/v1/responses";
|
|
1039
1052
|
var XAI_MODEL = "grok-4-fast-reasoning";
|
|
@@ -1082,13 +1095,14 @@ async function xaiSearch(http, secrets, logger, apiKeyRef, options) {
|
|
|
1082
1095
|
inline_citations: true
|
|
1083
1096
|
};
|
|
1084
1097
|
logger.debug("xAI search request", { query: options.query, handles: options.allowedHandles });
|
|
1085
|
-
const response = await
|
|
1098
|
+
const response = await fetchWithTimeout(http, XAI_RESPONSES_URL, {
|
|
1086
1099
|
method: "POST",
|
|
1087
1100
|
headers: {
|
|
1088
1101
|
"Content-Type": "application/json",
|
|
1089
1102
|
Authorization: `Bearer ${apiKey}`
|
|
1090
1103
|
},
|
|
1091
|
-
body: JSON.stringify(body)
|
|
1104
|
+
body: JSON.stringify(body),
|
|
1105
|
+
timeoutMs: 2e4
|
|
1092
1106
|
});
|
|
1093
1107
|
if (!response.ok) {
|
|
1094
1108
|
const errorText = await response.text();
|
|
@@ -1137,7 +1151,7 @@ async function batchLookupTweets(http, secrets, logger, bearerRef, tweetIds, sou
|
|
|
1137
1151
|
const batch = tweetIds.slice(i, i + BATCH_SIZE);
|
|
1138
1152
|
const url = `${X_API_BASE}/tweets?ids=${batch.join(",")}&tweet.fields=${TWEET_FIELDS}&expansions=${EXPANSIONS}&user.fields=${USER_FIELDS}`;
|
|
1139
1153
|
logger.debug("X API v2 batch lookup", { batchSize: batch.length, offset: i });
|
|
1140
|
-
const response = await http
|
|
1154
|
+
const response = await fetchWithTimeout(http, url, {
|
|
1141
1155
|
method: "GET",
|
|
1142
1156
|
headers: {
|
|
1143
1157
|
Authorization: `Bearer ${bearerToken}`
|
|
@@ -1187,7 +1201,7 @@ function mapToEnrichedTweet(tweet, author, source) {
|
|
|
1187
1201
|
async function verifyTweetExists(http, secrets, bearerRef, tweetId) {
|
|
1188
1202
|
const bearerToken = await secrets.resolve(bearerRef);
|
|
1189
1203
|
const url = `${X_API_BASE}/tweets?ids=${tweetId}&tweet.fields=id`;
|
|
1190
|
-
const response = await http
|
|
1204
|
+
const response = await fetchWithTimeout(http, url, {
|
|
1191
1205
|
method: "GET",
|
|
1192
1206
|
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
1193
1207
|
});
|
|
@@ -1198,7 +1212,7 @@ async function verifyTweetExists(http, secrets, bearerRef, tweetId) {
|
|
|
1198
1212
|
async function fetchTweetWithConversationId(http, secrets, logger, bearerRef, tweetId) {
|
|
1199
1213
|
const bearerToken = await secrets.resolve(bearerRef);
|
|
1200
1214
|
const url = `${X_API_BASE}/tweets?ids=${tweetId}&tweet.fields=conversation_id,${TWEET_FIELDS}&expansions=${EXPANSIONS}&user.fields=${USER_FIELDS}`;
|
|
1201
|
-
const response = await http
|
|
1215
|
+
const response = await fetchWithTimeout(http, url, {
|
|
1202
1216
|
method: "GET",
|
|
1203
1217
|
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
1204
1218
|
});
|
|
@@ -1225,7 +1239,7 @@ async function fetchConversationTweets(http, secrets, logger, bearerRef, convers
|
|
|
1225
1239
|
const query = encodeURIComponent(`conversation_id:${conversationId}`);
|
|
1226
1240
|
const url = `${X_API_BASE}/tweets/search/recent?query=${query}&tweet.fields=${TWEET_FIELDS}&expansions=${EXPANSIONS}&user.fields=${USER_FIELDS}&max_results=100`;
|
|
1227
1241
|
logger.debug("Fetching conversation tweets", { conversationId });
|
|
1228
|
-
const response = await http
|
|
1242
|
+
const response = await fetchWithTimeout(http, url, {
|
|
1229
1243
|
method: "GET",
|
|
1230
1244
|
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
1231
1245
|
});
|
|
@@ -1251,7 +1265,7 @@ async function fetchConversationTweets(http, secrets, logger, bearerRef, convers
|
|
|
1251
1265
|
async function fetchUserTimeline(http, secrets, logger, bearerRef, username, limit, sinceDate) {
|
|
1252
1266
|
const bearerToken = await secrets.resolve(bearerRef);
|
|
1253
1267
|
const userUrl = `${X_API_BASE}/users/by/username/${encodeURIComponent(username)}?user.fields=${USER_FIELDS}`;
|
|
1254
|
-
const userResponse = await http
|
|
1268
|
+
const userResponse = await fetchWithTimeout(http, userUrl, {
|
|
1255
1269
|
method: "GET",
|
|
1256
1270
|
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
1257
1271
|
});
|
|
@@ -1271,7 +1285,7 @@ async function fetchUserTimeline(http, secrets, logger, bearerRef, username, lim
|
|
|
1271
1285
|
timelineUrl += `&start_time=${sinceDate}T00:00:00Z`;
|
|
1272
1286
|
}
|
|
1273
1287
|
logger.debug("Fetching user timeline", { username, userId, limit: apiLimit });
|
|
1274
|
-
const timelineResponse = await http
|
|
1288
|
+
const timelineResponse = await fetchWithTimeout(http, timelineUrl, {
|
|
1275
1289
|
method: "GET",
|
|
1276
1290
|
headers: { Authorization: `Bearer ${bearerToken}` }
|
|
1277
1291
|
});
|
|
@@ -2228,58 +2242,69 @@ async function handleAnalyzeTopic(ctx, params, _runCtx) {
|
|
|
2228
2242
|
const topic = typeof p.topic === "string" ? p.topic : "";
|
|
2229
2243
|
const includeCorpus = p.include_corpus !== false;
|
|
2230
2244
|
if (!topic) return { error: "topic is required" };
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2245
|
+
try {
|
|
2246
|
+
const config = await getConfig(ctx);
|
|
2247
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2248
|
+
await ctx.metrics.write("tool.analyze_topic.invoked", 1);
|
|
2249
|
+
const queries = buildTopicQueries(topic, today);
|
|
2250
|
+
const freshResults = [];
|
|
2251
|
+
let totalCost = 0;
|
|
2252
|
+
const settled = await Promise.allSettled(
|
|
2253
|
+
queries.map(
|
|
2254
|
+
(query) => xaiSearch(
|
|
2255
|
+
ctx.http,
|
|
2256
|
+
ctx.secrets,
|
|
2257
|
+
ctx.logger,
|
|
2258
|
+
config.xai_api_key_ref,
|
|
2259
|
+
{ query: query.text, fromDate: today, toDate: today, excludedHandles: config.global_excluded }
|
|
2260
|
+
)
|
|
2261
|
+
)
|
|
2262
|
+
);
|
|
2263
|
+
for (let i = 0; i < settled.length; i++) {
|
|
2264
|
+
const outcome = settled[i];
|
|
2265
|
+
if (outcome.status === "fulfilled") {
|
|
2266
|
+
freshResults.push({
|
|
2267
|
+
tweet_ids: outcome.value.tweetIds,
|
|
2268
|
+
query_type: queries[i].topic,
|
|
2269
|
+
synthesis_text: outcome.value.synthesisText
|
|
2270
|
+
});
|
|
2271
|
+
totalCost += outcome.value.costUsdTicks;
|
|
2272
|
+
} else {
|
|
2273
|
+
ctx.logger.warn("Analyze topic sub-query failed", {
|
|
2274
|
+
queryType: queries[i].topic,
|
|
2275
|
+
error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2257
2278
|
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
synthesis: freshResults.map((r) => `## ${r.query_type}
|
|
2279
|
+
let corpusMatches = [];
|
|
2280
|
+
if (includeCorpus) {
|
|
2281
|
+
const allTweets = await ctx.entities.list({ entityType: ENTITY_TYPES.tweet, limit: 500 });
|
|
2282
|
+
const corpus = allTweets.filter((t) => t.status === "active").map((t) => t.data);
|
|
2283
|
+
corpusMatches = crossReferenceCorpus(topic, corpus);
|
|
2284
|
+
}
|
|
2285
|
+
await ctx.metrics.write("tool.analyze_topic.cost", totalCost / 1e9);
|
|
2286
|
+
const corpusWithUrls = corpusMatches.slice(0, 10).map(withUrl);
|
|
2287
|
+
const result = {
|
|
2288
|
+
topic,
|
|
2289
|
+
synthesis: freshResults.map((r) => `## ${r.query_type}
|
|
2270
2290
|
${r.synthesis_text}`).join("\n\n"),
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2291
|
+
corpus_matches: corpusWithUrls,
|
|
2292
|
+
fresh_results: freshResults,
|
|
2293
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2294
|
+
};
|
|
2295
|
+
const matchLinks = corpusWithUrls.slice(0, 5).map((t) => `- @${t.author_username}: ${t.url}`).join("\n");
|
|
2296
|
+
return {
|
|
2297
|
+
content: `Topic analysis for "${topic}": ${freshResults.length} queries completed, ${corpusMatches.length} corpus matches found.${matchLinks ? `
|
|
2278
2298
|
|
|
2279
2299
|
Top corpus matches:
|
|
2280
2300
|
${matchLinks}` : ""}`,
|
|
2281
|
-
|
|
2282
|
-
|
|
2301
|
+
data: result
|
|
2302
|
+
};
|
|
2303
|
+
} catch (err) {
|
|
2304
|
+
return {
|
|
2305
|
+
error: `Topic analysis failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2283
2308
|
}
|
|
2284
2309
|
async function handleGetThread(ctx, params, _runCtx) {
|
|
2285
2310
|
const p = params;
|
|
@@ -2442,162 +2467,179 @@ async function handleSearchX(ctx, params, _runCtx) {
|
|
|
2442
2467
|
const handles = Array.isArray(p.handles) ? p.handles : void 0;
|
|
2443
2468
|
const quick = p.quick === true;
|
|
2444
2469
|
if (!query) return { error: "query is required" };
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2470
|
+
try {
|
|
2471
|
+
const config = await getConfig(ctx);
|
|
2472
|
+
await ctx.metrics.write("tool.search_x.invoked", 1);
|
|
2473
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2474
|
+
const threeDaysAgo = new Date(Date.now() - 3 * 864e5).toISOString().split("T")[0];
|
|
2475
|
+
let totalCost = 0;
|
|
2476
|
+
const result = await xaiSearch(
|
|
2477
|
+
ctx.http,
|
|
2478
|
+
ctx.secrets,
|
|
2479
|
+
ctx.logger,
|
|
2480
|
+
config.xai_api_key_ref,
|
|
2481
|
+
{
|
|
2482
|
+
query,
|
|
2483
|
+
fromDate: threeDaysAgo,
|
|
2484
|
+
toDate: today,
|
|
2485
|
+
allowedHandles: handles,
|
|
2486
|
+
excludedHandles: handles ? void 0 : config.global_excluded
|
|
2487
|
+
}
|
|
2488
|
+
);
|
|
2489
|
+
totalCost += result.costUsdTicks;
|
|
2490
|
+
if (quick) {
|
|
2491
|
+
const limitedIds = result.tweetIds.slice(0, limit);
|
|
2492
|
+
await ctx.metrics.write("tool.search_x.cost", totalCost / 1e9);
|
|
2493
|
+
return {
|
|
2494
|
+
content: `Search for "${query}" (quick mode): ${limitedIds.length} tweets found.
|
|
2495
|
+
|
|
2496
|
+
${result.synthesisText}`,
|
|
2497
|
+
data: {
|
|
2498
|
+
query,
|
|
2499
|
+
mode: "quick",
|
|
2500
|
+
tweet_ids: limitedIds,
|
|
2501
|
+
synthesis: result.synthesisText,
|
|
2502
|
+
cost_usd: totalCost / 1e9
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2461
2505
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2506
|
+
const tweets = await batchLookupTweets(
|
|
2507
|
+
ctx.http,
|
|
2508
|
+
ctx.secrets,
|
|
2509
|
+
ctx.logger,
|
|
2510
|
+
config.x_api_bearer_ref,
|
|
2511
|
+
result.tweetIds,
|
|
2512
|
+
"open_discovery"
|
|
2513
|
+
);
|
|
2514
|
+
const { allHandles, byDomain } = buildAuthorityMaps(config);
|
|
2515
|
+
const scored = scoreTweets(
|
|
2516
|
+
tweets,
|
|
2517
|
+
config.scoring_weights,
|
|
2518
|
+
config.engagement_sub_weights,
|
|
2519
|
+
config.authority_boost,
|
|
2520
|
+
allHandles,
|
|
2521
|
+
byDomain,
|
|
2522
|
+
config.semantic_topics
|
|
2523
|
+
);
|
|
2524
|
+
const deduped = deduplicateTweets(scored, config.dedup_threshold);
|
|
2525
|
+
const limited = deduped.slice(0, limit).map(withUrl);
|
|
2466
2526
|
await ctx.metrics.write("tool.search_x.cost", totalCost / 1e9);
|
|
2527
|
+
const topLinks = limited.slice(0, 5).map(
|
|
2528
|
+
(t) => `- @${t.author_username} (score: ${t.score.toFixed(1)}): ${t.url}`
|
|
2529
|
+
).join("\n");
|
|
2467
2530
|
return {
|
|
2468
|
-
content: `Search for "${query}"
|
|
2531
|
+
content: `Search for "${query}": ${limited.length} enriched tweets (of ${deduped.length} unique).
|
|
2469
2532
|
|
|
2533
|
+
Top results:
|
|
2534
|
+
${topLinks}
|
|
2535
|
+
|
|
2536
|
+
Synthesis:
|
|
2470
2537
|
${result.synthesisText}`,
|
|
2471
2538
|
data: {
|
|
2472
2539
|
query,
|
|
2473
|
-
mode: "
|
|
2474
|
-
|
|
2540
|
+
mode: "full",
|
|
2541
|
+
items: limited,
|
|
2542
|
+
total_unique: deduped.length,
|
|
2475
2543
|
synthesis: result.synthesisText,
|
|
2476
2544
|
cost_usd: totalCost / 1e9
|
|
2477
2545
|
}
|
|
2478
2546
|
};
|
|
2547
|
+
} catch (err) {
|
|
2548
|
+
return {
|
|
2549
|
+
error: `Search failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2550
|
+
};
|
|
2479
2551
|
}
|
|
2480
|
-
const tweets = await batchLookupTweets(
|
|
2481
|
-
ctx.http,
|
|
2482
|
-
ctx.secrets,
|
|
2483
|
-
ctx.logger,
|
|
2484
|
-
config.x_api_bearer_ref,
|
|
2485
|
-
result.tweetIds,
|
|
2486
|
-
"open_discovery"
|
|
2487
|
-
);
|
|
2488
|
-
const { allHandles, byDomain } = buildAuthorityMaps(config);
|
|
2489
|
-
const scored = scoreTweets(
|
|
2490
|
-
tweets,
|
|
2491
|
-
config.scoring_weights,
|
|
2492
|
-
config.engagement_sub_weights,
|
|
2493
|
-
config.authority_boost,
|
|
2494
|
-
allHandles,
|
|
2495
|
-
byDomain,
|
|
2496
|
-
config.semantic_topics
|
|
2497
|
-
);
|
|
2498
|
-
const deduped = deduplicateTweets(scored, config.dedup_threshold);
|
|
2499
|
-
const limited = deduped.slice(0, limit).map(withUrl);
|
|
2500
|
-
await ctx.metrics.write("tool.search_x.cost", totalCost / 1e9);
|
|
2501
|
-
const topLinks = limited.slice(0, 5).map(
|
|
2502
|
-
(t) => `- @${t.author_username} (score: ${t.score.toFixed(1)}): ${t.url}`
|
|
2503
|
-
).join("\n");
|
|
2504
|
-
return {
|
|
2505
|
-
content: `Search for "${query}": ${limited.length} enriched tweets (of ${deduped.length} unique).
|
|
2506
|
-
|
|
2507
|
-
Top results:
|
|
2508
|
-
${topLinks}
|
|
2509
|
-
|
|
2510
|
-
Synthesis:
|
|
2511
|
-
${result.synthesisText}`,
|
|
2512
|
-
data: {
|
|
2513
|
-
query,
|
|
2514
|
-
mode: "full",
|
|
2515
|
-
items: limited,
|
|
2516
|
-
total_unique: deduped.length,
|
|
2517
|
-
synthesis: result.synthesisText,
|
|
2518
|
-
cost_usd: totalCost / 1e9
|
|
2519
|
-
}
|
|
2520
|
-
};
|
|
2521
2552
|
}
|
|
2522
2553
|
async function handleGetTrending(ctx, params, _runCtx) {
|
|
2523
2554
|
const p = params;
|
|
2524
2555
|
const limit = typeof p.limit === "number" ? p.limit : 20;
|
|
2525
2556
|
const pillar = typeof p.pillar === "string" ? p.pillar : void 0;
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
const
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
})() : config.semantic_topics;
|
|
2537
|
-
if (topics.length === 0) {
|
|
2538
|
-
return {
|
|
2539
|
-
error: pillar ? `No semantic topics match pillar "${pillar}". Available topics: ${config.semantic_topics.join(", ")}` : "No semantic topics configured"
|
|
2540
|
-
};
|
|
2541
|
-
}
|
|
2542
|
-
const queries = buildOpenQueries(topics, [], today);
|
|
2543
|
-
const allTweetIdArrays = [];
|
|
2544
|
-
let totalCost = 0;
|
|
2545
|
-
for (const query of queries) {
|
|
2546
|
-
try {
|
|
2547
|
-
const result = await xaiSearch(
|
|
2548
|
-
ctx.http,
|
|
2549
|
-
ctx.secrets,
|
|
2550
|
-
ctx.logger,
|
|
2551
|
-
config.xai_api_key_ref,
|
|
2552
|
-
{ query: query.text, fromDate: threeDaysAgo, toDate: today, excludedHandles: config.global_excluded }
|
|
2553
|
-
);
|
|
2554
|
-
allTweetIdArrays.push(result.tweetIds);
|
|
2555
|
-
totalCost += result.costUsdTicks;
|
|
2556
|
-
} catch (err) {
|
|
2557
|
-
ctx.logger.warn("Trending query failed", {
|
|
2558
|
-
query: query.text,
|
|
2559
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2557
|
+
try {
|
|
2558
|
+
const config = await getConfig(ctx);
|
|
2559
|
+
await ctx.metrics.write("tool.get_trending.invoked", 1);
|
|
2560
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2561
|
+
const threeDaysAgo = new Date(Date.now() - 3 * 864e5).toISOString().split("T")[0];
|
|
2562
|
+
const topics = pillar ? (() => {
|
|
2563
|
+
const words = pillar.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
|
|
2564
|
+
return config.semantic_topics.filter((t) => {
|
|
2565
|
+
const lower = t.toLowerCase();
|
|
2566
|
+
return words.some((w) => lower.includes(w));
|
|
2560
2567
|
});
|
|
2568
|
+
})() : config.semantic_topics;
|
|
2569
|
+
if (topics.length === 0) {
|
|
2570
|
+
return {
|
|
2571
|
+
error: pillar ? `No semantic topics match pillar "${pillar}". Available topics: ${config.semantic_topics.join(", ")}` : "No semantic topics configured"
|
|
2572
|
+
};
|
|
2561
2573
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2574
|
+
const queries = buildOpenQueries(topics, [], today);
|
|
2575
|
+
const allTweetIdArrays = [];
|
|
2576
|
+
let totalCost = 0;
|
|
2577
|
+
const settled = await Promise.allSettled(
|
|
2578
|
+
queries.map(
|
|
2579
|
+
(query) => xaiSearch(
|
|
2580
|
+
ctx.http,
|
|
2581
|
+
ctx.secrets,
|
|
2582
|
+
ctx.logger,
|
|
2583
|
+
config.xai_api_key_ref,
|
|
2584
|
+
{ query: query.text, fromDate: threeDaysAgo, toDate: today, excludedHandles: config.global_excluded }
|
|
2585
|
+
)
|
|
2586
|
+
)
|
|
2587
|
+
);
|
|
2588
|
+
for (let i = 0; i < settled.length; i++) {
|
|
2589
|
+
const outcome = settled[i];
|
|
2590
|
+
if (outcome.status === "fulfilled") {
|
|
2591
|
+
allTweetIdArrays.push(outcome.value.tweetIds);
|
|
2592
|
+
totalCost += outcome.value.costUsdTicks;
|
|
2593
|
+
} else {
|
|
2594
|
+
ctx.logger.warn("Trending query failed", {
|
|
2595
|
+
query: queries[i].text,
|
|
2596
|
+
error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
const tweetIds = deduplicateIds(allTweetIdArrays);
|
|
2601
|
+
const tweets = await batchLookupTweets(
|
|
2602
|
+
ctx.http,
|
|
2603
|
+
ctx.secrets,
|
|
2604
|
+
ctx.logger,
|
|
2605
|
+
config.x_api_bearer_ref,
|
|
2606
|
+
tweetIds,
|
|
2607
|
+
"open_discovery"
|
|
2608
|
+
);
|
|
2609
|
+
const { allHandles, byDomain } = buildAuthorityMaps(config);
|
|
2610
|
+
const scored = scoreTweets(
|
|
2611
|
+
tweets,
|
|
2612
|
+
config.scoring_weights,
|
|
2613
|
+
config.engagement_sub_weights,
|
|
2614
|
+
config.authority_boost,
|
|
2615
|
+
allHandles,
|
|
2616
|
+
byDomain,
|
|
2617
|
+
config.semantic_topics
|
|
2618
|
+
);
|
|
2619
|
+
const deduped = deduplicateTweets(scored, config.dedup_threshold);
|
|
2620
|
+
const limited = deduped.slice(0, limit).map(withUrl);
|
|
2621
|
+
await ctx.metrics.write("tool.get_trending.cost", totalCost / 1e9);
|
|
2622
|
+
const topLinks = limited.slice(0, 5).map(
|
|
2623
|
+
(t) => `- @${t.author_username} (score: ${t.score.toFixed(1)}): ${t.url}`
|
|
2624
|
+
).join("\n");
|
|
2625
|
+
return {
|
|
2626
|
+
content: `Trending${pillar ? ` in "${pillar}"` : ""}: ${limited.length} tweets across ${queries.length} topics.
|
|
2590
2627
|
|
|
2591
2628
|
Top results:
|
|
2592
2629
|
${topLinks}`,
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2630
|
+
data: {
|
|
2631
|
+
pillar: pillar ?? null,
|
|
2632
|
+
topics_queried: queries.length,
|
|
2633
|
+
items: limited,
|
|
2634
|
+
total_unique: deduped.length,
|
|
2635
|
+
cost_usd: totalCost / 1e9
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
return {
|
|
2640
|
+
error: `Trending search failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2601
2643
|
}
|
|
2602
2644
|
async function handleGetUserTimeline(ctx, params, _runCtx) {
|
|
2603
2645
|
const p = params;
|