Skip to content
This repository was archived by the owner on Feb 26, 2026. It is now read-only.

Commit bd31902

Browse files
committed
feat(integrations): x likes digest, perplexity model routing, elevenlabs voice quality
- add xLikesDigest() — 8 PM reading list curated from daily X likes scan-log - add x-likes-digest scheduler task (daily 8 PM) - add x_likes_digest handler in agent.ts - upgrade perplexity to sonar/sonar-pro/sonar-reasoning model routing - add ElevenLabs voice_settings (stability, similarity_boost, style, use_speaker_boost) - upgrade ElevenLabs default model to eleven_turbo_v2_5 - gitignore .mcp.json (local config with API keys — untracked going forward) - add .mcp.json to PII scanner exclusions - add 4 xLikesDigest tests + fix perplexity model name in test
1 parent 98ef616 commit bd31902

11 files changed

Lines changed: 234 additions & 49 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Dependencies
22
node_modules/
33

4+
# Local MCP server config (contains API keys — configure locally)
5+
.mcp.json
6+
47
# Build outputs
58
dist/
69
dashboard/dist/

.mcp.json

Lines changed: 0 additions & 42 deletions
This file was deleted.

scripts/scan-pii.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ EXCLUDE_PATTERNS=(
5050
"CLAUDE.md" # Creator credit belongs here
5151
"CONTRIBUTING.md" # May reference creator
5252
".claude" # Claude Code local config (not committed)
53+
".mcp.json" # MCP server config (local secrets — not committed)
5354
)
5455

5556
# Build exclude arguments for grep

src/autonomous/agent.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,6 +2161,18 @@ export class AutonomousAgent {
21612161
}
21622162
});
21632163

2164+
// 8 PM daily X likes curated digest — reading list from today's X likes
2165+
this.scheduler.registerHandler('x_likes_digest', async () => {
2166+
try {
2167+
if (this.briefingGenerator) {
2168+
await this.briefingGenerator.xLikesDigest();
2169+
}
2170+
log.info('X likes digest delivered');
2171+
} catch (error: unknown) {
2172+
log.error({ error }, 'X likes digest failed');
2173+
}
2174+
});
2175+
21642176
// ==========================================================================
21652177
// CONTENT ENGINE: Draft generation + delivery to Telegram
21662178
// ==========================================================================

src/autonomous/briefings.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
* - Career/intelligence updates since morning
2828
*/
2929

30+
import fs from 'node:fs/promises';
31+
import path from 'node:path';
3032
import { NotificationManager } from './notification-manager.js';
3133
import { NotionInbox } from '../integrations/notion/inbox.js';
3234
import { dailyAudit, type DailyAudit } from './daily-audit.js';
@@ -38,6 +40,7 @@ import type { NotionConfig } from './types.js';
3840
import type { DailyDigest } from './daily-digest.js';
3941
import type { LifeMonitorReport } from './life-monitor.js';
4042
import 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',

src/autonomous/scheduler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ const DEFAULT_TASKS: Omit<ScheduledTask, 'lastRun' | 'nextRun'>[] = [
173173
enabled: true,
174174
essential: true, // User-facing deliverable
175175
},
176+
{
177+
id: 'x-likes-digest',
178+
name: 'X Likes Curated Digest',
179+
cron: '0 20 * * *', // 8:00 PM daily — reading list from today's X likes
180+
handler: 'x_likes_digest',
181+
enabled: true,
182+
essential: false,
183+
},
176184

177185
// ============================================================================
178186
// NON-ESSENTIAL TASKS - Skipped when budget is constrained

src/integrations/perplexity/client.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ type FocusType = 'web' | 'academic' | 'news';
6161
export class PerplexityClient {
6262
private apiKey: string;
6363
private baseUrl = 'https://api.perplexity.ai/chat/completions';
64-
private model = 'llama-3.1-sonar-small-128k-online';
64+
private model = 'sonar'; // General search
65+
private deepModel = 'sonar-pro'; // Deep research reports
66+
private reasoningModel = 'sonar-reasoning'; // Financial/market analysis
6567
private cacheTtlMs = 10 * 60 * 1000; // 10 minutes
6668
private searchCache: Map<string, CacheEntry<PerplexityResult>> = new Map();
6769
private lastRequestTime = 0;
@@ -121,7 +123,7 @@ Always cite sources and provide factual information.`;
121123
? `Research topic: ${topic}\n\nAdditional context: ${context}`
122124
: `Research topic: ${topic}`;
123125

124-
const result = await this.makeRequest(userQuery, systemPrompt);
126+
const result = await this.makeRequest(userQuery, systemPrompt, this.deepModel);
125127

126128
const report = this.parseResearchReport(topic, result);
127129
log.info(`Completed deep research: "${topic}"`);
@@ -138,7 +140,7 @@ Always cite sources and provide factual information.`;
138140
Provide clear, factual explanations with relevant context and citations.
139141
Focus on: what happened, why it matters, and potential implications.`;
140142

141-
const result = await this.makeRequest(`${event} market analysis`, systemPrompt);
143+
const result = await this.makeRequest(`${event} market analysis today`, systemPrompt, this.reasoningModel);
142144
log.info(`Explained market event: "${event}"`);
143145
return result;
144146
}
@@ -174,7 +176,7 @@ Focus on: what happened, why it matters, and potential implications.`;
174176
return prompts[focus];
175177
}
176178

177-
private async makeRequest(query: string, systemPrompt: string): Promise<PerplexityResult> {
179+
private async makeRequest(query: string, systemPrompt: string, model?: string): Promise<PerplexityResult> {
178180
let lastError: Error | null = null;
179181

180182
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
@@ -186,11 +188,13 @@ Focus on: what happened, why it matters, and potential implications.`;
186188
'Content-Type': 'application/json',
187189
},
188190
body: JSON.stringify({
189-
model: this.model,
191+
model: model ?? this.model,
190192
messages: [
191193
{ role: 'system', content: systemPrompt },
192194
{ role: 'user', content: query },
193195
],
196+
return_citations: true,
197+
return_images: false,
194198
}),
195199
});
196200

src/plugins/tts/speech-generator.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ export class SpeechGenerator {
102102
body: JSON.stringify({
103103
text: request.text,
104104
model_id: model,
105+
voice_settings: {
106+
stability: 0.50, // Natural variation (not robotic)
107+
similarity_boost: 0.75, // Close to original voice clone
108+
style: 0.40, // Moderate expressiveness
109+
use_speaker_boost: true, // Clarity enhancement
110+
},
105111
}),
106112
signal: AbortSignal.timeout(30_000),
107113
},

src/plugins/tts/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { z } from 'zod';
77
export const TtsConfigSchema = z.object({
88
apiKey: z.string().optional(),
99
defaultVoice: z.string().default('Xb7hH8MSUJpSbSDYk0k2'), // Alice
10-
defaultModel: z.string().default('eleven_multilingual_v2'),
10+
defaultModel: z.string().default('eleven_turbo_v2_5'),
1111
dailyCap: z.number().default(2.00), // $2/day
1212
costPer1000Chars: z.number().default(0.30),
1313
});

0 commit comments

Comments
 (0)