@@ -108,43 +108,114 @@ async function fetchAllSources(ctx: Context): Promise<Mention[]> {
108108 ) ;
109109}
110110
111- async function searchWeb ( _ctx : Context ) : Promise < Mention [ ] > {
112- // TODO: wire to Tavily — https://tavily.com (good free tier, returns clean
113- // JSON tuned for LLM consumption). Set `TAVILY_API_KEY` via `relay providers
114- // connect tavily` once the runtime supports custom providers; for now the
115- // env var is read at agent boot.
116- //
117- // const res = await fetch("https://api.tavily.com/search", {
118- // method: "POST",
119- // headers: { "content-type": "application/json" },
120- // body: JSON.stringify({
121- // api_key: process.env.TAVILY_API_KEY,
122- // query: "proactive agents",
123- // time_range: "week",
124- // max_results: 20,
125- // }),
126- // signal: _ctx.signal,
127- // });
128- // const data = (await res.json()) as { results: { url: string; title: string; content: string; published_date: string }[] };
129- // return data.results.map((r) => ({ source: "web", url: r.url, title: r.title, excerpt: r.content.slice(0, 280), publishedAt: r.published_date }));
130- return [ ] ;
111+ async function searchWeb ( ctx : Context ) : Promise < Mention [ ] > {
112+ const results = await braveSearch ( ctx , '"proactive agents"' , { freshness : "pw" , count : 20 } ) ;
113+ return results . map ( ( r ) => ( {
114+ source : "web" ,
115+ url : r . url ,
116+ title : r . title ,
117+ excerpt : r . description . slice ( 0 , 280 ) ,
118+ publishedAt : r . age ?? new Date ( ) . toISOString ( ) ,
119+ } ) ) ;
120+ }
121+
122+ async function searchReddit ( ctx : Context , subreddit : string ) : Promise < Mention [ ] > {
123+ // Try Reddit's JSON endpoint first. Distinctive UA matters more than rate
124+ // here — generic UAs get blocked first; weekly cadence is well under any
125+ // limit. If we get 4xx/5xx or empty, fall back to Brave site:reddit.com.
126+ try {
127+ const res = await fetch (
128+ `https://www.reddit.com/r/${ subreddit } /search.json?q=${ encodeURIComponent ( '"proactive agents"' ) } &sort=new&t=week&restrict_sr=1` ,
129+ {
130+ headers : { "user-agent" : "proactive-agents-digest/0.1 (by /u/khaliqgant)" } ,
131+ signal : ctx . signal ,
132+ } ,
133+ ) ;
134+ if ( ! res . ok ) throw new Error ( `reddit ${ res . status } ` ) ;
135+ const data = ( await res . json ( ) ) as {
136+ data : {
137+ children : {
138+ data : { url : string ; title : string ; selftext : string ; created_utc : number } ;
139+ } [ ] ;
140+ } ;
141+ } ;
142+ if ( data . data . children . length === 0 ) throw new Error ( "reddit empty" ) ;
143+ return data . data . children . map ( ( c ) => ( {
144+ source : `reddit:${ subreddit } ` as Mention [ "source" ] ,
145+ url : c . data . url ,
146+ title : c . data . title ,
147+ excerpt : c . data . selftext . slice ( 0 , 280 ) ,
148+ publishedAt : new Date ( c . data . created_utc * 1000 ) . toISOString ( ) ,
149+ } ) ) ;
150+ } catch ( err ) {
151+ ctx . logger . warn ( "reddit failed, falling back to brave" , {
152+ subreddit,
153+ reason : String ( err ) ,
154+ } ) ;
155+ const results = await braveSearch (
156+ ctx ,
157+ `site:reddit.com/r/${ subreddit } "proactive agents"` ,
158+ { freshness : "pw" , count : 10 } ,
159+ ) ;
160+ return results . map ( ( r ) => ( {
161+ source : `reddit:${ subreddit } ` as Mention [ "source" ] ,
162+ url : r . url ,
163+ title : r . title ,
164+ excerpt : r . description . slice ( 0 , 280 ) ,
165+ publishedAt : r . age ?? new Date ( ) . toISOString ( ) ,
166+ } ) ) ;
167+ }
131168}
132169
133- async function searchReddit ( _ctx : Context , subreddit : string ) : Promise < Mention [ ] > {
134- // Reddit's JSON endpoint requires no auth for read; UA must be set.
135- // const res = await fetch(
136- // `https://www.reddit.com/r/${subreddit}/search.json?q=${encodeURIComponent('"proactive agents"')}&sort=new&t=week&restrict_sr=1`,
137- // { headers: { "user-agent": "proactive-agents-digest/0.1 (by /u/khaliqgant)" }, signal: _ctx.signal },
138- // );
139- // const data = (await res.json()) as { data: { children: { data: { url: string; title: string; selftext: string; created_utc: number } }[] } };
140- // return data.data.children.map((c) => ({
141- // source: `reddit:${subreddit}` as Mention["source"],
142- // url: c.data.url,
143- // title: c.data.title,
144- // excerpt: c.data.selftext.slice(0, 280),
145- // publishedAt: new Date(c.data.created_utc * 1000).toISOString(),
146- // }));
147- return [ ] ;
170+ // ─────────────────────────────────────────────────────────────────────────────
171+ // Brave Search
172+
173+ type BraveResult = {
174+ url : string ;
175+ title : string ;
176+ description : string ;
177+ age ?: string ; // ISO 8601 when present
178+ } ;
179+
180+ /**
181+ * Brave Search Web API. Free tier: 2,000 queries/month, 1 q/sec. We use
182+ * one query per source per week — well under the limit.
183+ *
184+ * Auth: read `BRAVE_API_KEY` from env. When the proactive runtime supports
185+ * provider connections, swap to `relay providers connect brave` and the SDK
186+ * will inject the key at boot.
187+ */
188+ async function braveSearch (
189+ ctx : Context ,
190+ query : string ,
191+ opts : { freshness ?: "pd" | "pw" | "pm" | "py" ; count ?: number } = { } ,
192+ ) : Promise < BraveResult [ ] > {
193+ const apiKey = process . env . BRAVE_API_KEY ;
194+ if ( ! apiKey ) {
195+ ctx . logger . warn ( "BRAVE_API_KEY missing — skipping web search" ) ;
196+ return [ ] ;
197+ }
198+ const params = new URLSearchParams ( {
199+ q : query ,
200+ count : String ( opts . count ?? 20 ) ,
201+ text_decorations : "false" ,
202+ } ) ;
203+ if ( opts . freshness ) params . set ( "freshness" , opts . freshness ) ;
204+
205+ const res = await fetch ( `https://api.search.brave.com/res/v1/web/search?${ params } ` , {
206+ headers : {
207+ "X-Subscription-Token" : apiKey ,
208+ Accept : "application/json" ,
209+ "Accept-Encoding" : "gzip" ,
210+ } ,
211+ signal : ctx . signal ,
212+ } ) ;
213+ if ( ! res . ok ) {
214+ ctx . logger . error ( "brave search failed" , { status : res . status , query } ) ;
215+ return [ ] ;
216+ }
217+ const data = ( await res . json ( ) ) as { web ?: { results ?: BraveResult [ ] } } ;
218+ return data . web ?. results ?? [ ] ;
148219}
149220
150221// ─────────────────────────────────────────────────────────────────────────────
0 commit comments