2727 * - Career/intelligence updates since morning
2828 */
2929
30+ import fs from 'node:fs/promises' ;
31+ import path from 'node:path' ;
3032import { NotificationManager } from './notification-manager.js' ;
3133import { NotionInbox } from '../integrations/notion/inbox.js' ;
3234import { dailyAudit , type DailyAudit } from './daily-audit.js' ;
@@ -38,6 +40,7 @@ import type { NotionConfig } from './types.js';
3840import type { DailyDigest } from './daily-digest.js' ;
3941import type { LifeMonitorReport } from './life-monitor.js' ;
4042import type { GovernanceSnapshot } from './governance-reporter.js' ;
43+ import type { IntelligenceItem } from './intelligence-scanner.js' ;
4144
4245// ─── Interfaces ─────────────────────────────────────────────────────────────
4346
@@ -496,6 +499,57 @@ export class BriefingGenerator {
496499 } ;
497500 }
498501
502+ /**
503+ * Generate and send X Likes Curated Digest (~8 PM)
504+ *
505+ * Reads today's intelligence scan results, filters for items sourced
506+ * from X likes, groups by domain, and surfaces the most relevant ones
507+ * as a scannable evening reading list.
508+ */
509+ async xLikesDigest ( ) : Promise < BriefingResult > {
510+ const INTEL_DIR = path . join ( process . env . HOME ?? '~' , '.ari' , 'knowledge' , 'intelligence' ) ;
511+ const SCAN_LOG = path . join ( INTEL_DIR , 'scan-log.json' ) ;
512+
513+ let socialItems : IntelligenceItem [ ] = [ ] ;
514+
515+ try {
516+ const raw = await fs . readFile ( SCAN_LOG , 'utf-8' ) ;
517+ const scanResult = JSON . parse ( raw ) as { topItems : IntelligenceItem [ ] ; startedAt : string } ;
518+ // Only use items from today's scan (within last 18 hours)
519+ const cutoffMs = Date . now ( ) - 18 * 60 * 60 * 1000 ;
520+ const scanAge = new Date ( scanResult . startedAt ) . getTime ( ) ;
521+ if ( scanAge > cutoffMs ) {
522+ socialItems = scanResult . topItems
523+ . filter ( item => item . sourceCategory === 'SOCIAL' )
524+ . sort ( ( a , b ) => b . score - a . score )
525+ . slice ( 0 , 12 ) ;
526+ }
527+ } catch {
528+ // No scan available yet — send empty digest
529+ }
530+
531+ const telegramHtml = splitTelegramMessage ( this . formatXLikesHtml ( socialItems ) ) ;
532+
533+ const notifyResult = await this . notificationManager . notify ( {
534+ category : 'daily' ,
535+ title : 'Your Reading List' ,
536+ body : socialItems . length > 0
537+ ? `${ socialItems . length } posts from today's likes curated for you.`
538+ : 'Nothing from your X likes today — clean slate.' ,
539+ priority : 'low' ,
540+ telegramHtml,
541+ } ) ;
542+
543+ await dailyAudit . logActivity (
544+ 'system_event' ,
545+ 'X Likes Digest' ,
546+ `Curated ${ socialItems . length } social items` ,
547+ { outcome : 'success' , details : { type : 'x_likes_digest' , count : socialItems . length } }
548+ ) ;
549+
550+ return { success : true , smsSent : notifyResult . sent } ;
551+ }
552+
499553 // ─── Telegram HTML Formatters ─────────────────────────────────────────────
500554
501555 /**
@@ -1092,6 +1146,74 @@ export class BriefingGenerator {
10921146 return lines ;
10931147 }
10941148
1149+ private formatXLikesHtml ( items : IntelligenceItem [ ] ) : string {
1150+ const now = new Date ( ) ;
1151+ const dateStr = now . toLocaleDateString ( 'en-US' , {
1152+ weekday : 'long' , month : 'short' , day : 'numeric' , timeZone : this . timezone ,
1153+ } ) ;
1154+
1155+ const lines : string [ ] = [ ] ;
1156+ lines . push ( `<b>📚 Your Reading List — ${ dateStr } </b>` ) ;
1157+ lines . push ( '' ) ;
1158+
1159+ if ( items . length === 0 ) {
1160+ lines . push ( '<i>Nothing from your X likes today.</i>' ) ;
1161+ return lines . join ( '\n' ) ;
1162+ }
1163+
1164+ // Group by primary domain
1165+ const grouped = new Map < string , IntelligenceItem [ ] > ( ) ;
1166+ for ( const item of items ) {
1167+ const domain = item . domains [ 0 ] ?? 'general' ;
1168+ const group = grouped . get ( domain ) ?? [ ] ;
1169+ group . push ( item ) ;
1170+ grouped . set ( domain , group ) ;
1171+ }
1172+
1173+ const domainEmoji : Record < string , string > = {
1174+ ai : '🤖' , programming : '💻' , investment : '📈' ,
1175+ career : '🎯' , business : '💡' , security : '🛡' ,
1176+ tools : '🔧' , general : '📌' ,
1177+ } ;
1178+
1179+ for ( const [ domain , domainItems ] of grouped ) {
1180+ const emoji = domainEmoji [ domain ] ?? '📌' ;
1181+ lines . push ( `<b>${ emoji } ${ domain . charAt ( 0 ) . toUpperCase ( ) + domain . slice ( 1 ) } </b>` ) ;
1182+
1183+ for ( const item of domainItems . slice ( 0 , 3 ) ) {
1184+ const meta = item . metadata ;
1185+ const author = meta ?. authorName as string | undefined ?? meta ?. authorUsername as string | undefined ?? '' ;
1186+ const authorStr = author ? `<i>${ this . esc ( author ) } </i> ` : '' ;
1187+
1188+ // Trim tweet text to 120 chars
1189+ const text = item . summary . length > 120
1190+ ? item . summary . slice ( 0 , 117 ) + '...'
1191+ : item . summary ;
1192+
1193+ const engagementNote = typeof meta ?. likes === 'number' && meta . likes > 100
1194+ ? ` · ❤️ ${ meta . likes } `
1195+ : '' ;
1196+
1197+ if ( item . url && ! item . url . includes ( 'x.com/i/status' ) ) {
1198+ lines . push ( `▸ ${ authorStr } <a href="${ item . url } ">${ this . esc ( text ) } </a>${ engagementNote } ` ) ;
1199+ } else {
1200+ lines . push ( `▸ ${ authorStr } ${ this . esc ( text ) } ${ engagementNote } ` ) ;
1201+ }
1202+ }
1203+
1204+ lines . push ( '' ) ;
1205+ }
1206+
1207+ const totalLikes = items . reduce ( ( sum , item ) => {
1208+ const meta = item . metadata ;
1209+ return sum + ( typeof meta ?. likes === 'number' ? meta . likes : 0 ) ;
1210+ } , 0 ) ;
1211+
1212+ lines . push ( `<i>${ items . length } posts from your X likes · ${ totalLikes . toLocaleString ( ) } total likes on sourced content</i>` ) ;
1213+
1214+ return lines . join ( '\n' ) ;
1215+ }
1216+
10951217 private getContextualGreeting ( dayName : string ) : string {
10961218 const greetings : Record < string , string > = {
10971219 Monday : 'Good morning, Pryce — new week, clean slate' ,
0 commit comments