peak6-x-intelligence-plugin 0.1.0 → 0.1.2

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/worker.js CHANGED
@@ -901,7 +901,11 @@ var TOOL_NAMES = {
901
901
  getToday: "get-today",
902
902
  getAuthorities: "get-authorities",
903
903
  suggestHandles: "suggest-handles",
904
- trackHandle: "track-handle"
904
+ trackHandle: "track-handle",
905
+ analyzeTopic: "analyze-topic",
906
+ getThread: "get-thread",
907
+ promoteHandle: "promote-handle",
908
+ rateTweet: "rate-tweet"
905
909
  };
906
910
  var ENTITY_TYPES = {
907
911
  tweet: "tweet",
@@ -915,7 +919,8 @@ var STATE_KEYS = {
915
919
  var EVENT_NAMES = {
916
920
  corpusUpdated: "corpus.updated",
917
921
  handlePromoted: "handle.promoted",
918
- gapIdentified: "gap.identified"
922
+ gapIdentified: "gap.identified",
923
+ tweetHighEngagement: "tweet.high-engagement"
919
924
  };
920
925
  var DEFAULT_CONFIG = {
921
926
  company_id: "",
@@ -1017,7 +1022,12 @@ var DEFAULT_CONFIG = {
1017
1022
  },
1018
1023
  tracking_window_days: 14
1019
1024
  },
1020
- corpus_retention_days: 90
1025
+ corpus_retention_days: 90,
1026
+ engagement_thresholds: {
1027
+ noise_floor: 10,
1028
+ expert: 50,
1029
+ escalation: 100
1030
+ }
1021
1031
  };
1022
1032
 
1023
1033
  // src/pipeline/xai-client.ts
@@ -1181,6 +1191,59 @@ async function verifyTweetExists(http, secrets, bearerRef, tweetId) {
1181
1191
  const data = await response.json();
1182
1192
  return (data.data?.length ?? 0) > 0;
1183
1193
  }
1194
+ async function fetchTweetWithConversationId(http, secrets, logger, bearerRef, tweetId) {
1195
+ const bearerToken = await secrets.resolve(bearerRef);
1196
+ const url = `${X_API_BASE}/tweets?ids=${tweetId}&tweet.fields=conversation_id,${TWEET_FIELDS}&expansions=${EXPANSIONS}&user.fields=${USER_FIELDS}`;
1197
+ const response = await http.fetch(url, {
1198
+ method: "GET",
1199
+ headers: { Authorization: `Bearer ${bearerToken}` }
1200
+ });
1201
+ if (!response.ok) {
1202
+ const errorText = await response.text();
1203
+ throw new Error(`X API v2 error ${response.status}: ${errorText}`);
1204
+ }
1205
+ const data = await response.json();
1206
+ if (!data.data?.length) {
1207
+ throw new Error(`Tweet ${tweetId} not found`);
1208
+ }
1209
+ const rawTweet = data.data[0];
1210
+ const userMap = /* @__PURE__ */ new Map();
1211
+ if (data.includes?.users) {
1212
+ for (const user of data.includes.users) userMap.set(user.id, user);
1213
+ }
1214
+ const tweet = mapToEnrichedTweet(rawTweet, userMap.get(rawTweet.author_id), "open_discovery");
1215
+ const conversationId = rawTweet.conversation_id ?? tweetId;
1216
+ logger.debug("Fetched tweet with conversation_id", { tweetId, conversationId });
1217
+ return { tweet, conversationId };
1218
+ }
1219
+ async function fetchConversationTweets(http, secrets, logger, bearerRef, conversationId) {
1220
+ const bearerToken = await secrets.resolve(bearerRef);
1221
+ const query = encodeURIComponent(`conversation_id:${conversationId}`);
1222
+ const url = `${X_API_BASE}/tweets/search/recent?query=${query}&tweet.fields=${TWEET_FIELDS}&expansions=${EXPANSIONS}&user.fields=${USER_FIELDS}&max_results=100`;
1223
+ logger.debug("Fetching conversation tweets", { conversationId });
1224
+ const response = await http.fetch(url, {
1225
+ method: "GET",
1226
+ headers: { Authorization: `Bearer ${bearerToken}` }
1227
+ });
1228
+ if (!response.ok) {
1229
+ const errorText = await response.text();
1230
+ logger.warn("Conversation search failed", { status: response.status, error: errorText });
1231
+ return [];
1232
+ }
1233
+ const data = await response.json();
1234
+ const userMap = /* @__PURE__ */ new Map();
1235
+ if (data.includes?.users) {
1236
+ for (const user of data.includes.users) userMap.set(user.id, user);
1237
+ }
1238
+ const tweets = [];
1239
+ if (data.data) {
1240
+ for (const tweet of data.data) {
1241
+ tweets.push(mapToEnrichedTweet(tweet, userMap.get(tweet.author_id), "open_discovery"));
1242
+ }
1243
+ }
1244
+ logger.debug("Conversation tweets fetched", { conversationId, count: tweets.length });
1245
+ return tweets;
1246
+ }
1184
1247
 
1185
1248
  // src/pipeline/scoring.ts
1186
1249
  function trigrams(text) {
@@ -1253,6 +1316,24 @@ function computeRelevanceScore(tweet, topics) {
1253
1316
  const topicBoost = Math.min(matchCount * 0.1, 0.5);
1254
1317
  return Math.min(0.5 + topicBoost, 1);
1255
1318
  }
1319
+ var SINCE_PATTERN = /^(\d+)([hd])$/;
1320
+ function parseSince(since) {
1321
+ const match = SINCE_PATTERN.exec(since.trim());
1322
+ if (match) {
1323
+ const amount = parseInt(match[1], 10);
1324
+ const unit = match[2];
1325
+ const ms = unit === "h" ? amount * 36e5 : amount * 864e5;
1326
+ return new Date(Date.now() - ms);
1327
+ }
1328
+ if (/^\d{4}-\d{2}-\d{2}/.test(since)) {
1329
+ const d = new Date(since);
1330
+ return isNaN(d.getTime()) ? null : d;
1331
+ }
1332
+ return null;
1333
+ }
1334
+ function computeTotalEngagement(metrics) {
1335
+ return metrics.like_count + metrics.retweet_count + metrics.reply_count + metrics.quote_count;
1336
+ }
1256
1337
  function scoreTweets(tweets, weights, subWeights, authorityBoost, authorityHandles, authorityByDomain, topics, now) {
1257
1338
  const scoreTime = now ?? /* @__PURE__ */ new Date();
1258
1339
  return tweets.map((tweet) => {
@@ -1556,7 +1637,56 @@ function buildHandleUpdatesFromTweets(tweets, topics, authorityByDomain) {
1556
1637
  return [...handleMap.values()];
1557
1638
  }
1558
1639
 
1640
+ // src/pipeline/analyze-topic.ts
1641
+ function buildTopicQueries(topic, dateStr) {
1642
+ return [
1643
+ {
1644
+ text: `What are people saying about ${topic} today (${dateStr})? Focus on significant discussions, breaking developments, and notable opinions.`,
1645
+ topic: "core",
1646
+ phase: "open"
1647
+ },
1648
+ {
1649
+ text: `Which experts, analysts, or thought leaders are currently discussing ${topic}? What insights and perspectives are they sharing?`,
1650
+ topic: "expert_voices",
1651
+ phase: "open"
1652
+ },
1653
+ {
1654
+ text: `What problems, complaints, concerns, or risks are people raising about ${topic}? What pain points are being discussed?`,
1655
+ topic: "pain_points",
1656
+ phase: "open"
1657
+ },
1658
+ {
1659
+ text: `What positive developments, innovations, breakthroughs, or success stories related to ${topic} are being shared?`,
1660
+ topic: "positive_signals",
1661
+ phase: "open"
1662
+ },
1663
+ {
1664
+ text: `What articles, research papers, blog posts, or reports about ${topic} are being shared and discussed on X?`,
1665
+ topic: "link_based",
1666
+ phase: "open"
1667
+ }
1668
+ ];
1669
+ }
1670
+ function crossReferenceCorpus(topic, corpus) {
1671
+ const topicWords = topic.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
1672
+ const threshold = Math.max(1, Math.ceil(topicWords.length * 0.4));
1673
+ return corpus.filter((tweet) => {
1674
+ const textLower = tweet.text.toLowerCase();
1675
+ const matchCount = topicWords.filter((w) => textLower.includes(w)).length;
1676
+ return matchCount >= threshold;
1677
+ }).sort((a, b) => b.score - a.score);
1678
+ }
1679
+
1559
1680
  // src/worker.ts
1681
+ function tweetUrl(authorUsername, tweetId) {
1682
+ if (!authorUsername || authorUsername === "unknown") {
1683
+ return `https://x.com/i/status/${tweetId}`;
1684
+ }
1685
+ return `https://x.com/${authorUsername}/status/${tweetId}`;
1686
+ }
1687
+ function withUrl(tweet) {
1688
+ return { ...tweet, url: tweetUrl(tweet.author_username, tweet.tweet_id) };
1689
+ }
1560
1690
  async function getConfig(ctx) {
1561
1691
  const raw = await ctx.config.get();
1562
1692
  return { ...DEFAULT_CONFIG, ...raw };
@@ -1571,6 +1701,7 @@ function buildAuthorityMaps(config) {
1571
1701
  }
1572
1702
  return { allHandles, byDomain };
1573
1703
  }
1704
+ var pluginCtx = null;
1574
1705
  async function runDiscoveryPipeline(ctx, job) {
1575
1706
  const config = await getConfig(ctx);
1576
1707
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -1697,6 +1828,24 @@ async function runDiscoveryPipeline(ctx, job) {
1697
1828
  afterDedup: deduplicated.length,
1698
1829
  finalCorpus: corpus.length
1699
1830
  });
1831
+ const engagementThresholds = config.engagement_thresholds ?? DEFAULT_CONFIG.engagement_thresholds;
1832
+ for (const tweet of corpus) {
1833
+ const totalEngagement = computeTotalEngagement(tweet.metrics);
1834
+ if (totalEngagement >= engagementThresholds.escalation) {
1835
+ await ctx.events.emit(EVENT_NAMES.tweetHighEngagement, config.company_id, {
1836
+ tweet_id: tweet.tweet_id,
1837
+ author_username: tweet.author_username,
1838
+ text: tweet.text.slice(0, 280),
1839
+ url: tweetUrl(tweet.author_username, tweet.tweet_id),
1840
+ score: tweet.score,
1841
+ total_engagement: totalEngagement,
1842
+ metrics: tweet.metrics,
1843
+ is_authority: tweet.is_authority,
1844
+ threshold_crossed: "escalation"
1845
+ });
1846
+ await ctx.metrics.write("pipeline.tweet.high_engagement", 1);
1847
+ }
1848
+ }
1700
1849
  for (const tweet of corpus) {
1701
1850
  await ctx.entities.upsert({
1702
1851
  entityType: ENTITY_TYPES.tweet,
@@ -1748,7 +1897,18 @@ async function runDiscoveryPipeline(ctx, job) {
1748
1897
  await ctx.events.emit(EVENT_NAMES.corpusUpdated, config.company_id, {
1749
1898
  date: today,
1750
1899
  corpusSize: corpus.length,
1751
- topScore: corpus[0]?.score ?? 0
1900
+ topScore: corpus[0]?.score ?? 0,
1901
+ top_items: corpus.slice(0, 5).map((t) => ({
1902
+ tweet_id: t.tweet_id,
1903
+ author_username: t.author_username,
1904
+ score: t.score,
1905
+ text: t.text.slice(0, 140),
1906
+ url: tweetUrl(t.author_username, t.tweet_id),
1907
+ is_authority: t.is_authority
1908
+ })),
1909
+ stats: summary.discovery_stats,
1910
+ authority_count: Object.values(config.authority_lists).reduce((n, l) => n + l.handles.length, 0),
1911
+ handle_candidates: handleStats.discovered
1752
1912
  });
1753
1913
  for (const promo of promotions) {
1754
1914
  if (promo.status === "promoted") {
@@ -1847,25 +2007,34 @@ async function handleSearchCorpus(ctx, params, _runCtx) {
1847
2007
  const date = typeof p.date === "string" ? p.date : void 0;
1848
2008
  const pillar = typeof p.pillar === "string" ? p.pillar.toLowerCase() : void 0;
1849
2009
  const limit = typeof p.limit === "number" ? p.limit : 20;
2010
+ const minScore = typeof p.min_score === "number" ? p.min_score : 0;
2011
+ const minEngagement = typeof p.min_engagement === "number" ? p.min_engagement : 0;
2012
+ const since = typeof p.since === "string" ? p.since : void 0;
2013
+ const sinceDate = since ? parseSince(since) : null;
1850
2014
  const tweets = await ctx.entities.list({
1851
2015
  entityType: ENTITY_TYPES.tweet,
1852
- limit: 200
2016
+ limit: 500
1853
2017
  });
1854
2018
  const results = [];
1855
2019
  for (const tweet of tweets) {
1856
2020
  if (tweet.status === "removed" || tweet.status === "archived") continue;
1857
2021
  const data = tweet.data;
1858
- const textMatch = query ? data.text.toLowerCase().includes(query) : true;
1859
- const dateMatch = date ? data.created_at?.startsWith(date) ?? false : true;
1860
- const pillarMatch = pillar ? data.authority_domains?.some((d) => d.toLowerCase().includes(pillar)) ?? false : true;
1861
- if (textMatch && dateMatch && pillarMatch) {
1862
- results.push(data);
1863
- }
2022
+ if (query && !data.text.toLowerCase().includes(query)) continue;
2023
+ if (date && !(data.created_at?.startsWith(date) ?? false)) continue;
2024
+ if (sinceDate && new Date(data.created_at) < sinceDate) continue;
2025
+ if (pillar && !(data.authority_domains?.some((d) => d.toLowerCase().includes(pillar)) ?? false)) continue;
2026
+ if (data.score < minScore) continue;
2027
+ if (minEngagement > 0 && computeTotalEngagement(data.metrics) < minEngagement) continue;
2028
+ results.push(data);
1864
2029
  }
1865
2030
  results.sort((a, b) => b.score - a.score);
1866
- const limited = results.slice(0, limit);
2031
+ const limited = results.slice(0, limit).map(withUrl);
2032
+ const topLinks = limited.slice(0, 5).map((t) => `- @${t.author_username} (score: ${t.score.toFixed(1)}): ${t.url}`).join("\n");
1867
2033
  return {
1868
- content: `Found ${limited.length} tweets matching "${query}"`,
2034
+ content: `Found ${limited.length} of ${results.length} tweets matching "${query}"
2035
+
2036
+ Top results:
2037
+ ${topLinks}`,
1869
2038
  data: limited
1870
2039
  };
1871
2040
  }
@@ -1873,27 +2042,38 @@ async function handleGetToday(ctx, params, _runCtx) {
1873
2042
  const p = params;
1874
2043
  const pillar = typeof p.pillar === "string" ? p.pillar : void 0;
1875
2044
  const limit = typeof p.limit === "number" ? p.limit : 20;
2045
+ const minScore = typeof p.min_score === "number" ? p.min_score : 0;
1876
2046
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2047
+ const tweets = await ctx.entities.list({
2048
+ entityType: ENTITY_TYPES.tweet,
2049
+ limit: 500
2050
+ });
2051
+ const results = [];
2052
+ for (const tweet of tweets) {
2053
+ if (tweet.status === "removed" || tweet.status === "archived") continue;
2054
+ const data = tweet.data;
2055
+ if (!data.created_at?.startsWith(today)) continue;
2056
+ if (data.score < minScore) continue;
2057
+ if (pillar && !(data.authority_domains?.some((d) => d.toLowerCase().includes(pillar.toLowerCase())) ?? false)) continue;
2058
+ results.push(data);
2059
+ }
2060
+ results.sort((a, b) => b.score - a.score);
2061
+ const limited = results.slice(0, limit).map(withUrl);
1877
2062
  const summary = await ctx.state.get({
1878
2063
  scopeKind: "instance",
1879
2064
  stateKey: `${STATE_KEYS.corpusPrefix}${today}`
1880
2065
  });
1881
- if (!summary) {
1882
- return { content: "No corpus available for today. Discovery may not have run yet." };
1883
- }
1884
- let items = summary.top_items;
1885
- if (pillar) {
1886
- items = items.filter(
1887
- (t) => t.authority_domains?.some((d) => d.toLowerCase().includes(pillar.toLowerCase())) ?? false
1888
- );
1889
- }
2066
+ const topLinks = limited.slice(0, 5).map((t) => `- @${t.author_username} (score: ${t.score.toFixed(1)}): ${t.url}`).join("\n");
1890
2067
  return {
1891
- content: `Today's corpus: ${summary.total_tweets} tweets. Showing top ${Math.min(limit, items.length)}.`,
2068
+ content: `Today's corpus: ${results.length} tweets total. Showing top ${limited.length}.
2069
+
2070
+ Top results:
2071
+ ${topLinks}`,
1892
2072
  data: {
1893
- date: summary.date,
1894
- total: summary.total_tweets,
1895
- items: items.slice(0, limit),
1896
- stats: summary.discovery_stats
2073
+ date: today,
2074
+ total: results.length,
2075
+ items: limited,
2076
+ stats: summary?.discovery_stats ?? null
1897
2077
  }
1898
2078
  };
1899
2079
  }
@@ -1967,8 +2147,221 @@ async function handleTrackHandle(ctx, params, _runCtx) {
1967
2147
  content: `Handle @${handle} tracked with relevance ${relevance} in domain "${domain}"`
1968
2148
  };
1969
2149
  }
2150
+ async function handleAnalyzeTopic(ctx, params, _runCtx) {
2151
+ const p = params;
2152
+ const topic = typeof p.topic === "string" ? p.topic : "";
2153
+ const includeCorpus = p.include_corpus !== false;
2154
+ if (!topic) return { error: "topic is required" };
2155
+ const config = await getConfig(ctx);
2156
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2157
+ await ctx.metrics.write("tool.analyze_topic.invoked", 1);
2158
+ const queries = buildTopicQueries(topic, today);
2159
+ const freshResults = [];
2160
+ let totalCost = 0;
2161
+ for (const query of queries) {
2162
+ try {
2163
+ const result2 = await xaiSearch(
2164
+ ctx.http,
2165
+ ctx.secrets,
2166
+ ctx.logger,
2167
+ config.xai_api_key_ref,
2168
+ { query: query.text, fromDate: today, toDate: today, excludedHandles: config.global_excluded }
2169
+ );
2170
+ freshResults.push({
2171
+ tweet_ids: result2.tweetIds,
2172
+ query_type: query.topic,
2173
+ synthesis_text: result2.synthesisText
2174
+ });
2175
+ totalCost += result2.costUsdTicks;
2176
+ } catch (err) {
2177
+ ctx.logger.warn("Analyze topic sub-query failed", {
2178
+ queryType: query.topic,
2179
+ error: err instanceof Error ? err.message : String(err)
2180
+ });
2181
+ }
2182
+ }
2183
+ let corpusMatches = [];
2184
+ if (includeCorpus) {
2185
+ const allTweets = await ctx.entities.list({ entityType: ENTITY_TYPES.tweet, limit: 500 });
2186
+ const corpus = allTweets.filter((t) => t.status === "active").map((t) => t.data);
2187
+ corpusMatches = crossReferenceCorpus(topic, corpus);
2188
+ }
2189
+ await ctx.metrics.write("tool.analyze_topic.cost", totalCost / 1e9);
2190
+ const corpusWithUrls = corpusMatches.slice(0, 10).map(withUrl);
2191
+ const result = {
2192
+ topic,
2193
+ synthesis: freshResults.map((r) => `## ${r.query_type}
2194
+ ${r.synthesis_text}`).join("\n\n"),
2195
+ corpus_matches: corpusWithUrls,
2196
+ fresh_results: freshResults,
2197
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
2198
+ };
2199
+ const matchLinks = corpusWithUrls.slice(0, 5).map((t) => `- @${t.author_username}: ${t.url}`).join("\n");
2200
+ return {
2201
+ content: `Topic analysis for "${topic}": ${freshResults.length} queries completed, ${corpusMatches.length} corpus matches found.${matchLinks ? `
2202
+
2203
+ Top corpus matches:
2204
+ ${matchLinks}` : ""}`,
2205
+ data: result
2206
+ };
2207
+ }
2208
+ async function handleGetThread(ctx, params, _runCtx) {
2209
+ const p = params;
2210
+ const tweetId = typeof p.tweet_id === "string" ? p.tweet_id : "";
2211
+ if (!tweetId) return { error: "tweet_id is required" };
2212
+ const config = await getConfig(ctx);
2213
+ await ctx.metrics.write("tool.get_thread.invoked", 1);
2214
+ try {
2215
+ const { tweet, conversationId } = await fetchTweetWithConversationId(
2216
+ ctx.http,
2217
+ ctx.secrets,
2218
+ ctx.logger,
2219
+ config.x_api_bearer_ref,
2220
+ tweetId
2221
+ );
2222
+ const conversationTweets = await fetchConversationTweets(
2223
+ ctx.http,
2224
+ ctx.secrets,
2225
+ ctx.logger,
2226
+ config.x_api_bearer_ref,
2227
+ conversationId
2228
+ );
2229
+ const allThreadTweets = [tweet, ...conversationTweets.filter((t) => t.tweet_id !== tweetId)].map(withUrl);
2230
+ const result = {
2231
+ root_tweet_id: tweetId,
2232
+ conversation_id: conversationId,
2233
+ tweets: allThreadTweets,
2234
+ total_in_conversation: conversationTweets.length + 1
2235
+ };
2236
+ const threadLinks = allThreadTweets.map((t) => `- @${t.author_username}: ${t.url}`).join("\n");
2237
+ return {
2238
+ content: `Thread for tweet ${tweetId}: ${result.tweets.length} tweets in conversation ${conversationId}
2239
+
2240
+ ${threadLinks}`,
2241
+ data: result
2242
+ };
2243
+ } catch (err) {
2244
+ return {
2245
+ error: `Failed to fetch thread: ${err instanceof Error ? err.message : String(err)}`
2246
+ };
2247
+ }
2248
+ }
2249
+ async function handlePromoteHandle(ctx, params, runCtx) {
2250
+ const p = params;
2251
+ const handle = typeof p.handle === "string" ? p.handle.toLowerCase().replace(/^@/, "") : "";
2252
+ const listName = typeof p.list_name === "string" ? p.list_name : "";
2253
+ const note = typeof p.note === "string" ? p.note : "";
2254
+ if (!handle) return { error: "handle is required" };
2255
+ if (!listName) return { error: "list_name is required" };
2256
+ const config = await getConfig(ctx);
2257
+ if (!config.authority_lists[listName]) {
2258
+ return { error: `Authority list "${listName}" not found. Available: ${Object.keys(config.authority_lists).join(", ")}` };
2259
+ }
2260
+ const existing = await ctx.entities.list({
2261
+ entityType: ENTITY_TYPES.handle,
2262
+ externalId: handle,
2263
+ limit: 1
2264
+ });
2265
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2266
+ if (existing.length > 0) {
2267
+ const record = existing[0];
2268
+ const data = record.data;
2269
+ await ctx.entities.upsert({
2270
+ entityType: ENTITY_TYPES.handle,
2271
+ scopeKind: "instance",
2272
+ externalId: handle,
2273
+ title: handle,
2274
+ status: "promoted",
2275
+ data: {
2276
+ ...data,
2277
+ promoted_to_list: listName,
2278
+ notes: note ? `${data.notes ? data.notes + "; " : ""}Promoted to ${listName}: ${note}` : data.notes
2279
+ }
2280
+ });
2281
+ } else {
2282
+ const data = {
2283
+ first_seen: now.split("T")[0],
2284
+ last_seen: now.split("T")[0],
2285
+ appearances: 0,
2286
+ avg_relevance: 0,
2287
+ domains: [listName],
2288
+ followed_by_existing: [],
2289
+ followers_count: 0,
2290
+ sample_tweet_ids: [],
2291
+ promoted_to_list: listName,
2292
+ notes: note ? `Manually promoted to ${listName}: ${note}` : `Manually promoted to ${listName}`
2293
+ };
2294
+ await ctx.entities.upsert({
2295
+ entityType: ENTITY_TYPES.handle,
2296
+ scopeKind: "instance",
2297
+ externalId: handle,
2298
+ title: handle,
2299
+ status: "promoted",
2300
+ data
2301
+ });
2302
+ }
2303
+ await ctx.events.emit(EVENT_NAMES.handlePromoted, config.company_id, {
2304
+ handle,
2305
+ list_name: listName,
2306
+ promoted_by: runCtx.agentId ?? "manual",
2307
+ note
2308
+ });
2309
+ await ctx.activity.log({
2310
+ companyId: config.company_id,
2311
+ message: `Handle @${handle} promoted to authority list "${listName}"`,
2312
+ metadata: { list_name: listName, note }
2313
+ });
2314
+ return {
2315
+ content: `Handle @${handle} promoted to authority list "${listName}"`
2316
+ };
2317
+ }
2318
+ async function handleRateTweet(ctx, params, runCtx) {
2319
+ const p = params;
2320
+ const tweetId = typeof p.tweet_id === "string" ? p.tweet_id : "";
2321
+ const rating = typeof p.rating === "string" ? p.rating : "";
2322
+ const ratedBy = typeof p.rated_by === "string" ? p.rated_by : runCtx.agentId ?? "unknown";
2323
+ if (!tweetId) return { error: "tweet_id is required" };
2324
+ if (!["relevant", "irrelevant", "skip"].includes(rating)) {
2325
+ return { error: 'rating must be "relevant", "irrelevant", or "skip"' };
2326
+ }
2327
+ const existing = await ctx.entities.list({
2328
+ entityType: ENTITY_TYPES.tweet,
2329
+ externalId: tweetId,
2330
+ limit: 1
2331
+ });
2332
+ if (existing.length === 0) {
2333
+ return { error: `Tweet ${tweetId} not found in corpus` };
2334
+ }
2335
+ const record = existing[0];
2336
+ const data = record.data;
2337
+ const existingRatings = Array.isArray(data.ratings) ? data.ratings : [];
2338
+ const newRating = {
2339
+ rated_by: ratedBy,
2340
+ rated_at: (/* @__PURE__ */ new Date()).toISOString(),
2341
+ value: rating
2342
+ };
2343
+ await ctx.entities.upsert({
2344
+ entityType: ENTITY_TYPES.tweet,
2345
+ scopeKind: "instance",
2346
+ externalId: tweetId,
2347
+ title: record.title ?? tweetId,
2348
+ status: record.status ?? "active",
2349
+ data: {
2350
+ ...data,
2351
+ ratings: [...existingRatings, newRating]
2352
+ }
2353
+ });
2354
+ await ctx.metrics.write("tool.rate_tweet.invoked", 1);
2355
+ const data2 = record.data;
2356
+ const ratedUrl = data2.author_username ? tweetUrl(data2.author_username, tweetId) : `https://x.com/i/status/${tweetId}`;
2357
+ return {
2358
+ content: `Tweet ${tweetId} rated as "${rating}" by ${ratedBy}. Total ratings: ${existingRatings.length + 1}
2359
+ ${ratedUrl}`
2360
+ };
2361
+ }
1970
2362
  var plugin = definePlugin({
1971
2363
  async setup(ctx) {
2364
+ pluginCtx = ctx;
1972
2365
  ctx.jobs.register(JOB_KEYS.discoveryRun, (job) => runDiscoveryPipeline(ctx, job));
1973
2366
  ctx.jobs.register(JOB_KEYS.authorityDecay, (job) => handleAuthorityDecay(ctx, job));
1974
2367
  ctx.jobs.register(JOB_KEYS.complianceCheck, (job) => handleComplianceCheck(ctx, job));
@@ -1998,6 +2391,26 @@ var plugin = definePlugin({
1998
2391
  { displayName: "Track Handle", description: "Record a handle appearance", parametersSchema: {} },
1999
2392
  (params, runCtx) => handleTrackHandle(ctx, params, runCtx)
2000
2393
  );
2394
+ ctx.tools.register(
2395
+ TOOL_NAMES.analyzeTopic,
2396
+ { displayName: "Analyze Topic", description: "On-demand xAI topic analysis with 5-perspective decomposition and corpus cross-reference", parametersSchema: {} },
2397
+ (params, runCtx) => handleAnalyzeTopic(ctx, params, runCtx)
2398
+ );
2399
+ ctx.tools.register(
2400
+ TOOL_NAMES.getThread,
2401
+ { displayName: "Get Tweet Thread", description: "Fetch conversation context for a tweet", parametersSchema: {} },
2402
+ (params, runCtx) => handleGetThread(ctx, params, runCtx)
2403
+ );
2404
+ ctx.tools.register(
2405
+ TOOL_NAMES.promoteHandle,
2406
+ { displayName: "Promote Handle", description: "Promote a handle to authority status in a specific list", parametersSchema: {} },
2407
+ (params, runCtx) => handlePromoteHandle(ctx, params, runCtx)
2408
+ );
2409
+ ctx.tools.register(
2410
+ TOOL_NAMES.rateTweet,
2411
+ { displayName: "Rate Tweet", description: "Record relevance feedback on a corpus tweet", parametersSchema: {} },
2412
+ (params, runCtx) => handleRateTweet(ctx, params, runCtx)
2413
+ );
2001
2414
  ctx.data.register("dashboard-summary", async () => {
2002
2415
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2003
2416
  const summary = await ctx.state.get({
@@ -2028,6 +2441,57 @@ var plugin = definePlugin({
2028
2441
  ctx.data.register("plugin-config", async () => {
2029
2442
  return await ctx.config.get();
2030
2443
  });
2444
+ ctx.data.register("corpus-browser", async (params) => {
2445
+ const p = params;
2446
+ const date = typeof p.date === "string" ? p.date : (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2447
+ const pillar = typeof p.pillar === "string" ? p.pillar : void 0;
2448
+ const minScore = typeof p.min_score === "number" ? p.min_score : 0;
2449
+ const minEngagement = typeof p.min_engagement === "number" ? p.min_engagement : 0;
2450
+ const authorityOnly = p.authority_only === true;
2451
+ const page = typeof p.page === "number" ? p.page : 1;
2452
+ const pageSize = typeof p.page_size === "number" ? p.page_size : 25;
2453
+ const allTweets = await ctx.entities.list({
2454
+ entityType: ENTITY_TYPES.tweet,
2455
+ limit: 500
2456
+ });
2457
+ const filtered = [];
2458
+ for (const tweet of allTweets) {
2459
+ if (tweet.status === "removed" || tweet.status === "archived") continue;
2460
+ const data = tweet.data;
2461
+ if (!data.created_at?.startsWith(date)) continue;
2462
+ if (data.score < minScore) continue;
2463
+ if (pillar && !(data.authority_domains?.some((d) => d.toLowerCase().includes(pillar.toLowerCase())) ?? false)) continue;
2464
+ if (minEngagement > 0 && computeTotalEngagement(data.metrics) < minEngagement) continue;
2465
+ if (authorityOnly && !data.is_authority) continue;
2466
+ filtered.push(data);
2467
+ }
2468
+ filtered.sort((a, b) => b.score - a.score);
2469
+ const totalCount = filtered.length;
2470
+ const totalPages = Math.ceil(totalCount / pageSize) || 1;
2471
+ const start = (page - 1) * pageSize;
2472
+ const items = filtered.slice(start, start + pageSize).map(withUrl);
2473
+ const dates = [];
2474
+ for (let i = 0; i < 7; i++) {
2475
+ const d = /* @__PURE__ */ new Date();
2476
+ d.setDate(d.getDate() - i);
2477
+ dates.push(d.toISOString().split("T")[0]);
2478
+ }
2479
+ const config = await getConfig(ctx);
2480
+ const result = {
2481
+ items,
2482
+ pagination: { page, pageSize, totalCount, totalPages },
2483
+ filters: {
2484
+ date,
2485
+ pillar: pillar ?? null,
2486
+ minScore,
2487
+ minEngagement,
2488
+ authorityOnly
2489
+ },
2490
+ available_pillars: config.content_pillars,
2491
+ available_dates: dates
2492
+ };
2493
+ return result;
2494
+ });
2031
2495
  let discoveryRunning = false;
2032
2496
  ctx.actions.register("trigger-discovery", async () => {
2033
2497
  if (discoveryRunning) {
@@ -2049,7 +2513,66 @@ var plugin = definePlugin({
2049
2513
  ctx.logger.info("X Intelligence plugin initialized");
2050
2514
  },
2051
2515
  async onHealth() {
2052
- return { status: "ok", message: "X Intelligence plugin is running" };
2516
+ if (!pluginCtx) return { status: "error", message: "Plugin not initialized" };
2517
+ try {
2518
+ const config = await getConfig(pluginCtx);
2519
+ const lastRun = await pluginCtx.state.get({
2520
+ scopeKind: "instance",
2521
+ stateKey: STATE_KEYS.lastDiscoveryRun
2522
+ });
2523
+ const now = /* @__PURE__ */ new Date();
2524
+ const lastRunDate = lastRun?.generatedAt ? new Date(lastRun.generatedAt) : null;
2525
+ const hoursSinceLastRun = lastRunDate ? (now.getTime() - lastRunDate.getTime()) / (1e3 * 60 * 60) : null;
2526
+ const status = hoursSinceLastRun === null ? "degraded" : hoursSinceLastRun > 36 ? "degraded" : "ok";
2527
+ return {
2528
+ status,
2529
+ message: lastRun ? `Last discovery: ${lastRun.date} (${Math.round(hoursSinceLastRun)}h ago)` : "No discovery runs yet",
2530
+ details: {
2531
+ lastDiscoveryRun: lastRun?.date ?? null,
2532
+ hoursSinceLastRun: hoursSinceLastRun ? Math.round(hoursSinceLastRun) : null,
2533
+ configuredTopics: config.semantic_topics.length,
2534
+ configuredAuthorities: Object.keys(config.authority_lists).length
2535
+ }
2536
+ };
2537
+ } catch (err) {
2538
+ return {
2539
+ status: "error",
2540
+ message: `Health check failed: ${err instanceof Error ? err.message : String(err)}`
2541
+ };
2542
+ }
2543
+ },
2544
+ async onValidateConfig(config) {
2545
+ const warnings = [];
2546
+ const errors = [];
2547
+ const xaiRef = config.xai_api_key_ref;
2548
+ const xRef = config.x_api_bearer_ref;
2549
+ if (!xaiRef) errors.push("xai_api_key_ref is required");
2550
+ if (!xRef) errors.push("x_api_bearer_ref is required");
2551
+ if (pluginCtx && xaiRef) {
2552
+ try {
2553
+ const key = await pluginCtx.secrets.resolve(xaiRef);
2554
+ if (!key || key.length < 10) warnings.push("xAI API key appears invalid (too short)");
2555
+ } catch {
2556
+ errors.push(`Cannot resolve xAI API key secret: ${xaiRef}`);
2557
+ }
2558
+ }
2559
+ if (pluginCtx && xRef) {
2560
+ try {
2561
+ const key = await pluginCtx.secrets.resolve(xRef);
2562
+ if (!key || key.length < 10) warnings.push("X API bearer token appears invalid (too short)");
2563
+ } catch {
2564
+ errors.push(`Cannot resolve X API bearer secret: ${xRef}`);
2565
+ }
2566
+ }
2567
+ const sw = config.scoring_weights;
2568
+ if (sw) {
2569
+ const sum = (sw.relevance ?? 0) + (sw.recency ?? 0) + (sw.engagement ?? 0);
2570
+ if (Math.abs(sum - 1) > 0.05) warnings.push(`Scoring weights sum to ${sum.toFixed(2)}, expected ~1.0`);
2571
+ }
2572
+ if (config.semantic_topics?.length === 0) {
2573
+ warnings.push("No semantic topics configured \u2014 discovery will produce no results");
2574
+ }
2575
+ return { ok: errors.length === 0, warnings, errors };
2053
2576
  }
2054
2577
  });
2055
2578
  var worker_default = plugin;