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 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
- The `x-discovery-engine` skill (source: [peak6-labs/x-discovery-engine-skill](https://github.com/peak6-labs/x-discovery-engine-skill)) teaches agents the full workflow.
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 globalThis.fetch(XAI_RESPONSES_URL, {
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.fetch(url, {
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.fetch(url, {
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.fetch(url, {
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.fetch(url, {
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.fetch(userUrl, {
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.fetch(timelineUrl, {
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
- const config = await getConfig(ctx);
2232
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2233
- await ctx.metrics.write("tool.analyze_topic.invoked", 1);
2234
- const queries = buildTopicQueries(topic, today);
2235
- const freshResults = [];
2236
- let totalCost = 0;
2237
- for (const query of queries) {
2238
- try {
2239
- const result2 = await xaiSearch(
2240
- ctx.http,
2241
- ctx.secrets,
2242
- ctx.logger,
2243
- config.xai_api_key_ref,
2244
- { query: query.text, fromDate: today, toDate: today, excludedHandles: config.global_excluded }
2245
- );
2246
- freshResults.push({
2247
- tweet_ids: result2.tweetIds,
2248
- query_type: query.topic,
2249
- synthesis_text: result2.synthesisText
2250
- });
2251
- totalCost += result2.costUsdTicks;
2252
- } catch (err) {
2253
- ctx.logger.warn("Analyze topic sub-query failed", {
2254
- queryType: query.topic,
2255
- error: err instanceof Error ? err.message : String(err)
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
- let corpusMatches = [];
2260
- if (includeCorpus) {
2261
- const allTweets = await ctx.entities.list({ entityType: ENTITY_TYPES.tweet, limit: 500 });
2262
- const corpus = allTweets.filter((t) => t.status === "active").map((t) => t.data);
2263
- corpusMatches = crossReferenceCorpus(topic, corpus);
2264
- }
2265
- await ctx.metrics.write("tool.analyze_topic.cost", totalCost / 1e9);
2266
- const corpusWithUrls = corpusMatches.slice(0, 10).map(withUrl);
2267
- const result = {
2268
- topic,
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
- corpus_matches: corpusWithUrls,
2272
- fresh_results: freshResults,
2273
- generated_at: (/* @__PURE__ */ new Date()).toISOString()
2274
- };
2275
- const matchLinks = corpusWithUrls.slice(0, 5).map((t) => `- @${t.author_username}: ${t.url}`).join("\n");
2276
- return {
2277
- content: `Topic analysis for "${topic}": ${freshResults.length} queries completed, ${corpusMatches.length} corpus matches found.${matchLinks ? `
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
- data: result
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
- const config = await getConfig(ctx);
2446
- await ctx.metrics.write("tool.search_x.invoked", 1);
2447
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2448
- const threeDaysAgo = new Date(Date.now() - 3 * 864e5).toISOString().split("T")[0];
2449
- let totalCost = 0;
2450
- const result = await xaiSearch(
2451
- ctx.http,
2452
- ctx.secrets,
2453
- ctx.logger,
2454
- config.xai_api_key_ref,
2455
- {
2456
- query,
2457
- fromDate: threeDaysAgo,
2458
- toDate: today,
2459
- allowedHandles: handles,
2460
- excludedHandles: handles ? void 0 : config.global_excluded
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
- totalCost += result.costUsdTicks;
2464
- if (quick) {
2465
- const limitedIds = result.tweetIds.slice(0, limit);
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}" (quick mode): ${limitedIds.length} tweets found.
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: "quick",
2474
- tweet_ids: limitedIds,
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
- const config = await getConfig(ctx);
2527
- await ctx.metrics.write("tool.get_trending.invoked", 1);
2528
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2529
- const threeDaysAgo = new Date(Date.now() - 3 * 864e5).toISOString().split("T")[0];
2530
- const topics = pillar ? (() => {
2531
- const words = pillar.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
2532
- return config.semantic_topics.filter((t) => {
2533
- const lower = t.toLowerCase();
2534
- return words.some((w) => lower.includes(w));
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
- const tweetIds = deduplicateIds(allTweetIdArrays);
2564
- const tweets = await batchLookupTweets(
2565
- ctx.http,
2566
- ctx.secrets,
2567
- ctx.logger,
2568
- config.x_api_bearer_ref,
2569
- tweetIds,
2570
- "open_discovery"
2571
- );
2572
- const { allHandles, byDomain } = buildAuthorityMaps(config);
2573
- const scored = scoreTweets(
2574
- tweets,
2575
- config.scoring_weights,
2576
- config.engagement_sub_weights,
2577
- config.authority_boost,
2578
- allHandles,
2579
- byDomain,
2580
- config.semantic_topics
2581
- );
2582
- const deduped = deduplicateTweets(scored, config.dedup_threshold);
2583
- const limited = deduped.slice(0, limit).map(withUrl);
2584
- await ctx.metrics.write("tool.get_trending.cost", totalCost / 1e9);
2585
- const topLinks = limited.slice(0, 5).map(
2586
- (t) => `- @${t.author_username} (score: ${t.score.toFixed(1)}): ${t.url}`
2587
- ).join("\n");
2588
- return {
2589
- content: `Trending${pillar ? ` in "${pillar}"` : ""}: ${limited.length} tweets across ${queries.length} topics.
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
- data: {
2594
- pillar: pillar ?? null,
2595
- topics_queried: queries.length,
2596
- items: limited,
2597
- total_unique: deduped.length,
2598
- cost_usd: totalCost / 1e9
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;