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/README.md +25 -10
- package/dist/manifest.js +102 -25
- package/dist/manifest.js.map +2 -2
- package/dist/ui/index.js +261 -22
- package/dist/ui/index.js.map +2 -2
- package/dist/worker.js +551 -28
- package/dist/worker.js.map +4 -4
- package/package.json +10 -16
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:
|
|
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
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
if (
|
|
1862
|
-
|
|
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
|
-
|
|
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: ${
|
|
2068
|
+
content: `Today's corpus: ${results.length} tweets total. Showing top ${limited.length}.
|
|
2069
|
+
|
|
2070
|
+
Top results:
|
|
2071
|
+
${topLinks}`,
|
|
1892
2072
|
data: {
|
|
1893
|
-
date:
|
|
1894
|
-
total:
|
|
1895
|
-
items:
|
|
1896
|
-
stats: summary
|
|
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: "
|
|
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;
|