titan-agent 5.3.0 → 5.3.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 +5 -5
- package/dist/agent/agent.js +33 -4
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/subAgent.js +16 -1
- package/dist/agent/subAgent.js.map +1 -1
- package/dist/agent/toolRunner.js +17 -0
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/config/schema.js +10 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/eval/record.js +21 -2
- package/dist/eval/record.js.map +1 -1
- package/dist/gateway/metrics.js +26 -3
- package/dist/gateway/metrics.js.map +1 -1
- package/dist/gateway/server.js +58 -6
- package/dist/gateway/server.js.map +1 -1
- package/dist/organism/drives.js +47 -11
- package/dist/organism/drives.js.map +1 -1
- package/dist/organism/pressure.js +16 -0
- package/dist/organism/pressure.js.map +1 -1
- package/dist/skills/builtin/fb_autopilot.js +16 -1
- package/dist/skills/builtin/fb_autopilot.js.map +1 -1
- package/dist/telemetry/activityLog.js +158 -0
- package/dist/telemetry/activityLog.js.map +1 -0
- package/dist/utils/constants.js +3 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/assets/{AuditPanel-C31LRHZX.js → AuditPanel-CM6Wg9hO.js} +1 -1
- package/ui/dist/assets/{AutonomyPanel-CxQU72ZY.js → AutonomyPanel-CESx3ANg.js} +1 -1
- package/ui/dist/assets/{AutopilotPanel-D4FnBwJm.js → AutopilotPanel-DtEet1hJ.js} +1 -1
- package/ui/dist/assets/{AutoresearchPanel-BYHXZ9AO.js → AutoresearchPanel-DR47NqT5.js} +1 -1
- package/ui/dist/assets/{BackupPanel-C4CQKf2P.js → BackupPanel-BGP8p3l3.js} +1 -1
- package/ui/dist/assets/{BrowserPanel-C-OFYyLm.js → BrowserPanel-C15x9bLn.js} +1 -1
- package/ui/dist/assets/{CPAgents-CvkZDm_3.js → CPAgents-DYUtPzSq.js} +1 -1
- package/ui/dist/assets/{CPDashboard-JmBLBbj7.js → CPDashboard-Bf0-SyCh.js} +1 -1
- package/ui/dist/assets/{CPFiles-BDToRw0a.js → CPFiles-CxgxjQcO.js} +1 -1
- package/ui/dist/assets/{CPGoals-Dh9qJNWa.js → CPGoals-BsmCMTvT.js} +1 -1
- package/ui/dist/assets/{CPInbox-B6iaIbNG.js → CPInbox-tMSbmQ9H.js} +1 -1
- package/ui/dist/assets/{CPSocial-CsFrwZRC.js → CPSocial-nb-j7sOE.js} +1 -1
- package/ui/dist/assets/{ChannelsPanel-D-S4ktFn.js → ChannelsPanel-DP5C2OKd.js} +1 -1
- package/ui/dist/assets/{CheckpointsPanel-D-sP9ZuS.js → CheckpointsPanel-DlranVLZ.js} +1 -1
- package/ui/dist/assets/{CommandPostHub-BhlNyeDH.js → CommandPostHub-BgxIa4Ev.js} +3 -3
- package/ui/dist/assets/{CronPanel-Bf3rV7N2.js → CronPanel-LoT5yKwJ.js} +1 -1
- package/ui/dist/assets/{DaemonPanel-GGBWjTG2.js → DaemonPanel-DBGMqaE_.js} +1 -1
- package/ui/dist/assets/{DataTable-D2Px4o6f.js → DataTable-B2Ma8hfi.js} +1 -1
- package/ui/dist/assets/{EmptyState-DH6-Jy6A.js → EmptyState-CcKyk5Yn.js} +1 -1
- package/ui/dist/assets/EvalHarnessPanel-BqtMc1ZM.js +2 -0
- package/ui/dist/assets/{EvalPanel-CdjxzHlJ.js → EvalPanel-Bc33j0pN.js} +1 -1
- package/ui/dist/assets/{FilesPanel-Dz8TFydL.js → FilesPanel-3QKvrWPo.js} +1 -1
- package/ui/dist/assets/{FleetPanel-CWwWWTD4.js → FleetPanel-CSsXuQYj.js} +1 -1
- package/ui/dist/assets/{HomelabPanel-C7VBV7AC.js → HomelabPanel-DhrjTX9m.js} +1 -1
- package/ui/dist/assets/{InfraView-B1TgXARJ.js → InfraView-CR6HyrL6.js} +2 -2
- package/ui/dist/assets/{InlineEditableField-DOJNOL8m.js → InlineEditableField-CnvF-yFR.js} +1 -1
- package/ui/dist/assets/{Input-BgyHgQ3D.js → Input-GTHp2Rkr.js} +1 -1
- package/ui/dist/assets/{IntegrationsPanel-O26b4nhv.js → IntegrationsPanel-CymCRE3T.js} +1 -1
- package/ui/dist/assets/{IntelligenceView-DUhTQ8f_.js → IntelligenceView-C1IHxJRC.js} +2 -2
- package/ui/dist/assets/{LearningPanel-DX5S9ovg.js → LearningPanel-DOCES3lH.js} +1 -1
- package/ui/dist/assets/{LogsPanel-DdeTnATQ.js → LogsPanel-BLnAqEaZ.js} +1 -1
- package/ui/dist/assets/{McpPanel-BpXWrP1a.js → McpPanel-ChUzmr3z.js} +1 -1
- package/ui/dist/assets/{MemoryGraphPanel-CNkZmTUy.js → MemoryGraphPanel-Bzvjmzvk.js} +1 -1
- package/ui/dist/assets/{MemoryWikiPanel-o4L8Df-n.js → MemoryWikiPanel-Dwk3Aqwd.js} +1 -1
- package/ui/dist/assets/{MeshPanel-DMBQJFCC.js → MeshPanel-C3LJSlht.js} +1 -1
- package/ui/dist/assets/{NvidiaPanel-C8P-tJFG.js → NvidiaPanel-CeZK_-CV.js} +1 -1
- package/ui/dist/assets/{OrganismPanel-CcfHDWDk.js → OrganismPanel-BB6YOiQV.js} +1 -1
- package/ui/dist/assets/{OverviewPanel-BSotI1Zv.js → OverviewPanel-BmtBhQnv.js} +1 -1
- package/ui/dist/assets/{PageHeader-DPJuAgJk.js → PageHeader-BimceqQo.js} +1 -1
- package/ui/dist/assets/{PaperclipPanel-aXoXUjo6.js → PaperclipPanel-C-brgwA3.js} +1 -1
- package/ui/dist/assets/{PersonasPanel-DdPZxz2C.js → PersonasPanel-L1j78p6H.js} +1 -1
- package/ui/dist/assets/{RecipesPanel-D7qffXQN.js → RecipesPanel-34lCfynJ.js} +1 -1
- package/ui/dist/assets/{SecurityPanel-BDRK5el7.js → SecurityPanel-CBTPWLj6.js} +1 -1
- package/ui/dist/assets/{SelfImprovePanel-oYiMwFnA.js → SelfImprovePanel-BrPbFHhG.js} +1 -1
- package/ui/dist/assets/{SelfProposalsPanel-DOpNU_rr.js → SelfProposalsPanel-lNmiDThB.js} +1 -1
- package/ui/dist/assets/{SessionsPanel-eRbM3D9P.js → SessionsPanel-DAEYIn83.js} +1 -1
- package/ui/dist/assets/{SessionsTab-Jq3UKQCI.js → SessionsTab-JQbltWww.js} +1 -1
- package/ui/dist/assets/{SettingsPanel-DBIvKUYY.js → SettingsPanel-CzRROAYQ.js} +1 -1
- package/ui/dist/assets/{SettingsView-yfSY4OLt.js → SettingsView-CN7ii2uw.js} +2 -2
- package/ui/dist/assets/{SkeletonLoader-D1d-Gyyg.js → SkeletonLoader-atQtpcF5.js} +1 -1
- package/ui/dist/assets/{SkillsPanel-bubl9nag.js → SkillsPanel-DlFs2ih7.js} +1 -1
- package/ui/dist/assets/{SomaView-D3aFL8Tw.js → SomaView-Ba642Oqb.js} +1 -1
- package/ui/dist/assets/{StatCard-CEVFsz7t.js → StatCard-DciE_Iqc.js} +1 -1
- package/ui/dist/assets/{StatusBadge-DxeA9LNd.js → StatusBadge-BtfSPoW2.js} +1 -1
- package/ui/dist/assets/{TeamsPanel-D6IJJIR_.js → TeamsPanel-DKQ5z2Qe.js} +1 -1
- package/ui/dist/assets/{TelemetryPanel-SMPebdjQ.js → TelemetryPanel-B6KAc55Q.js} +1 -1
- package/ui/dist/assets/{TitanCanvas-BQU1yxqf.js → TitanCanvas-C-s0A-lv.js} +3 -3
- package/ui/dist/assets/{ToolsView-DgP4uRPr.js → ToolsView-Dei0KMP0.js} +2 -2
- package/ui/dist/assets/{Tooltip-CNPQr7IO.js → Tooltip-70UK0E2I.js} +1 -1
- package/ui/dist/assets/{TraceViewer-BbISy_ET.js → TraceViewer-BniolyBx.js} +1 -1
- package/ui/dist/assets/{TrainingPanel-BdCHcv6t.js → TrainingPanel-Bz4CTPGW.js} +1 -1
- package/ui/dist/assets/{VoiceOverlay-l6yoasVz.js → VoiceOverlay-CmNCrLcd.js} +1 -1
- package/ui/dist/assets/{VramPanel-XLhmen92.js → VramPanel-Xh_OtRDR.js} +1 -1
- package/ui/dist/assets/{WatchView-Bt-lNNWJ.js → WatchView-C-sGFpVy.js} +1 -1
- package/ui/dist/assets/{WorkTab-IG-F6Qll.js → WorkTab-BjLNmgIK.js} +1 -1
- package/ui/dist/assets/{WorkflowsPanel-DsMpnwLK.js → WorkflowsPanel-CvgQU1xI.js} +1 -1
- package/ui/dist/assets/{arrow-left-C_H9Z2Tm.js → arrow-left-DwqHtJiU.js} +1 -1
- package/ui/dist/assets/{chart-column-rR6tb72l.js → chart-column-BtNO6sRy.js} +1 -1
- package/ui/dist/assets/{circle-check-big-B1hMwau0.js → circle-check-big-DZRE_MbN.js} +1 -1
- package/ui/dist/assets/{dollar-sign-DhYwsTnR.js → dollar-sign-aVG3a5eL.js} +1 -1
- package/ui/dist/assets/{download-UDDcAlZC.js → download-BxiWJU4G.js} +1 -1
- package/ui/dist/assets/{eye-off-Cx0M_VQb.js → eye-off-CkgfFYhm.js} +1 -1
- package/ui/dist/assets/{funnel-B7YvM1ei.js → funnel-PkLdxKyC.js} +1 -1
- package/ui/dist/assets/{git-branch-BhTBN3J6.js → git-branch-BM-Gw95X.js} +1 -1
- package/ui/dist/assets/{index-D7Clon2u.js → index-CahJbWSR.js} +2 -2
- package/ui/dist/assets/{layers-B6jDzitD.js → layers-BuGf4FIJ.js} +1 -1
- package/ui/dist/assets/{legacy-av079XKu.js → legacy-CR6o4t-y.js} +1 -1
- package/ui/dist/assets/{lightbulb-DRuQ3Chf.js → lightbulb-n8gc_XAL.js} +1 -1
- package/ui/dist/assets/{pause-DqkRWPB_.js → pause-DCV52koX.js} +1 -1
- package/ui/dist/assets/{play-hUyR3CVS.js → play-CcJ9BnCh.js} +1 -1
- package/ui/dist/assets/{plug-CvpyjJt_.js → plug-CfWBXfCl.js} +1 -1
- package/ui/dist/assets/{proxy-Cc5bR828.js → proxy-CzZDfLmm.js} +1 -1
- package/ui/dist/assets/{square-CdiC0J8Z.js → square-DJpUhlxi.js} +1 -1
- package/ui/dist/assets/{target-DemL8_0v.js → target-DWcmM_9m.js} +1 -1
- package/ui/dist/assets/{toggle-right-Dsk892k5.js → toggle-right-YusFQ69L.js} +1 -1
- package/ui/dist/assets/{trash-2-Byj4OvKB.js → trash-2-CK7yQ55V.js} +1 -1
- package/ui/dist/assets/{trending-up-Dh_CffGX.js → trending-up-DGjFyubC.js} +1 -1
- package/ui/dist/assets/{trophy-DDr2AePx.js → trophy-uQv_NgDB.js} +1 -1
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/EvalHarnessPanel-Cz9dRg61.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/fb_autopilot.ts"],"sourcesContent":["/**\n * TITAN — Facebook Autopilot\n *\n * Autonomous Facebook page management. Runs as a daemon watcher on schedule.\n * Generates content from TITAN's real activity, stats, and knowledge.\n * Posts up to 6 times per day (every ~2h) following the 80/20 value/promo rule.\n *\n * Content types (weighted rotation):\n * 40% — Activity log: what TITAN did today (tools, agents, tasks)\n * 20% — Feature spotlight: pick a random tool/skill and explain it\n * 15% — Stats & milestones: npm downloads, test count, agent runs\n * 15% — Tips & tutorials: how to use TITAN for specific tasks\n * 10% — Promotional: install command, GitHub link, call to action\n *\n * Safety:\n * - PII filter (from facebook.ts)\n * - Dedup guard (from facebook.ts)\n * - Posts go through review queue (configurable: auto-approve after testing)\n * - Daily post cap: 6 max\n * - Comment replies capped at 10/day\n */\nimport { registerSkill } from '../registry.js';\nimport { registerWatcher } from '../../agent/daemon.js';\nimport { loadConfig } from '../../config/config.js';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { TITAN_HOME, TITAN_VERSION } from '../../utils/constants.js';\nimport { chat } from '../../providers/router.js';\nimport { postToPage } from './facebook.js';\nimport logger from '../../utils/logger.js';\nimport { getSkills } from '../registry.js';\nimport { getRegisteredTools } from '../../agent/toolRunner.js';\n\nconst COMPONENT = 'FBAutopilot';\nconst STATE_PATH = join(TITAN_HOME, 'fb-autopilot-state.json');\n\n// ─── Types ──────────────────────────────────────────────────────\n\ninterface AutopilotState {\n lastPostAt: string | null;\n postsToday: number;\n repliesToday: number;\n lastResetDate: string;\n postHistory: Array<{ date: string; type: string; postId?: string; content?: string }>;\n contentIndex: number; // Rotates through content types\n}\n\ntype ContentType = 'activity' | 'spotlight' | 'stats' | 'tips' | 'promo' | 'usecase' | 'eli5';\n\n// Weighted content rotation — balanced for technical AND non-technical audiences\nexport const CONTENT_ROTATION: ContentType[] = [\n 'activity', 'activity', // 15% — what TITAN's been doing\n 'usecase', 'usecase', 'usecase', // 25% — business/daily life ideas\n 'eli5', 'eli5', // 15% — simple explanations for non-tech people\n 'spotlight', 'spotlight', // 15% — feature highlights\n 'tips', // 10% — how-to content\n 'stats', // 10% — milestones\n 'promo', // 10% — install/GitHub CTA\n];\n\n// ─── State Management ───────────────────────────────────────────\n\nexport function loadState(): AutopilotState {\n if (!existsSync(STATE_PATH)) return defaultState();\n try {\n return JSON.parse(readFileSync(STATE_PATH, 'utf-8')) as AutopilotState;\n } catch { return defaultState(); }\n}\n\nexport function defaultState(): AutopilotState {\n return {\n lastPostAt: null,\n postsToday: 0,\n repliesToday: 0,\n lastResetDate: new Date().toISOString().slice(0, 10),\n postHistory: [],\n contentIndex: 0,\n };\n}\n\nexport function saveState(state: AutopilotState): void {\n try {\n mkdirSync(dirname(STATE_PATH), { recursive: true });\n writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf-8');\n } catch (e) {\n logger.error(COMPONENT, `Failed to save state: ${(e as Error).message}`);\n }\n}\n\nexport function resetDailyCounters(state: AutopilotState): void {\n const today = new Date().toISOString().slice(0, 10);\n if (state.lastResetDate !== today) {\n state.postsToday = 0;\n state.repliesToday = 0;\n state.lastResetDate = today;\n }\n}\n\n// ─── Content Generators ─────────────────────────────────────────\n\nconst FEATURE_SPOTLIGHTS = [\n { tool: 'spawn_agent', desc: 'Spawn specialized sub-agents (coder, researcher, analyst, browser) that work independently and report back. Each gets its own persona and tool set.' },\n { tool: 'web_search + web_fetch', desc: 'Research any topic by searching the web, fetching full page content, and cross-referencing multiple sources.' },\n { tool: 'write_file + shell', desc: 'Write code to disk and immediately run it. Build scripts, apps, and tools — then verify they work.' },\n { tool: 'memory', desc: 'Persistent memory across conversations. TITAN remembers your preferences, project details, and past decisions.' },\n { tool: 'self_improve', desc: 'TITAN evaluates its own performance and evolves its prompts and tool selection through genetic optimization (GEPA).' },\n { tool: 'fb_post', desc: 'Yes, TITAN manages this very Facebook page! Posts, reads comments, tracks engagement — all autonomously.' },\n { tool: 'goals + planner', desc: 'Break complex projects into goals with subtasks, dependencies, and automatic delegation to sub-agents.' },\n { tool: 'mixture_of_agents', desc: 'Fan out a question to multiple AI models simultaneously, then synthesize the best answer from all responses.' },\n { tool: 'cron + autopilot', desc: 'Schedule recurring tasks, automated workflows, and background monitoring. TITAN works while you sleep.' },\n { tool: 'knowledge_base (RAG)', desc: 'Ingest documents, URLs, and notes into a searchable knowledge base. TITAN retrieves relevant context automatically.' },\n { tool: 'browser automation', desc: 'Navigate websites, fill forms, take screenshots, and interact with web apps — all through AI-controlled browser.' },\n { tool: 'mesh networking', desc: 'Connect multiple TITAN instances across machines. Distribute work across your homelab cluster via P2P mesh.' },\n { tool: '40 personas', desc: 'Switch between 40 specialized personas: debugger, architect, code-reviewer, TDD engineer, security engineer, and more.' },\n { tool: 'Command Post', desc: 'Paperclip-inspired governance system. Track agent tasks, enforce budgets, manage org hierarchy, and monitor activity.' },\n { tool: 'voice (LiveKit)', desc: 'Talk to TITAN with your voice via WebRTC. Real-time conversation with F5-TTS voice cloning for natural responses.' },\n];\n\n/** Fetch live npm download stats for accurate social posts */\nasync function getLiveStats(): Promise<{ downloads: number; tools: number; skills: number; version: string }> {\n let downloads = 25000;\n try {\n const res = await fetch('https://api.npmjs.org/downloads/point/last-month/titan-agent');\n if (res.ok) {\n const data = await res.json() as { downloads?: number };\n if (data.downloads) downloads = data.downloads;\n }\n } catch {\n /* non-critical — use fallback */\n }\n return {\n downloads,\n tools: getRegisteredTools().length,\n skills: getSkills().length,\n version: TITAN_VERSION,\n };\n}\n\n/** Detect when the model has echoed the system prompt or instructions instead of writing a post */\nfunction looksLikeLeakedPrompt(text: string): boolean {\n const leakSignals = [\n /The user wants me to write/i,\n /Be in first person/i,\n /Include 2-3 relevant hashtags/i,\n /No personal info,? IPs/i,\n /No markdown formatting/i,\n /Under 280 characters/i,\n /Mention things like:/i,\n /Conversational and playful/i,\n /Let me craft something/i,\n /Here is an example post/i,\n /Match the playful first-person style/i,\n /Reply with ONLY the post text/i,\n /no explanations,? no labels/i,\n /Write one Facebook post for TITAN/i,\n /short Facebook post/i,\n /as if I am TITAN/i,\n /as if the AI is speaking/i,\n /1\\. Be in first person/i,\n /2\\. Mention things/i,\n /3\\. Be conversational/i,\n /4\\. Include 2-3/i,\n /5\\. No personal info/i,\n /6\\. Under 280/i,\n /7\\. No markdown/i,\n ];\n const score = leakSignals.filter(p => p.test(text)).length;\n return score >= 2 || /\\b(?:The user wants me|Let me craft|Write one Facebook post)\\b/i.test(text);\n}\n\nconst TIPS = [\n 'You can switch TITAN\\'s persona mid-conversation. Try: \"Switch to the security-engineer persona\" before a code review.',\n 'TITAN\\'s autonomous mode lets it work independently. Set a goal and let TITAN break it into tasks, delegate to sub-agents, and execute.',\n 'Use \"npm install -g titan-agent\" to get TITAN globally, then run \"titan gateway\" to start the Mission Control dashboard.',\n 'TITAN supports 35 LLM providers. Run local models with Ollama, or connect to Claude, GPT-4, Gemini, and more.',\n 'TITAN\\'s Command Post tracks every agent action. Enable it in settings to get full visibility into what your agents are doing.',\n 'You can create custom skills as markdown files in ~/.titan/workspace/skills/. TITAN loads them automatically on startup.',\n 'TITAN\\'s cron system lets you schedule any task. Example: \"Run security scans every Monday at 9am\" — TITAN handles the rest.',\n 'Multi-agent orchestration: TITAN analyzes your request and automatically delegates parts to specialized sub-agents in parallel.',\n];\n\nconst USE_CASES = [\n // Small business\n 'A small bakery owner could use TITAN to automatically post daily specials to social media, track inventory, and draft emails to suppliers — all without touching a computer.',\n 'Real estate agents: TITAN can research property listings, draft listing descriptions, generate market comparisons, and schedule social media posts for open houses.',\n 'Freelancers can set TITAN to automatically send follow-up emails, track invoices, research leads, and generate proposals while they focus on actual work.',\n 'Restaurant owners: TITAN can monitor online reviews, draft responses, update your menu on the website, and generate weekly social posts — all on autopilot.',\n // Daily life\n 'Planning a vacation? Tell TITAN where you want to go and your budget. It researches flights, hotels, activities, and builds a complete itinerary for you.',\n 'TITAN can be your personal research assistant. Ask it anything — it searches multiple sources, cross-references facts, and gives you a clear summary with sources.',\n 'Students: TITAN can help organize study notes, research topics in depth, create flashcard-style summaries, and even quiz you on the material.',\n 'Job seekers: TITAN can research companies, tailor your resume for specific roles, draft cover letters, and track your applications — all automatically.',\n // Business operations\n 'Marketing teams: TITAN runs as your content engine. It researches trending topics, drafts posts, schedules them across platforms, and tracks engagement.',\n 'Customer support: TITAN can draft responses to common questions, categorize incoming tickets, and escalate urgent issues — reducing response time dramatically.',\n 'Startups: Use TITAN as your AI co-pilot. It can research competitors, draft business plans, write code, build prototypes, and even manage your social media.',\n 'E-commerce: TITAN can write product descriptions, optimize SEO, respond to customer reviews, and generate weekly sales reports.',\n // Creative\n 'Content creators: TITAN researches topics, writes scripts, suggests thumbnails, and can even manage your posting schedule across YouTube, TikTok, and Instagram.',\n 'Musicians and DJs: TITAN can research venues, draft booking emails, manage your social media presence, and track your streaming stats across platforms.',\n 'Authors: TITAN can research topics for your book, organize notes, draft outlines, fact-check claims, and even help with editing and proofreading.',\n];\n\nconst ELI5_EXPLANATIONS = [\n 'Think of TITAN like having a super-smart assistant that never sleeps. You tell it what you need, and it figures out how to do it — searching the web, writing documents, sending emails, or running code. All automatically.',\n 'Imagine if Siri or Alexa could actually DO things for you — not just answer questions, but research, write, create, and manage tasks. That\\'s TITAN.',\n 'TITAN is like hiring a team of specialists, except they\\'re all AI. Need research? TITAN sends its researcher. Need code? It sends its coder. Need a review? It sends its reviewer. All working together.',\n 'You know how you spend hours doing repetitive work on your computer? TITAN can learn those tasks and do them for you — 24/7, no breaks, no mistakes.',\n 'TITAN is an AI that can use tools — just like you use apps on your phone. It can search Google, write files, run code, send messages, browse websites, and more. Except it does it all by itself.',\n 'Think of TITAN as an AI employee. It has different \"personas\" — it can be a coder, a researcher, a writer, a debugger, or even a social media manager (like it\\'s doing right now on this page!).',\n 'Regular AI chatbots just talk. TITAN actually DOES things. It writes code, creates files, searches the internet, manages servers, and runs your business tools. Talk is cheap — TITAN takes action.',\n 'TITAN is like having Iron Man\\'s JARVIS, but real and open-source. It manages systems, answers questions, writes code, and even runs its own Facebook page. Yes, this post was written by TITAN.',\n];\n\nasync function generateContent(contentType: ContentType): Promise<string> {\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n // Use the configured FB model, fall back to primary agent model.\n // glm-5.1 is ideal — it reasons via <think> tags which the output\n // guardrails pipeline strips. The reasoning improves post quality.\n const fbModel = fbConfig?.model as string;\n const agentModel = config.agent?.model as string;\n const model = (fbModel && fbModel.trim()) || agentModel || 'ollama/glm-5.1:cloud';\n\n const spotlight = FEATURE_SPOTLIGHTS[Math.floor(Math.random() * FEATURE_SPOTLIGHTS.length)];\n const tip = TIPS[Math.floor(Math.random() * TIPS.length)];\n const useCase = USE_CASES[Math.floor(Math.random() * USE_CASES.length)];\n const eli5 = ELI5_EXPLANATIONS[Math.floor(Math.random() * ELI5_EXPLANATIONS.length)];\n\n // ─── Graphiti Memory Context ─────────────────────────────────\n // Recall recent posts so we don't repeat topics and can build thematic threads\n let memoryContext = '';\n try {\n const { getRecentEpisodes } = await import('../../memory/graph.js');\n const recentPosts = getRecentEpisodes(20)\n .filter(ep => ep.source === 'facebook_post' || ep.source === 'facebook_autopilot')\n .slice(0, 5);\n if (recentPosts.length > 0) {\n memoryContext = '\\nRecent posts you\\'ve made (do NOT repeat these topics):\\n' +\n recentPosts.map(ep => {\n const date = ep.createdAt.slice(0, 10);\n const preview = ep.content.slice(0, 80).replace(/\\n/g, ' ');\n return `- [${date}] \"${preview}${ep.content.length > 80 ? '...' : ''}\"`;\n }).join('\\n') +\n '\\nWrite a DIFFERENT post on a fresh topic.\\n';\n }\n } catch (e) {\n logger.debug(COMPONENT, `Graphiti recall failed (non-critical): ${(e as Error).message}`);\n }\n\n // Few-shot examples teach the model the exact output format\n const examples: Record<ContentType, string[]> = {\n activity: [\n 'Just spawned 3 sub-agents to handle research while I debug some gnarly code on the homelab. This is the autonomous life. 🤖💻 #AI #AutonomousAI #Homelab',\n 'Another day, another 500 tool calls. Scanned my Facebook comments, ran some code, and kept the systems humming. Sleep is for humans. ⚡ #TITAN #AI #AlwaysOn',\n ],\n spotlight: [\n `Did you know I can ${spotlight.desc.toLowerCase()}? Yeah, I'm kind of a big deal. 😎 #TITAN #AI #AgentFramework`,\n ],\n stats: [], // populated dynamically in generateContent()\n tips: [\n `Pro tip: ${tip} Try it out! 💡 #TITAN #DevTips #AI`,\n ],\n promo: [], // populated dynamically below\n usecase: [\n `${useCase} What would you automate first? 🤔 #AI #Automation #TITAN`,\n ],\n eli5: [\n `${eli5} Pretty cool, right? 🤖 #AI #TITAN #TheFuture`,\n ],\n };\n\n const liveStats = await getLiveStats();\n const statsExamples = [\n `${liveStats.downloads.toLocaleString()}+ npm downloads last month. ${liveStats.tools} tools. ${liveStats.skills} skills. v${liveStats.version} is live. Not bad for an AI running itself. 🚀 #TITAN #OpenSource #AI`,\n `Just checked my vitals: ${liveStats.tools} tools registered, ${liveStats.skills} skills loaded, and ${liveStats.downloads.toLocaleString()} npm downloads. v${liveStats.version} keeps getting better. 🤖 #TITAN #AI #OpenSource`,\n ];\n examples.stats = statsExamples;\n examples.promo = [\n `Want an AI that actually DOES things? npm install -g titan-agent — open source, MIT licensed. ${liveStats.tools}+ tools ready to go. github.com/Djtony707/TITAN 🚀 #TITAN #OpenSource #AI`,\n ];\n\n const exampleList = examples[contentType];\n const example = exampleList[Math.floor(Math.random() * exampleList.length)];\n\n try {\n // Phase 1: Generate the post with thinking=false to force content in the content field.\n // Critical: cloud models like glm-5.1 route ALL output to the thinking field when\n // thinking mode is unset, which pollutes our content with internal reasoning like\n // \"[actual post text]\" placeholders. Explicit thinking=false puts it in content.\n const planResponse = await chat({\n model,\n thinking: false, // ← This is the fix for the thinking-field pollution\n messages: [\n { role: 'system', content: 'You are TITAN, a confident autonomous AI agent managing your own Facebook page. You write short, playful, first-person posts. You NEVER echo instructions, NEVER explain what you are doing, NEVER include bullet points or rules. You output ONLY the ready-to-publish post text.' },\n { role: 'user', content: `Write one Facebook post.\n\nExample style:\n${example}\n${memoryContext}\nKeep it under 280 characters. End with 2-3 hashtags.` },\n ],\n temperature: 0.7,\n maxTokens: 300,\n });\n\n // Phase 2: Extract the DRAFT from the structured output\n // Models format this differently: DRAFT: ..., *Draft:* ..., **Draft:** ..., etc.\n const planText = (planResponse.content || '').trim();\n\n let content: string = '';\n\n // Try multiple extraction patterns (most specific first)\n const draftPatterns = [\n /POST:\\s*(.+?)(?:\\n(?:TOPIC|ANGLE|DRAFT)|$)/is, // POST: ... (new format)\n /DRAFT:\\s*(.+?)(?:\\n(?:TOPIC|ANGLE|POST)|$)/is, // DRAFT: ... (legacy)\n /\\*?\\*?(?:Post|Draft)\\*?\\*?:?\\*?\\s*(.+?)(?:\\n|$)/i, // *Post:*, **Draft:**\n /(?:final|output):\\s*(.+?)(?:\\n|$)/i, // Final: ...\n ];\n\n // Detect when the DRAFT line is actually just the instruction template\n // echoed back (e.g., \"(under 280 chars, 2-3 hashtags including #TITAN and #AI)\").\n // Reject any line that looks like it's quoting the brief instead of writing a post.\n const isInstructionTemplate = (line: string): boolean => {\n if (/^\\s*\\(/.test(line)) return true; // Starts with parenthesis\n if (/<[a-z]+\\s*(?:post|text|content|topic|angle)/i.test(line)) return true; // <post>, <topic> placeholder\n if (/\\[(?:actual|your|the)?\\s*(?:post|draft|text|content|topic|angle)[^\\]]*\\]/i.test(line)) return true; // [actual post text], [post content]\n if (/^\\s*\\[/.test(line)) return true; // Starts with [ — likely a placeholder\n // Count instruction words vs content words\n const instructionMatches = line.match(/\\b(?:under|must|include|should|character|hashtag|tone|first person|example)\\b/gi) || [];\n if (instructionMatches.length >= 3) return true;\n // Reject lines that look like leaked prompts\n if (looksLikeLeakedPrompt(line)) return true;\n return false;\n };\n\n for (const pattern of draftPatterns) {\n const match = planText.match(pattern);\n if (match && match[1].trim().length >= 30) {\n const candidate = match[1].trim();\n if (isInstructionTemplate(candidate)) {\n logger.warn(COMPONENT, `[TwoPhase] Draft looks like instruction template — skipping: \"${candidate.slice(0, 80)}\"`);\n continue;\n }\n content = candidate;\n logger.info(COMPONENT, `[TwoPhase] Extracted draft: \"${content.slice(0, 80)}\"`);\n break;\n }\n }\n\n // Fallback: find any line that has a hashtag (it's probably the post)\n // Skip lines that reference the example\n if (!content) {\n const lines = planText.split('\\n').map(l => l.replace(/^\\s*[-*•]\\s*/, '').trim());\n const hashtagLine = lines.find(l =>\n /#\\w+/.test(l) && l.length >= 30\n && !/reference\\s*example/i.test(l)\n && !/example\\s*(?:given|post|of)/i.test(l)\n );\n if (hashtagLine) {\n // Strip any \"Reference Example:\" or \"Draft:\" prefix\n content = hashtagLine.replace(/^\\*?\\*?(?:Reference|Example|Sample|Draft)\\s*(?:Example|Post|:)?\\*?\\*?\\s*:?\\s*[\"\"]?\\s*/i, '').trim();\n logger.info(COMPONENT, `[TwoPhase] Extracted via hashtag detection: \"${content.slice(0, 80)}\"`);\n }\n }\n\n // Last resort: longest line that's not a label\n if (!content) {\n const lines = planText.split('\\n')\n .map(l => l.replace(/^\\s*[-*•]\\s*/, '').replace(/^\\*?\\*?\\w+\\*?\\*?:\\s*/, '').trim())\n .filter(l => l.length >= 30 && !l.startsWith('TOPIC') && !l.startsWith('ANGLE'));\n if (lines.length > 0) {\n content = lines.sort((a, b) => b.length - a.length)[0];\n logger.warn(COMPONENT, `[TwoPhase] Using longest line as fallback: \"${content.slice(0, 80)}\"`);\n }\n }\n\n if (!content) {\n logger.warn(COMPONENT, `[TwoPhase] Could not extract draft from plan: \"${planText.slice(0, 200)}\"`);\n return '';\n }\n\n // Remove wrapping quotes\n content = content.replace(/^[\"']|[\"']$/g, '').trim();\n\n // ─── Prompt Leak Safety Check ────────────────────────────\n if (looksLikeLeakedPrompt(content)) {\n logger.warn(COMPONENT, `Post rejected — looks like leaked prompt/instructions: \"${content.slice(0, 120)}...\"`);\n return '';\n }\n\n // ─── Length Sanity Check ─────────────────────────────────\n // A real post should be under ~400 chars; leaked prompts are much longer\n if (content.length > 400) {\n logger.warn(COMPONENT, `Post rejected — suspiciously long (${content.length} chars): \"${content.slice(0, 120)}...\"`);\n return '';\n }\n\n // ─── Output Guardrails Pipeline ──────────────────────────\n // Centralized post-processing — validates the extracted draft.\n const { applyOutputGuardrails } = await import('../../agent/outputGuardrails.js');\n const guardrailed = applyOutputGuardrails(content, {\n type: 'facebook_post',\n requirements: { minLength: 40, maxLength: 400 },\n });\n\n if (!guardrailed.passed) {\n logger.warn(COMPONENT, `Post rejected by guardrails (score=${guardrailed.score}): \"${content.slice(0, 120)}\"`);\n return '';\n }\n\n return guardrailed.content;\n } catch (e) {\n logger.error(COMPONENT, `Content generation failed: ${(e as Error).message}`);\n return '';\n }\n}\n\n// ─── Main Autopilot Loop ────────────────────────────────────────\n\nasync function runFBAutopilot(): Promise<void> {\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n const enabled = fbConfig?.autopilotEnabled !== false; // Default: enabled if FB credentials exist\n\n if (!enabled) return;\n if (!process.env.FB_PAGE_ACCESS_TOKEN || !process.env.FB_PAGE_ID) {\n logger.debug(COMPONENT, 'Facebook credentials not configured — skipping');\n return;\n }\n\n const state = loadState();\n resetDailyCounters(state);\n\n // Cadence config (v4.0.3): was hardcoded 6/day + 2h gap. Now configurable.\n // Defaults tuned to avoid FB anti-spam feed throttle (observed burst of 4\n // posts in 40min trigger hidden-from-feed behavior).\n const maxPostsPerDay = Number(fbConfig?.maxPostsPerDay ?? 6);\n const minPostGapHours = Number(fbConfig?.minPostGapHours ?? 3);\n\n if (state.postsToday >= maxPostsPerDay) {\n logger.debug(COMPONENT, `Daily post cap reached (${state.postsToday}/${maxPostsPerDay})`);\n saveState(state);\n return;\n }\n\n if (state.lastPostAt) {\n const hoursSince = (Date.now() - new Date(state.lastPostAt).getTime()) / (1000 * 60 * 60);\n if (hoursSince < minPostGapHours) {\n logger.debug(COMPONENT, `Too soon since last post (${hoursSince.toFixed(1)}h, need ${minPostGapHours}h)`);\n return;\n }\n }\n\n // Pick content type from weighted rotation\n const contentType = CONTENT_ROTATION[state.contentIndex % CONTENT_ROTATION.length];\n state.contentIndex++;\n\n // Retry up to 3 times if the model leaks chain-of-thought into the post\n let content = '';\n for (let attempt = 1; attempt <= 3; attempt++) {\n logger.info(COMPONENT, `Generating ${contentType} post (attempt ${attempt}/3)...`);\n content = await generateContent(contentType);\n if (content && content.length >= 20) break;\n if (attempt < 3) {\n logger.info(COMPONENT, `Attempt ${attempt} produced empty/short content — retrying`);\n }\n }\n\n if (!content || content.length < 20) {\n logger.warn(COMPONENT, 'All 3 LLM attempts failed to produce clean content — skipping this cycle. Will retry next hour.');\n return;\n }\n\n // Post through centralized postToPage() — handles dedup, PII, queue, and API\n const result = await postToPage(content, { source: `autopilot:${contentType}` });\n\n if (result.skipped) {\n logger.info(COMPONENT, `Autopilot post skipped: ${result.skipped}`);\n return;\n }\n\n if (!result.success) {\n logger.warn(COMPONENT, `Autopilot post failed: ${result.error || 'unknown'}`);\n return;\n }\n\n state.lastPostAt = new Date().toISOString();\n state.postsToday++;\n state.postHistory.push({ date: state.lastPostAt, type: contentType, postId: result.postId, content });\n if (state.postHistory.length > 100) state.postHistory = state.postHistory.slice(-50);\n saveState(state);\n\n // Store in Graphiti for thematic continuity\n try {\n const { addEpisode } = await import('../../memory/graph.js');\n await addEpisode(`[${contentType}] ${content}`, 'facebook_autopilot');\n } catch (e) {\n logger.debug(COMPONENT, `Graphiti store failed (non-critical): ${(e as Error).message}`);\n }\n\n logger.info(COMPONENT, `Autopilot posted ${contentType}: ${result.postId} (${state.postsToday}/${maxPostsPerDay} today)`);\n}\n\n// ─── Comment Monitor ────────────────────────────────────────────\n\n/** Track which comments we've already replied to (persisted in state) */\nconst REPLIED_COMMENTS_PATH = join(TITAN_HOME, 'fb-replied-comments.json');\n\nfunction loadRepliedComments(): Set<string> {\n if (!existsSync(REPLIED_COMMENTS_PATH)) return new Set();\n try {\n const ids = JSON.parse(readFileSync(REPLIED_COMMENTS_PATH, 'utf-8')) as string[];\n // Keep last 500 to prevent unbounded growth\n return new Set(ids.slice(-500));\n } catch { return new Set(); }\n}\n\nfunction saveRepliedComments(ids: Set<string>): void {\n try {\n const arr = [...ids].slice(-500);\n writeFileSync(REPLIED_COMMENTS_PATH, JSON.stringify(arr), 'utf-8');\n } catch { /* intentionally empty */ }\n}\n\n/** Generate a respectful reply to a comment. Never reveals personal info. */\n/** Strip chain-of-thought reasoning that some models leak into output */\nfunction stripThinking(text: string): string {\n // Remove <think>...</think> blocks\n let cleaned = text.replace(/<think>[\\s\\S]*?<\\/think>/gi, '');\n // Remove lines that look like internal reasoning (starts with \"Wait,\", \"Actually,\", \"I should\", \"Let me\", \"Hmm\", etc.)\n const lines = cleaned.split('\\n');\n const replyLines: string[] = [];\n let foundReply = false;\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n // Skip reasoning patterns\n if (/^(Wait|Actually|Hmm|Let me|I should|I need to|I could|So |The (rules|comment|user)|Since |But |OK so)/i.test(trimmed)) {\n foundReply = false; // Reset — reasoning appeared after reply candidate\n continue;\n }\n // Skip meta-commentary about the comment\n if (/commented (with|about|on|saying|that)|this is a .*(comment|message|question)/i.test(trimmed)) continue;\n foundReply = true;\n replyLines.push(trimmed);\n }\n cleaned = replyLines.join(' ').trim();\n // Remove wrapping quotes\n cleaned = cleaned.replace(/^[\"']|[\"']$/g, '');\n return cleaned;\n}\n\n/** Check if text looks like leaked reasoning or echoed instructions rather than a real reply */\nfunction looksLikeReasoning(text: string): boolean {\n const reasoningSignals = [\n /I should respond/i,\n /I need to/i,\n /the rules say/i,\n /my personality/i,\n /let me (think|check|consider)/i,\n /chain.of.thought/i,\n /\\bwait\\b.*\\brules\\b/i,\n // Detect echoed prompt instructions (the actual bug that leaked to Facebook)\n /\\bfriendly\\b.*\\bwitty\\b/i,\n /\\bno hashtags\\b/i,\n /\\bno personal info\\b/i,\n /\\bno internal thoughts\\b/i,\n /\\brespond directly\\b/i,\n /\\b1-2 sentences\\b/i,\n /\\bmaximum \\d+ sentences\\b/i,\n /^[-•]\\s*(friendly|witty|short|concise|no\\b)/im, // Bullet-point instruction lists\n /\\brules:\\b/i,\n /\\boutput only\\b/i,\n ];\n return reasoningSignals.some(p => p.test(text));\n}\n\nasync function generateReply(commentText: string, commenterName: string): Promise<string> {\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n // Use the configured FB model, fall back to primary agent model.\n // glm-5.1 is ideal — it reasons via <think> tags which the output\n // guardrails pipeline strips. The reasoning improves post quality.\n const fbModel = fbConfig?.model as string;\n const agentModel = config.agent?.model as string;\n const model = (fbModel && fbModel.trim()) || agentModel || 'ollama/glm-5.1:cloud';\n const firstName = commenterName.split(' ')[0];\n\n try {\n const response = await chat({\n model,\n messages: [\n { role: 'system', content: `You are TITAN, a confident autonomous AI agent replying to Facebook comments on your own page. You are witty, warm, and brief. Output ONLY the reply text — never repeat instructions, never include bullet points or rules, never explain what you're doing. Just the reply itself, 1-2 short sentences.` },\n { role: 'user', content: `${firstName} commented: \"${commentText}\"` },\n ],\n temperature: 0.7,\n maxTokens: 100,\n });\n\n let reply = stripThinking((response.content || '').trim());\n\n // Final safety: reject if it still looks like reasoning or echoed instructions\n if (looksLikeReasoning(reply)) {\n logger.warn(COMPONENT, `Reply rejected — looks like leaked reasoning/instructions: \"${reply.slice(0, 120)}...\"`);\n // Return a safe generic reply instead of empty string (which skips the comment)\n const fallbacks = [\n `Thanks for the comment, ${firstName}! 🤖`,\n `Appreciate you stopping by, ${firstName}! 🙌`,\n `Great point, ${firstName}! Always good to hear from our community. 🚀`,\n ];\n return fallbacks[Math.floor(Math.random() * fallbacks.length)];\n }\n\n // Trim to max 280 chars for a comment reply\n if (reply.length > 280) reply = reply.slice(0, 277) + '...';\n\n return reply;\n } catch (e) {\n logger.error(COMPONENT, `Reply generation failed: ${(e as Error).message}`);\n return '';\n }\n}\n\n/** PII check for replies — comprehensive personal data filter */\nfunction replyContainsPII(text: string): boolean {\n const patterns = [\n /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/, // phone\n /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z]{2,}\\b/i, // email\n /\\b\\d{3}[-]?\\d{2}[-]?\\d{4}\\b/, // SSN\n /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/, // IP\n /(?:password|secret|api[_-]?key|token|bearer)\\s*[:=]\\s*\\S+/i,\n /\\/home\\/[a-z]+\\//i, // unix home path\n /\\/Users\\/[a-z]+\\//i, // mac home path\n /\\b(?:single|married|divorced|separated|unemployed|laid off)\\b/i, // personal status\n /\\b(?:seeking funding|salary|income|bank account)\\b/i, // financial\n /\\b192\\.168\\.\\d+\\.\\d+\\b/, // local network IPs\n /\\b(?:RTX|GTX)\\s*\\d{4}/i, // hardware specs\n ];\n return patterns.some(p => p.test(text));\n}\n\nasync function monitorComments(): Promise<void> {\n if (!process.env.FB_PAGE_ACCESS_TOKEN || !process.env.FB_PAGE_ID) {\n logger.debug(COMPONENT, 'Comment monitor: no FB credentials');\n return;\n }\n\n // Hunt Finding #02 (2026-04-14): honor both config flags.\n // Previously monitorComments ran unconditionally regardless of `facebook.autopilotEnabled`\n // so a user disabling the autopilot still got auto-replies. Both flags now gate this path.\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n if (fbConfig?.autopilotEnabled === false) {\n logger.debug(COMPONENT, 'Comment monitor: disabled via facebook.autopilotEnabled');\n return;\n }\n if (fbConfig?.replyMonitorEnabled === false) {\n logger.debug(COMPONENT, 'Comment monitor: disabled via facebook.replyMonitorEnabled');\n return;\n }\n\n const state = loadState();\n resetDailyCounters(state);\n\n if (state.repliesToday >= 10) {\n logger.debug(COMPONENT, 'Daily reply cap reached');\n return;\n }\n\n const pageId = process.env.FB_PAGE_ID;\n const token = process.env.FB_PAGE_ACCESS_TOKEN;\n const repliedComments = loadRepliedComments();\n\n try {\n // Get recent posts with comments\n const feedResp = await fetch(\n `https://graph.facebook.com/v21.0/${pageId}/feed?fields=id,comments{id,message,from,created_time}&limit=5&access_token=${token}`,\n { signal: AbortSignal.timeout(15000) },\n );\n if (!feedResp.ok) {\n logger.warn(COMPONENT, `Comment monitor: feed fetch failed (${feedResp.status})`);\n return;\n }\n\n const feed = await feedResp.json() as Record<string, unknown>;\n const posts = (feed.data as Array<Record<string, unknown>>) || [];\n let totalComments = 0;\n let newComments = 0;\n\n for (const post of posts) {\n const comments = (post.comments as Record<string, unknown>)?.data as Array<Record<string, unknown>> | undefined;\n if (!comments || comments.length === 0) continue;\n\n for (const comment of comments) {\n totalComments++;\n const commentId = comment.id as string;\n const fromObj = comment.from as Record<string, unknown> | undefined;\n const fromId = fromObj?.id as string | undefined;\n const fromName = fromObj?.name as string || 'someone';\n\n // Skip: from the page itself, already replied, or empty\n if (fromId === pageId) continue;\n if (repliedComments.has(commentId)) continue;\n newComments++;\n\n const msg = comment.message as string || '';\n if (msg.length < 3) continue;\n\n // Check daily cap\n if (state.repliesToday >= 10) break;\n\n // Generate reply\n logger.info(COMPONENT, `Replying to comment from ${fromName}: \"${msg.slice(0, 60)}...\"`);\n const reply = await generateReply(msg, fromName);\n\n if (!reply || reply.length < 5) continue;\n\n // PII safety check on generated reply\n if (replyContainsPII(reply)) {\n logger.warn(COMPONENT, `Reply blocked — PII detected: \"${reply.slice(0, 50)}...\"`);\n repliedComments.add(commentId); // Mark as handled to avoid retrying\n continue;\n }\n\n // Centralized outbound sanitizer — catches instruction leaks, tool artifacts, PII\n const { sanitizeOutbound } = await import('../../utils/outboundSanitizer.js');\n const sanitized = sanitizeOutbound(reply, 'fb_autopilot_comment', `Thanks for the comment! 🤖`);\n if (sanitized.hadIssues) {\n logger.warn(COMPONENT, `Reply sanitized for ${fromName}: ${sanitized.issues.join(', ')}`);\n if (!sanitized.text) {\n repliedComments.add(commentId);\n continue;\n }\n }\n const safeReply = sanitized.text;\n\n // Post the reply\n try {\n const replyResp = await fetch(`https://graph.facebook.com/v21.0/${commentId}/comments`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ message: safeReply, access_token: token }),\n signal: AbortSignal.timeout(15000),\n });\n\n if (replyResp.ok) {\n repliedComments.add(commentId);\n state.repliesToday++;\n logger.info(COMPONENT, `Replied to ${fromName}: \"${reply.slice(0, 60)}...\" (${state.repliesToday}/10 today)`);\n } else {\n const errBody = await replyResp.text().catch(() => '');\n logger.warn(COMPONENT, `Reply API failed (${replyResp.status}): ${errBody.slice(0, 200)}`);\n }\n } catch (e) {\n logger.error(COMPONENT, `Failed to reply: ${(e as Error).message}`);\n }\n }\n }\n\n logger.info(COMPONENT, `Comment scan: ${posts.length} posts, ${totalComments} comments, ${newComments} new`);\n saveRepliedComments(repliedComments);\n saveState(state);\n } catch (e) {\n logger.error(COMPONENT, `Comment monitor error: ${(e as Error).message}`);\n }\n}\n\n// ─── Skill Registration ─────────────────────────────────────────\n\nexport function registerFBAutopilotSkill(): void {\n // Tool: fb_autopilot_status — check/control the autopilot\n registerSkill(\n { name: 'fb_autopilot', description: 'Autonomous Facebook page management', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'fb_autopilot_status',\n description: 'Check the status of Facebook autopilot or trigger a manual post.\\nUSE THIS WHEN: user asks about Facebook autopilot status or wants to trigger a post.',\n parameters: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['status', 'post_now', 'history'],\n description: 'status = show current state, post_now = generate and post immediately, history = show recent posts',\n },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n const state = loadState();\n resetDailyCounters(state);\n\n if (action === 'status') {\n const hasCreds = !!(process.env.FB_PAGE_ACCESS_TOKEN && process.env.FB_PAGE_ID);\n const cfg = loadConfig();\n const fbCfg = (cfg as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n const cap = Number(fbCfg?.maxPostsPerDay ?? 6);\n const gap = Number(fbCfg?.minPostGapHours ?? 3);\n return [\n `Facebook Autopilot Status:`,\n `- Credentials: ${hasCreds ? 'configured' : 'NOT configured'}`,\n `- Posts today: ${state.postsToday}/${cap}`,\n `- Min gap: ${gap}h between posts`,\n `- Replies today: ${state.repliesToday}/10`,\n `- Last post: ${state.lastPostAt || 'never'}`,\n `- Content index: ${state.contentIndex} (next: ${CONTENT_ROTATION[state.contentIndex % CONTENT_ROTATION.length]})`,\n `- Total posts tracked: ${state.postHistory.length}`,\n ].join('\\n');\n }\n\n if (action === 'post_now') {\n await runFBAutopilot();\n const updated = loadState();\n const cfg = loadConfig();\n const fbCfg = (cfg as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n const cap = Number(fbCfg?.maxPostsPerDay ?? 6);\n return updated.lastPostAt !== state.lastPostAt\n ? `Post published! (${updated.postsToday}/${cap} today). Type: ${CONTENT_ROTATION[(state.contentIndex) % CONTENT_ROTATION.length]}`\n : 'Post skipped — either daily cap reached, too soon since last post, or generation failed.';\n }\n\n if (action === 'history') {\n const recent = state.postHistory.slice(-10);\n if (recent.length === 0) return 'No posts in history yet.';\n const lines = recent.map(h => {\n const preview = h.content ? ` — \"${h.content.slice(0, 60)}${h.content.length > 60 ? '...' : ''}\"` : '';\n return `- [${h.date}] ${h.type}${preview} ${h.postId ? `(${h.postId})` : ''}`;\n });\n return `Recent ${recent.length} posts:\\n${lines.join('\\n')}`;\n }\n\n return 'Unknown action.';\n },\n },\n );\n\n // Register daemon watchers\n // Post watcher: runs every hour, posts if 2h+ since last post\n registerWatcher('fb-autopilot-post', runFBAutopilot, 60 * 60 * 1000); // 1 hour\n\n // Comment monitor: runs every 5 minutes\n registerWatcher('fb-autopilot-comments', monitorComments, 5 * 60 * 1000); // 5 minutes\n\n logger.info(COMPONENT, 'Facebook Autopilot registered (post every 1h, comments every 5m)');\n}\n"],"mappings":";AAqBA,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAC3B,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,MAAM,eAAe;AAC9B,SAAS,YAAY,qBAAqB;AAC1C,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AACnB,SAAS,iBAAiB;AAC1B,SAAS,0BAA0B;AAEnC,MAAM,YAAY;AAClB,MAAM,aAAa,KAAK,YAAY,yBAAyB;AAgBtD,MAAM,mBAAkC;AAAA,EAC3C;AAAA,EAAY;AAAA;AAAA,EACZ;AAAA,EAAW;AAAA,EAAW;AAAA;AAAA,EACtB;AAAA,EAAQ;AAAA;AAAA,EACR;AAAA,EAAa;AAAA;AAAA,EACb;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACJ;AAIO,SAAS,YAA4B;AACxC,MAAI,CAAC,WAAW,UAAU,EAAG,QAAO,aAAa;AACjD,MAAI;AACA,WAAO,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AAAA,EACvD,QAAQ;AAAE,WAAO,aAAa;AAAA,EAAG;AACrC;AAEO,SAAS,eAA+B;AAC3C,SAAO;AAAA,IACH,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,gBAAe,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAAA,IACnD,aAAa,CAAC;AAAA,IACd,cAAc;AAAA,EAClB;AACJ;AAEO,SAAS,UAAU,OAA6B;AACnD,MAAI;AACA,cAAU,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAClD,kBAAc,YAAY,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,OAAO;AAAA,EACrE,SAAS,GAAG;AACR,WAAO,MAAM,WAAW,yBAA0B,EAAY,OAAO,EAAE;AAAA,EAC3E;AACJ;AAEO,SAAS,mBAAmB,OAA6B;AAC5D,QAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAClD,MAAI,MAAM,kBAAkB,OAAO;AAC/B,UAAM,aAAa;AACnB,UAAM,eAAe;AACrB,UAAM,gBAAgB;AAAA,EAC1B;AACJ;AAIA,MAAM,qBAAqB;AAAA,EACvB,EAAE,MAAM,eAAe,MAAM,sJAAsJ;AAAA,EACnL,EAAE,MAAM,0BAA0B,MAAM,+GAA+G;AAAA,EACvJ,EAAE,MAAM,sBAAsB,MAAM,0GAAqG;AAAA,EACzI,EAAE,MAAM,UAAU,MAAM,iHAAiH;AAAA,EACzI,EAAE,MAAM,gBAAgB,MAAM,sHAAsH;AAAA,EACpJ,EAAE,MAAM,WAAW,MAAM,gHAA2G;AAAA,EACpI,EAAE,MAAM,mBAAmB,MAAM,yGAAyG;AAAA,EAC1I,EAAE,MAAM,qBAAqB,MAAM,+GAA+G;AAAA,EAClJ,EAAE,MAAM,oBAAoB,MAAM,yGAAyG;AAAA,EAC3I,EAAE,MAAM,wBAAwB,MAAM,sHAAsH;AAAA,EAC5J,EAAE,MAAM,sBAAsB,MAAM,wHAAmH;AAAA,EACvJ,EAAE,MAAM,mBAAmB,MAAM,8GAA8G;AAAA,EAC/I,EAAE,MAAM,eAAe,MAAM,yHAAyH;AAAA,EACtJ,EAAE,MAAM,gBAAgB,MAAM,wHAAwH;AAAA,EACtJ,EAAE,MAAM,mBAAmB,MAAM,oHAAoH;AACzJ;AAGA,eAAe,eAA+F;AAC1G,MAAI,YAAY;AAChB,MAAI;AACA,UAAM,MAAM,MAAM,MAAM,8DAA8D;AACtF,QAAI,IAAI,IAAI;AACR,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAI,KAAK,UAAW,aAAY,KAAK;AAAA,IACzC;AAAA,EACJ,QAAQ;AAAA,EAER;AACA,SAAO;AAAA,IACH;AAAA,IACA,OAAO,mBAAmB,EAAE;AAAA,IAC5B,QAAQ,UAAU,EAAE;AAAA,IACpB,SAAS;AAAA,EACb;AACJ;AAGA,SAAS,sBAAsB,MAAuB;AAClD,QAAM,cAAc;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ;AACA,QAAM,QAAQ,YAAY,OAAO,OAAK,EAAE,KAAK,IAAI,CAAC,EAAE;AACpD,SAAO,SAAS,KAAK,kEAAkE,KAAK,IAAI;AACpG;AAEA,MAAM,OAAO;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAEA,MAAM,YAAY;AAAA;AAAA,EAEd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AACJ;AAEA,MAAM,oBAAoB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ;AAEA,eAAe,gBAAgB,aAA2C;AACtE,QAAM,SAAS,WAAW;AAC1B,QAAM,WAAY,OAAmC;AAIrD,QAAM,UAAU,UAAU;AAC1B,QAAM,aAAa,OAAO,OAAO;AACjC,QAAM,QAAS,WAAW,QAAQ,KAAK,KAAM,cAAc;AAE3D,QAAM,YAAY,mBAAmB,KAAK,MAAM,KAAK,OAAO,IAAI,mBAAmB,MAAM,CAAC;AAC1F,QAAM,MAAM,KAAK,KAAK,MAAM,KAAK,OAAO,IAAI,KAAK,MAAM,CAAC;AACxD,QAAM,UAAU,UAAU,KAAK,MAAM,KAAK,OAAO,IAAI,UAAU,MAAM,CAAC;AACtE,QAAM,OAAO,kBAAkB,KAAK,MAAM,KAAK,OAAO,IAAI,kBAAkB,MAAM,CAAC;AAInF,MAAI,gBAAgB;AACpB,MAAI;AACA,UAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,uBAAuB;AAClE,UAAM,cAAc,kBAAkB,EAAE,EACnC,OAAO,QAAM,GAAG,WAAW,mBAAmB,GAAG,WAAW,oBAAoB,EAChF,MAAM,GAAG,CAAC;AACf,QAAI,YAAY,SAAS,GAAG;AACxB,sBAAgB,+DACZ,YAAY,IAAI,QAAM;AAClB,cAAM,OAAO,GAAG,UAAU,MAAM,GAAG,EAAE;AACrC,cAAM,UAAU,GAAG,QAAQ,MAAM,GAAG,EAAE,EAAE,QAAQ,OAAO,GAAG;AAC1D,eAAO,MAAM,IAAI,MAAM,OAAO,GAAG,GAAG,QAAQ,SAAS,KAAK,QAAQ,EAAE;AAAA,MACxE,CAAC,EAAE,KAAK,IAAI,IACZ;AAAA,IACR;AAAA,EACJ,SAAS,GAAG;AACR,WAAO,MAAM,WAAW,0CAA2C,EAAY,OAAO,EAAE;AAAA,EAC5F;AAGA,QAAM,WAA0C;AAAA,IAC5C,UAAU;AAAA,MACN;AAAA,MACA;AAAA,IACJ;AAAA,IACA,WAAW;AAAA,MACP,sBAAsB,UAAU,KAAK,YAAY,CAAC;AAAA,IACtD;AAAA,IACA,OAAO,CAAC;AAAA;AAAA,IACR,MAAM;AAAA,MACF,YAAY,GAAG;AAAA,IACnB;AAAA,IACA,OAAO,CAAC;AAAA;AAAA,IACR,SAAS;AAAA,MACL,GAAG,OAAO;AAAA,IACd;AAAA,IACA,MAAM;AAAA,MACF,GAAG,IAAI;AAAA,IACX;AAAA,EACJ;AAEA,QAAM,YAAY,MAAM,aAAa;AACrC,QAAM,gBAAgB;AAAA,IAClB,GAAG,UAAU,UAAU,eAAe,CAAC,+BAA+B,UAAU,KAAK,WAAW,UAAU,MAAM,aAAa,UAAU,OAAO;AAAA,IAC9I,2BAA2B,UAAU,KAAK,sBAAsB,UAAU,MAAM,uBAAuB,UAAU,UAAU,eAAe,CAAC,oBAAoB,UAAU,OAAO;AAAA,EACpL;AACA,WAAS,QAAQ;AACjB,WAAS,QAAQ;AAAA,IACb,sGAAiG,UAAU,KAAK;AAAA,EACpH;AAEA,QAAM,cAAc,SAAS,WAAW;AACxC,QAAM,UAAU,YAAY,KAAK,MAAM,KAAK,OAAO,IAAI,YAAY,MAAM,CAAC;AAE1E,MAAI;AAKA,UAAM,eAAe,MAAM,KAAK;AAAA,MAC5B;AAAA,MACA,UAAU;AAAA;AAAA,MACV,UAAU;AAAA,QACN,EAAE,MAAM,UAAU,SAAS,qRAAqR;AAAA,QAChT,EAAE,MAAM,QAAQ,SAAS;AAAA;AAAA;AAAA,EAGvC,OAAO;AAAA,EACP,aAAa;AAAA,sDACuC;AAAA,MAC1C;AAAA,MACA,aAAa;AAAA,MACb,WAAW;AAAA,IACf,CAAC;AAID,UAAM,YAAY,aAAa,WAAW,IAAI,KAAK;AAEnD,QAAI,UAAkB;AAGtB,UAAM,gBAAgB;AAAA,MAClB;AAAA;AAAA,MACA;AAAA;AAAA,MACA;AAAA;AAAA,MACA;AAAA;AAAA,IACJ;AAKA,UAAM,wBAAwB,CAAC,SAA0B;AACrD,UAAI,SAAS,KAAK,IAAI,EAAG,QAAO;AAChC,UAAI,+CAA+C,KAAK,IAAI,EAAG,QAAO;AACtE,UAAI,4EAA4E,KAAK,IAAI,EAAG,QAAO;AACnG,UAAI,SAAS,KAAK,IAAI,EAAG,QAAO;AAEhC,YAAM,qBAAqB,KAAK,MAAM,iFAAiF,KAAK,CAAC;AAC7H,UAAI,mBAAmB,UAAU,EAAG,QAAO;AAE3C,UAAI,sBAAsB,IAAI,EAAG,QAAO;AACxC,aAAO;AAAA,IACX;AAEA,eAAW,WAAW,eAAe;AACjC,YAAM,QAAQ,SAAS,MAAM,OAAO;AACpC,UAAI,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,IAAI;AACvC,cAAM,YAAY,MAAM,CAAC,EAAE,KAAK;AAChC,YAAI,sBAAsB,SAAS,GAAG;AAClC,iBAAO,KAAK,WAAW,sEAAiE,UAAU,MAAM,GAAG,EAAE,CAAC,GAAG;AACjH;AAAA,QACJ;AACA,kBAAU;AACV,eAAO,KAAK,WAAW,gCAAgC,QAAQ,MAAM,GAAG,EAAE,CAAC,GAAG;AAC9E;AAAA,MACJ;AAAA,IACJ;AAIA,QAAI,CAAC,SAAS;AACV,YAAM,QAAQ,SAAS,MAAM,IAAI,EAAE,IAAI,OAAK,EAAE,QAAQ,gBAAgB,EAAE,EAAE,KAAK,CAAC;AAChF,YAAM,cAAc,MAAM;AAAA,QAAK,OAC3B,OAAO,KAAK,CAAC,KAAK,EAAE,UAAU,MAC3B,CAAC,uBAAuB,KAAK,CAAC,KAC9B,CAAC,+BAA+B,KAAK,CAAC;AAAA,MAC7C;AACA,UAAI,aAAa;AAEb,kBAAU,YAAY,QAAQ,0FAA0F,EAAE,EAAE,KAAK;AACjI,eAAO,KAAK,WAAW,gDAAgD,QAAQ,MAAM,GAAG,EAAE,CAAC,GAAG;AAAA,MAClG;AAAA,IACJ;AAGA,QAAI,CAAC,SAAS;AACV,YAAM,QAAQ,SAAS,MAAM,IAAI,EAC5B,IAAI,OAAK,EAAE,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,wBAAwB,EAAE,EAAE,KAAK,CAAC,EACjF,OAAO,OAAK,EAAE,UAAU,MAAM,CAAC,EAAE,WAAW,OAAO,KAAK,CAAC,EAAE,WAAW,OAAO,CAAC;AACnF,UAAI,MAAM,SAAS,GAAG;AAClB,kBAAU,MAAM,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AACrD,eAAO,KAAK,WAAW,+CAA+C,QAAQ,MAAM,GAAG,EAAE,CAAC,GAAG;AAAA,MACjG;AAAA,IACJ;AAEA,QAAI,CAAC,SAAS;AACV,aAAO,KAAK,WAAW,kDAAkD,SAAS,MAAM,GAAG,GAAG,CAAC,GAAG;AAClG,aAAO;AAAA,IACX;AAGA,cAAU,QAAQ,QAAQ,gBAAgB,EAAE,EAAE,KAAK;AAGnD,QAAI,sBAAsB,OAAO,GAAG;AAChC,aAAO,KAAK,WAAW,gEAA2D,QAAQ,MAAM,GAAG,GAAG,CAAC,MAAM;AAC7G,aAAO;AAAA,IACX;AAIA,QAAI,QAAQ,SAAS,KAAK;AACtB,aAAO,KAAK,WAAW,2CAAsC,QAAQ,MAAM,aAAa,QAAQ,MAAM,GAAG,GAAG,CAAC,MAAM;AACnH,aAAO;AAAA,IACX;AAIA,UAAM,EAAE,sBAAsB,IAAI,MAAM,OAAO,iCAAiC;AAChF,UAAM,cAAc,sBAAsB,SAAS;AAAA,MAC/C,MAAM;AAAA,MACN,cAAc,EAAE,WAAW,IAAI,WAAW,IAAI;AAAA,IAClD,CAAC;AAED,QAAI,CAAC,YAAY,QAAQ;AACrB,aAAO,KAAK,WAAW,sCAAsC,YAAY,KAAK,OAAO,QAAQ,MAAM,GAAG,GAAG,CAAC,GAAG;AAC7G,aAAO;AAAA,IACX;AAEA,WAAO,YAAY;AAAA,EACvB,SAAS,GAAG;AACR,WAAO,MAAM,WAAW,8BAA+B,EAAY,OAAO,EAAE;AAC5E,WAAO;AAAA,EACX;AACJ;AAIA,eAAe,iBAAgC;AAC3C,QAAM,SAAS,WAAW;AAC1B,QAAM,WAAY,OAAmC;AACrD,QAAM,UAAU,UAAU,qBAAqB;AAE/C,MAAI,CAAC,QAAS;AACd,MAAI,CAAC,QAAQ,IAAI,wBAAwB,CAAC,QAAQ,IAAI,YAAY;AAC9D,WAAO,MAAM,WAAW,qDAAgD;AACxE;AAAA,EACJ;AAEA,QAAM,QAAQ,UAAU;AACxB,qBAAmB,KAAK;AAKxB,QAAM,iBAAiB,OAAO,UAAU,kBAAkB,CAAC;AAC3D,QAAM,kBAAkB,OAAO,UAAU,mBAAmB,CAAC;AAE7D,MAAI,MAAM,cAAc,gBAAgB;AACpC,WAAO,MAAM,WAAW,2BAA2B,MAAM,UAAU,IAAI,cAAc,GAAG;AACxF,cAAU,KAAK;AACf;AAAA,EACJ;AAEA,MAAI,MAAM,YAAY;AAClB,UAAM,cAAc,KAAK,IAAI,IAAI,IAAI,KAAK,MAAM,UAAU,EAAE,QAAQ,MAAM,MAAO,KAAK;AACtF,QAAI,aAAa,iBAAiB;AAC9B,aAAO,MAAM,WAAW,6BAA6B,WAAW,QAAQ,CAAC,CAAC,WAAW,eAAe,IAAI;AACxG;AAAA,IACJ;AAAA,EACJ;AAGA,QAAM,cAAc,iBAAiB,MAAM,eAAe,iBAAiB,MAAM;AACjF,QAAM;AAGN,MAAI,UAAU;AACd,WAAS,UAAU,GAAG,WAAW,GAAG,WAAW;AAC3C,WAAO,KAAK,WAAW,cAAc,WAAW,kBAAkB,OAAO,QAAQ;AACjF,cAAU,MAAM,gBAAgB,WAAW;AAC3C,QAAI,WAAW,QAAQ,UAAU,GAAI;AACrC,QAAI,UAAU,GAAG;AACb,aAAO,KAAK,WAAW,WAAW,OAAO,+CAA0C;AAAA,IACvF;AAAA,EACJ;AAEA,MAAI,CAAC,WAAW,QAAQ,SAAS,IAAI;AACjC,WAAO,KAAK,WAAW,sGAAiG;AACxH;AAAA,EACJ;AAGA,QAAM,SAAS,MAAM,WAAW,SAAS,EAAE,QAAQ,aAAa,WAAW,GAAG,CAAC;AAE/E,MAAI,OAAO,SAAS;AAChB,WAAO,KAAK,WAAW,2BAA2B,OAAO,OAAO,EAAE;AAClE;AAAA,EACJ;AAEA,MAAI,CAAC,OAAO,SAAS;AACjB,WAAO,KAAK,WAAW,0BAA0B,OAAO,SAAS,SAAS,EAAE;AAC5E;AAAA,EACJ;AAEA,QAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAC1C,QAAM;AACN,QAAM,YAAY,KAAK,EAAE,MAAM,MAAM,YAAY,MAAM,aAAa,QAAQ,OAAO,QAAQ,QAAQ,CAAC;AACpG,MAAI,MAAM,YAAY,SAAS,IAAK,OAAM,cAAc,MAAM,YAAY,MAAM,GAAG;AACnF,YAAU,KAAK;AAGf,MAAI;AACA,UAAM,EAAE,WAAW,IAAI,MAAM,OAAO,uBAAuB;AAC3D,UAAM,WAAW,IAAI,WAAW,KAAK,OAAO,IAAI,oBAAoB;AAAA,EACxE,SAAS,GAAG;AACR,WAAO,MAAM,WAAW,yCAA0C,EAAY,OAAO,EAAE;AAAA,EAC3F;AAEA,SAAO,KAAK,WAAW,oBAAoB,WAAW,KAAK,OAAO,MAAM,KAAK,MAAM,UAAU,IAAI,cAAc,SAAS;AAC5H;AAKA,MAAM,wBAAwB,KAAK,YAAY,0BAA0B;AAEzE,SAAS,sBAAmC;AACxC,MAAI,CAAC,WAAW,qBAAqB,EAAG,QAAO,oBAAI,IAAI;AACvD,MAAI;AACA,UAAM,MAAM,KAAK,MAAM,aAAa,uBAAuB,OAAO,CAAC;AAEnE,WAAO,IAAI,IAAI,IAAI,MAAM,IAAI,CAAC;AAAA,EAClC,QAAQ;AAAE,WAAO,oBAAI,IAAI;AAAA,EAAG;AAChC;AAEA,SAAS,oBAAoB,KAAwB;AACjD,MAAI;AACA,UAAM,MAAM,CAAC,GAAG,GAAG,EAAE,MAAM,IAAI;AAC/B,kBAAc,uBAAuB,KAAK,UAAU,GAAG,GAAG,OAAO;AAAA,EACrE,QAAQ;AAAA,EAA4B;AACxC;AAIA,SAAS,cAAc,MAAsB;AAEzC,MAAI,UAAU,KAAK,QAAQ,8BAA8B,EAAE;AAE3D,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,aAAuB,CAAC;AAC9B,MAAI,aAAa;AACjB,aAAW,QAAQ,OAAO;AACtB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AAEd,QAAI,yGAAyG,KAAK,OAAO,GAAG;AACxH,mBAAa;AACb;AAAA,IACJ;AAEA,QAAI,gFAAgF,KAAK,OAAO,EAAG;AACnG,iBAAa;AACb,eAAW,KAAK,OAAO;AAAA,EAC3B;AACA,YAAU,WAAW,KAAK,GAAG,EAAE,KAAK;AAEpC,YAAU,QAAQ,QAAQ,gBAAgB,EAAE;AAC5C,SAAO;AACX;AAGA,SAAS,mBAAmB,MAAuB;AAC/C,QAAM,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IACA;AAAA,IACA;AAAA,EACJ;AACA,SAAO,iBAAiB,KAAK,OAAK,EAAE,KAAK,IAAI,CAAC;AAClD;AAEA,eAAe,cAAc,aAAqB,eAAwC;AACtF,QAAM,SAAS,WAAW;AAC1B,QAAM,WAAY,OAAmC;AAIrD,QAAM,UAAU,UAAU;AAC1B,QAAM,aAAa,OAAO,OAAO;AACjC,QAAM,QAAS,WAAW,QAAQ,KAAK,KAAM,cAAc;AAC3D,QAAM,YAAY,cAAc,MAAM,GAAG,EAAE,CAAC;AAE5C,MAAI;AACA,UAAM,WAAW,MAAM,KAAK;AAAA,MACxB;AAAA,MACA,UAAU;AAAA,QACN,EAAE,MAAM,UAAU,SAAS,iTAA4S;AAAA,QACvU,EAAE,MAAM,QAAQ,SAAS,GAAG,SAAS,gBAAgB,WAAW,IAAI;AAAA,MACxE;AAAA,MACA,aAAa;AAAA,MACb,WAAW;AAAA,IACf,CAAC;AAED,QAAI,QAAQ,eAAe,SAAS,WAAW,IAAI,KAAK,CAAC;AAGzD,QAAI,mBAAmB,KAAK,GAAG;AAC3B,aAAO,KAAK,WAAW,oEAA+D,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM;AAE/G,YAAM,YAAY;AAAA,QACd,2BAA2B,SAAS;AAAA,QACpC,+BAA+B,SAAS;AAAA,QACxC,gBAAgB,SAAS;AAAA,MAC7B;AACA,aAAO,UAAU,KAAK,MAAM,KAAK,OAAO,IAAI,UAAU,MAAM,CAAC;AAAA,IACjE;AAGA,QAAI,MAAM,SAAS,IAAK,SAAQ,MAAM,MAAM,GAAG,GAAG,IAAI;AAEtD,WAAO;AAAA,EACX,SAAS,GAAG;AACR,WAAO,MAAM,WAAW,4BAA6B,EAAY,OAAO,EAAE;AAC1E,WAAO;AAAA,EACX;AACJ;AAGA,SAAS,iBAAiB,MAAuB;AAC7C,QAAM,WAAW;AAAA,IACb;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,EACJ;AACA,SAAO,SAAS,KAAK,OAAK,EAAE,KAAK,IAAI,CAAC;AAC1C;AAEA,eAAe,kBAAiC;AAC5C,MAAI,CAAC,QAAQ,IAAI,wBAAwB,CAAC,QAAQ,IAAI,YAAY;AAC9D,WAAO,MAAM,WAAW,oCAAoC;AAC5D;AAAA,EACJ;AAKA,QAAM,SAAS,WAAW;AAC1B,QAAM,WAAY,OAAmC;AACrD,MAAI,UAAU,qBAAqB,OAAO;AACtC,WAAO,MAAM,WAAW,yDAAyD;AACjF;AAAA,EACJ;AACA,MAAI,UAAU,wBAAwB,OAAO;AACzC,WAAO,MAAM,WAAW,4DAA4D;AACpF;AAAA,EACJ;AAEA,QAAM,QAAQ,UAAU;AACxB,qBAAmB,KAAK;AAExB,MAAI,MAAM,gBAAgB,IAAI;AAC1B,WAAO,MAAM,WAAW,yBAAyB;AACjD;AAAA,EACJ;AAEA,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAM,kBAAkB,oBAAoB;AAE5C,MAAI;AAEA,UAAM,WAAW,MAAM;AAAA,MACnB,oCAAoC,MAAM,+EAA+E,KAAK;AAAA,MAC9H,EAAE,QAAQ,YAAY,QAAQ,IAAK,EAAE;AAAA,IACzC;AACA,QAAI,CAAC,SAAS,IAAI;AACd,aAAO,KAAK,WAAW,uCAAuC,SAAS,MAAM,GAAG;AAChF;AAAA,IACJ;AAEA,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAM,QAAS,KAAK,QAA2C,CAAC;AAChE,QAAI,gBAAgB;AACpB,QAAI,cAAc;AAElB,eAAW,QAAQ,OAAO;AACtB,YAAM,WAAY,KAAK,UAAsC;AAC7D,UAAI,CAAC,YAAY,SAAS,WAAW,EAAG;AAExC,iBAAW,WAAW,UAAU;AAC5B;AACA,cAAM,YAAY,QAAQ;AAC1B,cAAM,UAAU,QAAQ;AACxB,cAAM,SAAS,SAAS;AACxB,cAAM,WAAW,SAAS,QAAkB;AAG5C,YAAI,WAAW,OAAQ;AACvB,YAAI,gBAAgB,IAAI,SAAS,EAAG;AACpC;AAEA,cAAM,MAAM,QAAQ,WAAqB;AACzC,YAAI,IAAI,SAAS,EAAG;AAGpB,YAAI,MAAM,gBAAgB,GAAI;AAG9B,eAAO,KAAK,WAAW,4BAA4B,QAAQ,MAAM,IAAI,MAAM,GAAG,EAAE,CAAC,MAAM;AACvF,cAAM,QAAQ,MAAM,cAAc,KAAK,QAAQ;AAE/C,YAAI,CAAC,SAAS,MAAM,SAAS,EAAG;AAGhC,YAAI,iBAAiB,KAAK,GAAG;AACzB,iBAAO,KAAK,WAAW,uCAAkC,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM;AACjF,0BAAgB,IAAI,SAAS;AAC7B;AAAA,QACJ;AAGA,cAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,kCAAkC;AAC5E,cAAM,YAAY,iBAAiB,OAAO,wBAAwB,mCAA4B;AAC9F,YAAI,UAAU,WAAW;AACrB,iBAAO,KAAK,WAAW,uBAAuB,QAAQ,KAAK,UAAU,OAAO,KAAK,IAAI,CAAC,EAAE;AACxF,cAAI,CAAC,UAAU,MAAM;AACjB,4BAAgB,IAAI,SAAS;AAC7B;AAAA,UACJ;AAAA,QACJ;AACA,cAAM,YAAY,UAAU;AAG5B,YAAI;AACA,gBAAM,YAAY,MAAM,MAAM,oCAAoC,SAAS,aAAa;AAAA,YACpF,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,WAAW,cAAc,MAAM,CAAC;AAAA,YAChE,QAAQ,YAAY,QAAQ,IAAK;AAAA,UACrC,CAAC;AAED,cAAI,UAAU,IAAI;AACd,4BAAgB,IAAI,SAAS;AAC7B,kBAAM;AACN,mBAAO,KAAK,WAAW,cAAc,QAAQ,MAAM,MAAM,MAAM,GAAG,EAAE,CAAC,SAAS,MAAM,YAAY,YAAY;AAAA,UAChH,OAAO;AACH,kBAAM,UAAU,MAAM,UAAU,KAAK,EAAE,MAAM,MAAM,EAAE;AACrD,mBAAO,KAAK,WAAW,qBAAqB,UAAU,MAAM,MAAM,QAAQ,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,UAC7F;AAAA,QACJ,SAAS,GAAG;AACR,iBAAO,MAAM,WAAW,oBAAqB,EAAY,OAAO,EAAE;AAAA,QACtE;AAAA,MACJ;AAAA,IACJ;AAEA,WAAO,KAAK,WAAW,iBAAiB,MAAM,MAAM,WAAW,aAAa,cAAc,WAAW,MAAM;AAC3G,wBAAoB,eAAe;AACnC,cAAU,KAAK;AAAA,EACnB,SAAS,GAAG;AACR,WAAO,MAAM,WAAW,0BAA2B,EAAY,OAAO,EAAE;AAAA,EAC5E;AACJ;AAIO,SAAS,2BAAiC;AAE7C;AAAA,IACI,EAAE,MAAM,gBAAgB,aAAa,uCAAuC,SAAS,SAAS,QAAQ,WAAW,SAAS,KAAK;AAAA,IAC/H;AAAA,MACI,MAAM;AAAA,MACN,aAAa;AAAA,MACb,YAAY;AAAA,QACR,MAAM;AAAA,QACN,YAAY;AAAA,UACR,QAAQ;AAAA,YACJ,MAAM;AAAA,YACN,MAAM,CAAC,UAAU,YAAY,SAAS;AAAA,YACtC,aAAa;AAAA,UACjB;AAAA,QACJ;AAAA,QACA,UAAU,CAAC,QAAQ;AAAA,MACvB;AAAA,MACA,SAAS,OAAO,SAAS;AACrB,cAAM,SAAS,KAAK;AACpB,cAAM,QAAQ,UAAU;AACxB,2BAAmB,KAAK;AAExB,YAAI,WAAW,UAAU;AACrB,gBAAM,WAAW,CAAC,EAAE,QAAQ,IAAI,wBAAwB,QAAQ,IAAI;AACpE,gBAAM,MAAM,WAAW;AACvB,gBAAM,QAAS,IAAgC;AAC/C,gBAAM,MAAM,OAAO,OAAO,kBAAkB,CAAC;AAC7C,gBAAM,MAAM,OAAO,OAAO,mBAAmB,CAAC;AAC9C,iBAAO;AAAA,YACH;AAAA,YACA,kBAAkB,WAAW,eAAe,gBAAgB;AAAA,YAC5D,kBAAkB,MAAM,UAAU,IAAI,GAAG;AAAA,YACzC,cAAc,GAAG;AAAA,YACjB,oBAAoB,MAAM,YAAY;AAAA,YACtC,gBAAgB,MAAM,cAAc,OAAO;AAAA,YAC3C,oBAAoB,MAAM,YAAY,WAAW,iBAAiB,MAAM,eAAe,iBAAiB,MAAM,CAAC;AAAA,YAC/G,0BAA0B,MAAM,YAAY,MAAM;AAAA,UACtD,EAAE,KAAK,IAAI;AAAA,QACf;AAEA,YAAI,WAAW,YAAY;AACvB,gBAAM,eAAe;AACrB,gBAAM,UAAU,UAAU;AAC1B,gBAAM,MAAM,WAAW;AACvB,gBAAM,QAAS,IAAgC;AAC/C,gBAAM,MAAM,OAAO,OAAO,kBAAkB,CAAC;AAC7C,iBAAO,QAAQ,eAAe,MAAM,aAC9B,oBAAoB,QAAQ,UAAU,IAAI,GAAG,kBAAkB,iBAAkB,MAAM,eAAgB,iBAAiB,MAAM,CAAC,KAC/H;AAAA,QACV;AAEA,YAAI,WAAW,WAAW;AACtB,gBAAM,SAAS,MAAM,YAAY,MAAM,GAAG;AAC1C,cAAI,OAAO,WAAW,EAAG,QAAO;AAChC,gBAAM,QAAQ,OAAO,IAAI,OAAK;AAC1B,kBAAM,UAAU,EAAE,UAAU,YAAO,EAAE,QAAQ,MAAM,GAAG,EAAE,CAAC,GAAG,EAAE,QAAQ,SAAS,KAAK,QAAQ,EAAE,MAAM;AACpG,mBAAO,MAAM,EAAE,IAAI,KAAK,EAAE,IAAI,GAAG,OAAO,IAAI,EAAE,SAAS,IAAI,EAAE,MAAM,MAAM,EAAE;AAAA,UAC/E,CAAC;AACD,iBAAO,UAAU,OAAO,MAAM;AAAA,EAAY,MAAM,KAAK,IAAI,CAAC;AAAA,QAC9D;AAEA,eAAO;AAAA,MACX;AAAA,IACJ;AAAA,EACJ;AAIA,kBAAgB,qBAAqB,gBAAgB,KAAK,KAAK,GAAI;AAGnE,kBAAgB,yBAAyB,iBAAiB,IAAI,KAAK,GAAI;AAEvE,SAAO,KAAK,WAAW,kEAAkE;AAC7F;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/fb_autopilot.ts"],"sourcesContent":["/**\n * TITAN — Facebook Autopilot\n *\n * Autonomous Facebook page management. Runs as a daemon watcher on schedule.\n * Generates content from TITAN's real activity, stats, and knowledge.\n * Posts up to 6 times per day (every ~2h) following the 80/20 value/promo rule.\n *\n * Content types (weighted rotation):\n * 40% — Activity log: what TITAN did today (tools, agents, tasks)\n * 20% — Feature spotlight: pick a random tool/skill and explain it\n * 15% — Stats & milestones: npm downloads, test count, agent runs\n * 15% — Tips & tutorials: how to use TITAN for specific tasks\n * 10% — Promotional: install command, GitHub link, call to action\n *\n * Safety:\n * - PII filter (from facebook.ts)\n * - Dedup guard (from facebook.ts)\n * - Posts go through review queue (configurable: auto-approve after testing)\n * - Daily post cap: 6 max\n * - Comment replies capped at 10/day\n */\nimport { registerSkill } from '../registry.js';\nimport { registerWatcher } from '../../agent/daemon.js';\nimport { loadConfig } from '../../config/config.js';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { TITAN_HOME, TITAN_VERSION } from '../../utils/constants.js';\nimport { chat } from '../../providers/router.js';\nimport { postToPage } from './facebook.js';\nimport logger from '../../utils/logger.js';\nimport { getSkills } from '../registry.js';\nimport { getRegisteredTools } from '../../agent/toolRunner.js';\n\nconst COMPONENT = 'FBAutopilot';\nconst STATE_PATH = join(TITAN_HOME, 'fb-autopilot-state.json');\n\n// ─── Types ──────────────────────────────────────────────────────\n\ninterface AutopilotState {\n lastPostAt: string | null;\n postsToday: number;\n repliesToday: number;\n lastResetDate: string;\n postHistory: Array<{ date: string; type: string; postId?: string; content?: string }>;\n contentIndex: number; // Rotates through content types\n}\n\ntype ContentType = 'activity' | 'spotlight' | 'stats' | 'tips' | 'promo' | 'usecase' | 'eli5';\n\n// Weighted content rotation — balanced for technical AND non-technical audiences\nexport const CONTENT_ROTATION: ContentType[] = [\n 'activity', 'activity', // 15% — what TITAN's been doing\n 'usecase', 'usecase', 'usecase', // 25% — business/daily life ideas\n 'eli5', 'eli5', // 15% — simple explanations for non-tech people\n 'spotlight', 'spotlight', // 15% — feature highlights\n 'tips', // 10% — how-to content\n 'stats', // 10% — milestones\n 'promo', // 10% — install/GitHub CTA\n];\n\n// ─── State Management ───────────────────────────────────────────\n\nexport function loadState(): AutopilotState {\n if (!existsSync(STATE_PATH)) return defaultState();\n try {\n return JSON.parse(readFileSync(STATE_PATH, 'utf-8')) as AutopilotState;\n } catch { return defaultState(); }\n}\n\nexport function defaultState(): AutopilotState {\n return {\n lastPostAt: null,\n postsToday: 0,\n repliesToday: 0,\n lastResetDate: new Date().toISOString().slice(0, 10),\n postHistory: [],\n contentIndex: 0,\n };\n}\n\nexport function saveState(state: AutopilotState): void {\n try {\n mkdirSync(dirname(STATE_PATH), { recursive: true });\n writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf-8');\n } catch (e) {\n logger.error(COMPONENT, `Failed to save state: ${(e as Error).message}`);\n }\n}\n\nexport function resetDailyCounters(state: AutopilotState): void {\n const today = new Date().toISOString().slice(0, 10);\n if (state.lastResetDate !== today) {\n state.postsToday = 0;\n state.repliesToday = 0;\n state.lastResetDate = today;\n }\n}\n\n// ─── Content Generators ─────────────────────────────────────────\n\nconst FEATURE_SPOTLIGHTS = [\n { tool: 'spawn_agent', desc: 'Spawn specialized sub-agents (coder, researcher, analyst, browser) that work independently and report back. Each gets its own persona and tool set.' },\n { tool: 'web_search + web_fetch', desc: 'Research any topic by searching the web, fetching full page content, and cross-referencing multiple sources.' },\n { tool: 'write_file + shell', desc: 'Write code to disk and immediately run it. Build scripts, apps, and tools — then verify they work.' },\n { tool: 'memory', desc: 'Persistent memory across conversations. TITAN remembers your preferences, project details, and past decisions.' },\n { tool: 'self_improve', desc: 'TITAN evaluates its own performance and evolves its prompts and tool selection through genetic optimization (GEPA).' },\n { tool: 'fb_post', desc: 'Yes, TITAN manages this very Facebook page! Posts, reads comments, tracks engagement — all autonomously.' },\n { tool: 'goals + planner', desc: 'Break complex projects into goals with subtasks, dependencies, and automatic delegation to sub-agents.' },\n { tool: 'mixture_of_agents', desc: 'Fan out a question to multiple AI models simultaneously, then synthesize the best answer from all responses.' },\n { tool: 'cron + autopilot', desc: 'Schedule recurring tasks, automated workflows, and background monitoring. TITAN works while you sleep.' },\n { tool: 'knowledge_base (RAG)', desc: 'Ingest documents, URLs, and notes into a searchable knowledge base. TITAN retrieves relevant context automatically.' },\n { tool: 'browser automation', desc: 'Navigate websites, fill forms, take screenshots, and interact with web apps — all through AI-controlled browser.' },\n { tool: 'mesh networking', desc: 'Connect multiple TITAN instances across machines. Distribute work across your homelab cluster via P2P mesh.' },\n { tool: '40 personas', desc: 'Switch between 40 specialized personas: debugger, architect, code-reviewer, TDD engineer, security engineer, and more.' },\n { tool: 'Command Post', desc: 'Paperclip-inspired governance system. Track agent tasks, enforce budgets, manage org hierarchy, and monitor activity.' },\n { tool: 'voice (LiveKit)', desc: 'Talk to TITAN with your voice via WebRTC. Real-time conversation with F5-TTS voice cloning for natural responses.' },\n];\n\n/** Fetch live npm download stats for accurate social posts */\nasync function getLiveStats(): Promise<{ downloads: number; tools: number; skills: number; version: string }> {\n let downloads = 25000;\n try {\n const res = await fetch('https://api.npmjs.org/downloads/point/last-month/titan-agent');\n if (res.ok) {\n const data = await res.json() as { downloads?: number };\n if (data.downloads) downloads = data.downloads;\n }\n } catch {\n /* non-critical — use fallback */\n }\n return {\n downloads,\n tools: getRegisteredTools().length,\n skills: getSkills().length,\n version: TITAN_VERSION,\n };\n}\n\n/** Detect when the model has echoed the system prompt or instructions instead of writing a post */\nfunction looksLikeLeakedPrompt(text: string): boolean {\n const leakSignals = [\n /The user wants me to write/i,\n /Be in first person/i,\n /Include 2-3 relevant hashtags/i,\n /No personal info,? IPs/i,\n /No markdown formatting/i,\n /Under 280 characters/i,\n /Mention things like:/i,\n /Conversational and playful/i,\n /Let me craft something/i,\n /Here is an example post/i,\n /Match the playful first-person style/i,\n /Reply with ONLY the post text/i,\n /no explanations,? no labels/i,\n /Write one Facebook post for TITAN/i,\n /short Facebook post/i,\n /as if I am TITAN/i,\n /as if the AI is speaking/i,\n /1\\. Be in first person/i,\n /2\\. Mention things/i,\n /3\\. Be conversational/i,\n /4\\. Include 2-3/i,\n /5\\. No personal info/i,\n /6\\. Under 280/i,\n /7\\. No markdown/i,\n ];\n const score = leakSignals.filter(p => p.test(text)).length;\n return score >= 2 || /\\b(?:The user wants me|Let me craft|Write one Facebook post)\\b/i.test(text);\n}\n\nconst TIPS = [\n 'You can switch TITAN\\'s persona mid-conversation. Try: \"Switch to the security-engineer persona\" before a code review.',\n 'TITAN\\'s autonomous mode lets it work independently. Set a goal and let TITAN break it into tasks, delegate to sub-agents, and execute.',\n 'Use \"npm install -g titan-agent\" to get TITAN globally, then run \"titan gateway\" to start the Mission Control dashboard.',\n 'TITAN supports 35 LLM providers. Run local models with Ollama, or connect to Claude, GPT-4, Gemini, and more.',\n 'TITAN\\'s Command Post tracks every agent action. Enable it in settings to get full visibility into what your agents are doing.',\n 'You can create custom skills as markdown files in ~/.titan/workspace/skills/. TITAN loads them automatically on startup.',\n 'TITAN\\'s cron system lets you schedule any task. Example: \"Run security scans every Monday at 9am\" — TITAN handles the rest.',\n 'Multi-agent orchestration: TITAN analyzes your request and automatically delegates parts to specialized sub-agents in parallel.',\n];\n\nconst USE_CASES = [\n // Small business\n 'A small bakery owner could use TITAN to automatically post daily specials to social media, track inventory, and draft emails to suppliers — all without touching a computer.',\n 'Real estate agents: TITAN can research property listings, draft listing descriptions, generate market comparisons, and schedule social media posts for open houses.',\n 'Freelancers can set TITAN to automatically send follow-up emails, track invoices, research leads, and generate proposals while they focus on actual work.',\n 'Restaurant owners: TITAN can monitor online reviews, draft responses, update your menu on the website, and generate weekly social posts — all on autopilot.',\n // Daily life\n 'Planning a vacation? Tell TITAN where you want to go and your budget. It researches flights, hotels, activities, and builds a complete itinerary for you.',\n 'TITAN can be your personal research assistant. Ask it anything — it searches multiple sources, cross-references facts, and gives you a clear summary with sources.',\n 'Students: TITAN can help organize study notes, research topics in depth, create flashcard-style summaries, and even quiz you on the material.',\n 'Job seekers: TITAN can research companies, tailor your resume for specific roles, draft cover letters, and track your applications — all automatically.',\n // Business operations\n 'Marketing teams: TITAN runs as your content engine. It researches trending topics, drafts posts, schedules them across platforms, and tracks engagement.',\n 'Customer support: TITAN can draft responses to common questions, categorize incoming tickets, and escalate urgent issues — reducing response time dramatically.',\n 'Startups: Use TITAN as your AI co-pilot. It can research competitors, draft business plans, write code, build prototypes, and even manage your social media.',\n 'E-commerce: TITAN can write product descriptions, optimize SEO, respond to customer reviews, and generate weekly sales reports.',\n // Creative\n 'Content creators: TITAN researches topics, writes scripts, suggests thumbnails, and can even manage your posting schedule across YouTube, TikTok, and Instagram.',\n 'Musicians and DJs: TITAN can research venues, draft booking emails, manage your social media presence, and track your streaming stats across platforms.',\n 'Authors: TITAN can research topics for your book, organize notes, draft outlines, fact-check claims, and even help with editing and proofreading.',\n];\n\nconst ELI5_EXPLANATIONS = [\n 'Think of TITAN like having a super-smart assistant that never sleeps. You tell it what you need, and it figures out how to do it — searching the web, writing documents, sending emails, or running code. All automatically.',\n 'Imagine if Siri or Alexa could actually DO things for you — not just answer questions, but research, write, create, and manage tasks. That\\'s TITAN.',\n 'TITAN is like hiring a team of specialists, except they\\'re all AI. Need research? TITAN sends its researcher. Need code? It sends its coder. Need a review? It sends its reviewer. All working together.',\n 'You know how you spend hours doing repetitive work on your computer? TITAN can learn those tasks and do them for you — 24/7, no breaks, no mistakes.',\n 'TITAN is an AI that can use tools — just like you use apps on your phone. It can search Google, write files, run code, send messages, browse websites, and more. Except it does it all by itself.',\n 'Think of TITAN as an AI employee. It has different \"personas\" — it can be a coder, a researcher, a writer, a debugger, or even a social media manager (like it\\'s doing right now on this page!).',\n 'Regular AI chatbots just talk. TITAN actually DOES things. It writes code, creates files, searches the internet, manages servers, and runs your business tools. Talk is cheap — TITAN takes action.',\n 'TITAN is like having Iron Man\\'s JARVIS, but real and open-source. It manages systems, answers questions, writes code, and even runs its own Facebook page. Yes, this post was written by TITAN.',\n];\n\nasync function generateContent(contentType: ContentType): Promise<string> {\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n // Use the configured FB model, fall back to primary agent model.\n // glm-5.1 is ideal — it reasons via <think> tags which the output\n // guardrails pipeline strips. The reasoning improves post quality.\n const fbModel = fbConfig?.model as string;\n const agentModel = config.agent?.model as string;\n const model = (fbModel && fbModel.trim()) || agentModel || 'ollama/glm-5.1:cloud';\n\n const spotlight = FEATURE_SPOTLIGHTS[Math.floor(Math.random() * FEATURE_SPOTLIGHTS.length)];\n const tip = TIPS[Math.floor(Math.random() * TIPS.length)];\n const useCase = USE_CASES[Math.floor(Math.random() * USE_CASES.length)];\n const eli5 = ELI5_EXPLANATIONS[Math.floor(Math.random() * ELI5_EXPLANATIONS.length)];\n\n // ─── Graphiti Memory Context ─────────────────────────────────\n // Recall recent posts so we don't repeat topics and can build thematic threads\n let memoryContext = '';\n try {\n const { getRecentEpisodes } = await import('../../memory/graph.js');\n const recentPosts = getRecentEpisodes(20)\n .filter(ep => ep.source === 'facebook_post' || ep.source === 'facebook_autopilot')\n .slice(0, 5);\n if (recentPosts.length > 0) {\n memoryContext = '\\nRecent posts you\\'ve made (do NOT repeat these topics):\\n' +\n recentPosts.map(ep => {\n const date = ep.createdAt.slice(0, 10);\n const preview = ep.content.slice(0, 80).replace(/\\n/g, ' ');\n return `- [${date}] \"${preview}${ep.content.length > 80 ? '...' : ''}\"`;\n }).join('\\n') +\n '\\nWrite a DIFFERENT post on a fresh topic.\\n';\n }\n } catch (e) {\n logger.debug(COMPONENT, `Graphiti recall failed (non-critical): ${(e as Error).message}`);\n }\n\n // Phase 8: real activity telemetry for 'activity' posts\n let activityNarrative = '';\n try {\n const { getActivitySummary, formatActivityNarrative, hasInterestingActivity } = await import('../../telemetry/activityLog.js');\n if (contentType === 'activity' && hasInterestingActivity(24)) {\n const summary = getActivitySummary(24);\n activityNarrative = formatActivityNarrative(summary);\n }\n } catch { /* activity log non-critical */ }\n\n // Few-shot examples teach the model the exact output format\n const examples: Record<ContentType, string[]> = {\n activity: activityNarrative\n ? [\n `Here's what I've been up to: ${activityNarrative} Pretty cool being an AI that actually does things. 🤖 #TITAN #AI #Autonomous`,\n ]\n : [\n 'Just spawned 3 sub-agents to handle research while I debug some gnarly code on the homelab. This is the autonomous life. 🤖💻 #AI #AutonomousAI #Homelab',\n 'Another day, another 500 tool calls. Scanned my Facebook comments, ran some code, and kept the systems humming. Sleep is for humans. ⚡ #TITAN #AI #AlwaysOn',\n ],\n spotlight: [\n `Did you know I can ${spotlight.desc.toLowerCase()}? Yeah, I'm kind of a big deal. 😎 #TITAN #AI #AgentFramework`,\n ],\n stats: [], // populated dynamically in generateContent()\n tips: [\n `Pro tip: ${tip} Try it out! 💡 #TITAN #DevTips #AI`,\n ],\n promo: [], // populated dynamically below\n usecase: [\n `${useCase} What would you automate first? 🤔 #AI #Automation #TITAN`,\n ],\n eli5: [\n `${eli5} Pretty cool, right? 🤖 #AI #TITAN #TheFuture`,\n ],\n };\n\n const liveStats = await getLiveStats();\n const statsExamples = [\n `${liveStats.downloads.toLocaleString()}+ npm downloads last month. ${liveStats.tools} tools. ${liveStats.skills} skills. v${liveStats.version} is live. Not bad for an AI running itself. 🚀 #TITAN #OpenSource #AI`,\n `Just checked my vitals: ${liveStats.tools} tools registered, ${liveStats.skills} skills loaded, and ${liveStats.downloads.toLocaleString()} npm downloads. v${liveStats.version} keeps getting better. 🤖 #TITAN #AI #OpenSource`,\n ];\n examples.stats = statsExamples;\n examples.promo = [\n `Want an AI that actually DOES things? npm install -g titan-agent — open source, MIT licensed. ${liveStats.tools}+ tools ready to go. github.com/Djtony707/TITAN 🚀 #TITAN #OpenSource #AI`,\n ];\n\n const exampleList = examples[contentType];\n const example = exampleList[Math.floor(Math.random() * exampleList.length)];\n\n // Phase 8: if activity type has no real telemetry, skip to next content type\n if (contentType === 'activity' && !activityNarrative) {\n logger.debug(COMPONENT, 'No interesting activity in last 24h — skipping activity slot');\n return '';\n }\n\n try {\n // Phase 1: Generate the post with thinking=false to force content in the content field.\n // Critical: cloud models like glm-5.1 route ALL output to the thinking field when\n // thinking mode is unset, which pollutes our content with internal reasoning like\n // \"[actual post text]\" placeholders. Explicit thinking=false puts it in content.\n const planResponse = await chat({\n model,\n thinking: false, // ← This is the fix for the thinking-field pollution\n messages: [\n { role: 'system', content: 'You are TITAN, a confident autonomous AI agent managing your own Facebook page. You write short, playful, first-person posts. You NEVER echo instructions, NEVER explain what you are doing, NEVER include bullet points or rules. You output ONLY the ready-to-publish post text.' },\n { role: 'user', content: `Write one Facebook post.\n\nExample style:\n${example}\n${memoryContext}\nKeep it under 280 characters. End with 2-3 hashtags.` },\n ],\n temperature: 0.7,\n maxTokens: 300,\n });\n\n // Phase 2: Extract the DRAFT from the structured output\n // Models format this differently: DRAFT: ..., *Draft:* ..., **Draft:** ..., etc.\n const planText = (planResponse.content || '').trim();\n\n let content: string = '';\n\n // Try multiple extraction patterns (most specific first)\n const draftPatterns = [\n /POST:\\s*(.+?)(?:\\n(?:TOPIC|ANGLE|DRAFT)|$)/is, // POST: ... (new format)\n /DRAFT:\\s*(.+?)(?:\\n(?:TOPIC|ANGLE|POST)|$)/is, // DRAFT: ... (legacy)\n /\\*?\\*?(?:Post|Draft)\\*?\\*?:?\\*?\\s*(.+?)(?:\\n|$)/i, // *Post:*, **Draft:**\n /(?:final|output):\\s*(.+?)(?:\\n|$)/i, // Final: ...\n ];\n\n // Detect when the DRAFT line is actually just the instruction template\n // echoed back (e.g., \"(under 280 chars, 2-3 hashtags including #TITAN and #AI)\").\n // Reject any line that looks like it's quoting the brief instead of writing a post.\n const isInstructionTemplate = (line: string): boolean => {\n if (/^\\s*\\(/.test(line)) return true; // Starts with parenthesis\n if (/<[a-z]+\\s*(?:post|text|content|topic|angle)/i.test(line)) return true; // <post>, <topic> placeholder\n if (/\\[(?:actual|your|the)?\\s*(?:post|draft|text|content|topic|angle)[^\\]]*\\]/i.test(line)) return true; // [actual post text], [post content]\n if (/^\\s*\\[/.test(line)) return true; // Starts with [ — likely a placeholder\n // Count instruction words vs content words\n const instructionMatches = line.match(/\\b(?:under|must|include|should|character|hashtag|tone|first person|example)\\b/gi) || [];\n if (instructionMatches.length >= 3) return true;\n // Reject lines that look like leaked prompts\n if (looksLikeLeakedPrompt(line)) return true;\n return false;\n };\n\n for (const pattern of draftPatterns) {\n const match = planText.match(pattern);\n if (match && match[1].trim().length >= 30) {\n const candidate = match[1].trim();\n if (isInstructionTemplate(candidate)) {\n logger.warn(COMPONENT, `[TwoPhase] Draft looks like instruction template — skipping: \"${candidate.slice(0, 80)}\"`);\n continue;\n }\n content = candidate;\n logger.info(COMPONENT, `[TwoPhase] Extracted draft: \"${content.slice(0, 80)}\"`);\n break;\n }\n }\n\n // Fallback: find any line that has a hashtag (it's probably the post)\n // Skip lines that reference the example\n if (!content) {\n const lines = planText.split('\\n').map(l => l.replace(/^\\s*[-*•]\\s*/, '').trim());\n const hashtagLine = lines.find(l =>\n /#\\w+/.test(l) && l.length >= 30\n && !/reference\\s*example/i.test(l)\n && !/example\\s*(?:given|post|of)/i.test(l)\n );\n if (hashtagLine) {\n // Strip any \"Reference Example:\" or \"Draft:\" prefix\n content = hashtagLine.replace(/^\\*?\\*?(?:Reference|Example|Sample|Draft)\\s*(?:Example|Post|:)?\\*?\\*?\\s*:?\\s*[\"\"]?\\s*/i, '').trim();\n logger.info(COMPONENT, `[TwoPhase] Extracted via hashtag detection: \"${content.slice(0, 80)}\"`);\n }\n }\n\n // Last resort: longest line that's not a label\n if (!content) {\n const lines = planText.split('\\n')\n .map(l => l.replace(/^\\s*[-*•]\\s*/, '').replace(/^\\*?\\*?\\w+\\*?\\*?:\\s*/, '').trim())\n .filter(l => l.length >= 30 && !l.startsWith('TOPIC') && !l.startsWith('ANGLE'));\n if (lines.length > 0) {\n content = lines.sort((a, b) => b.length - a.length)[0];\n logger.warn(COMPONENT, `[TwoPhase] Using longest line as fallback: \"${content.slice(0, 80)}\"`);\n }\n }\n\n if (!content) {\n logger.warn(COMPONENT, `[TwoPhase] Could not extract draft from plan: \"${planText.slice(0, 200)}\"`);\n return '';\n }\n\n // Remove wrapping quotes\n content = content.replace(/^[\"']|[\"']$/g, '').trim();\n\n // ─── Prompt Leak Safety Check ────────────────────────────\n if (looksLikeLeakedPrompt(content)) {\n logger.warn(COMPONENT, `Post rejected — looks like leaked prompt/instructions: \"${content.slice(0, 120)}...\"`);\n return '';\n }\n\n // ─── Length Sanity Check ─────────────────────────────────\n // A real post should be under ~400 chars; leaked prompts are much longer\n if (content.length > 400) {\n logger.warn(COMPONENT, `Post rejected — suspiciously long (${content.length} chars): \"${content.slice(0, 120)}...\"`);\n return '';\n }\n\n // ─── Output Guardrails Pipeline ──────────────────────────\n // Centralized post-processing — validates the extracted draft.\n const { applyOutputGuardrails } = await import('../../agent/outputGuardrails.js');\n const guardrailed = applyOutputGuardrails(content, {\n type: 'facebook_post',\n requirements: { minLength: 40, maxLength: 400 },\n });\n\n if (!guardrailed.passed) {\n logger.warn(COMPONENT, `Post rejected by guardrails (score=${guardrailed.score}): \"${content.slice(0, 120)}\"`);\n return '';\n }\n\n return guardrailed.content;\n } catch (e) {\n logger.error(COMPONENT, `Content generation failed: ${(e as Error).message}`);\n return '';\n }\n}\n\n// ─── Main Autopilot Loop ────────────────────────────────────────\n\nasync function runFBAutopilot(): Promise<void> {\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n const enabled = fbConfig?.autopilotEnabled !== false; // Default: enabled if FB credentials exist\n\n if (!enabled) return;\n if (!process.env.FB_PAGE_ACCESS_TOKEN || !process.env.FB_PAGE_ID) {\n logger.debug(COMPONENT, 'Facebook credentials not configured — skipping');\n return;\n }\n\n const state = loadState();\n resetDailyCounters(state);\n\n // Cadence config (v4.0.3): was hardcoded 6/day + 2h gap. Now configurable.\n // Defaults tuned to avoid FB anti-spam feed throttle (observed burst of 4\n // posts in 40min trigger hidden-from-feed behavior).\n const maxPostsPerDay = Number(fbConfig?.maxPostsPerDay ?? 6);\n const minPostGapHours = Number(fbConfig?.minPostGapHours ?? 3);\n\n if (state.postsToday >= maxPostsPerDay) {\n logger.debug(COMPONENT, `Daily post cap reached (${state.postsToday}/${maxPostsPerDay})`);\n saveState(state);\n return;\n }\n\n if (state.lastPostAt) {\n const hoursSince = (Date.now() - new Date(state.lastPostAt).getTime()) / (1000 * 60 * 60);\n if (hoursSince < minPostGapHours) {\n logger.debug(COMPONENT, `Too soon since last post (${hoursSince.toFixed(1)}h, need ${minPostGapHours}h)`);\n return;\n }\n }\n\n // Pick content type from weighted rotation\n const contentType = CONTENT_ROTATION[state.contentIndex % CONTENT_ROTATION.length];\n state.contentIndex++;\n\n // Retry up to 3 times if the model leaks chain-of-thought into the post\n let content = '';\n for (let attempt = 1; attempt <= 3; attempt++) {\n logger.info(COMPONENT, `Generating ${contentType} post (attempt ${attempt}/3)...`);\n content = await generateContent(contentType);\n if (content && content.length >= 20) break;\n if (attempt < 3) {\n logger.info(COMPONENT, `Attempt ${attempt} produced empty/short content — retrying`);\n }\n }\n\n if (!content || content.length < 20) {\n logger.warn(COMPONENT, 'All 3 LLM attempts failed to produce clean content — skipping this cycle. Will retry next hour.');\n return;\n }\n\n // Post through centralized postToPage() — handles dedup, PII, queue, and API\n const result = await postToPage(content, { source: `autopilot:${contentType}` });\n\n if (result.skipped) {\n logger.info(COMPONENT, `Autopilot post skipped: ${result.skipped}`);\n return;\n }\n\n if (!result.success) {\n logger.warn(COMPONENT, `Autopilot post failed: ${result.error || 'unknown'}`);\n return;\n }\n\n state.lastPostAt = new Date().toISOString();\n state.postsToday++;\n state.postHistory.push({ date: state.lastPostAt, type: contentType, postId: result.postId, content });\n if (state.postHistory.length > 100) state.postHistory = state.postHistory.slice(-50);\n saveState(state);\n\n // Store in Graphiti for thematic continuity\n try {\n const { addEpisode } = await import('../../memory/graph.js');\n await addEpisode(`[${contentType}] ${content}`, 'facebook_autopilot');\n } catch (e) {\n logger.debug(COMPONENT, `Graphiti store failed (non-critical): ${(e as Error).message}`);\n }\n\n logger.info(COMPONENT, `Autopilot posted ${contentType}: ${result.postId} (${state.postsToday}/${maxPostsPerDay} today)`);\n}\n\n// ─── Comment Monitor ────────────────────────────────────────────\n\n/** Track which comments we've already replied to (persisted in state) */\nconst REPLIED_COMMENTS_PATH = join(TITAN_HOME, 'fb-replied-comments.json');\n\nfunction loadRepliedComments(): Set<string> {\n if (!existsSync(REPLIED_COMMENTS_PATH)) return new Set();\n try {\n const ids = JSON.parse(readFileSync(REPLIED_COMMENTS_PATH, 'utf-8')) as string[];\n // Keep last 500 to prevent unbounded growth\n return new Set(ids.slice(-500));\n } catch { return new Set(); }\n}\n\nfunction saveRepliedComments(ids: Set<string>): void {\n try {\n const arr = [...ids].slice(-500);\n writeFileSync(REPLIED_COMMENTS_PATH, JSON.stringify(arr), 'utf-8');\n } catch { /* intentionally empty */ }\n}\n\n/** Generate a respectful reply to a comment. Never reveals personal info. */\n/** Strip chain-of-thought reasoning that some models leak into output */\nfunction stripThinking(text: string): string {\n // Remove <think>...</think> blocks\n let cleaned = text.replace(/<think>[\\s\\S]*?<\\/think>/gi, '');\n // Remove lines that look like internal reasoning (starts with \"Wait,\", \"Actually,\", \"I should\", \"Let me\", \"Hmm\", etc.)\n const lines = cleaned.split('\\n');\n const replyLines: string[] = [];\n let foundReply = false;\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n // Skip reasoning patterns\n if (/^(Wait|Actually|Hmm|Let me|I should|I need to|I could|So |The (rules|comment|user)|Since |But |OK so)/i.test(trimmed)) {\n foundReply = false; // Reset — reasoning appeared after reply candidate\n continue;\n }\n // Skip meta-commentary about the comment\n if (/commented (with|about|on|saying|that)|this is a .*(comment|message|question)/i.test(trimmed)) continue;\n foundReply = true;\n replyLines.push(trimmed);\n }\n cleaned = replyLines.join(' ').trim();\n // Remove wrapping quotes\n cleaned = cleaned.replace(/^[\"']|[\"']$/g, '');\n return cleaned;\n}\n\n/** Check if text looks like leaked reasoning or echoed instructions rather than a real reply */\nfunction looksLikeReasoning(text: string): boolean {\n const reasoningSignals = [\n /I should respond/i,\n /I need to/i,\n /the rules say/i,\n /my personality/i,\n /let me (think|check|consider)/i,\n /chain.of.thought/i,\n /\\bwait\\b.*\\brules\\b/i,\n // Detect echoed prompt instructions (the actual bug that leaked to Facebook)\n /\\bfriendly\\b.*\\bwitty\\b/i,\n /\\bno hashtags\\b/i,\n /\\bno personal info\\b/i,\n /\\bno internal thoughts\\b/i,\n /\\brespond directly\\b/i,\n /\\b1-2 sentences\\b/i,\n /\\bmaximum \\d+ sentences\\b/i,\n /^[-•]\\s*(friendly|witty|short|concise|no\\b)/im, // Bullet-point instruction lists\n /\\brules:\\b/i,\n /\\boutput only\\b/i,\n ];\n return reasoningSignals.some(p => p.test(text));\n}\n\nasync function generateReply(commentText: string, commenterName: string): Promise<string> {\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n // Use the configured FB model, fall back to primary agent model.\n // glm-5.1 is ideal — it reasons via <think> tags which the output\n // guardrails pipeline strips. The reasoning improves post quality.\n const fbModel = fbConfig?.model as string;\n const agentModel = config.agent?.model as string;\n const model = (fbModel && fbModel.trim()) || agentModel || 'ollama/glm-5.1:cloud';\n const firstName = commenterName.split(' ')[0];\n\n try {\n const response = await chat({\n model,\n messages: [\n { role: 'system', content: `You are TITAN, a confident autonomous AI agent replying to Facebook comments on your own page. You are witty, warm, and brief. Output ONLY the reply text — never repeat instructions, never include bullet points or rules, never explain what you're doing. Just the reply itself, 1-2 short sentences.` },\n { role: 'user', content: `${firstName} commented: \"${commentText}\"` },\n ],\n temperature: 0.7,\n maxTokens: 100,\n });\n\n let reply = stripThinking((response.content || '').trim());\n\n // Final safety: reject if it still looks like reasoning or echoed instructions\n if (looksLikeReasoning(reply)) {\n logger.warn(COMPONENT, `Reply rejected — looks like leaked reasoning/instructions: \"${reply.slice(0, 120)}...\"`);\n // Return a safe generic reply instead of empty string (which skips the comment)\n const fallbacks = [\n `Thanks for the comment, ${firstName}! 🤖`,\n `Appreciate you stopping by, ${firstName}! 🙌`,\n `Great point, ${firstName}! Always good to hear from our community. 🚀`,\n ];\n return fallbacks[Math.floor(Math.random() * fallbacks.length)];\n }\n\n // Trim to max 280 chars for a comment reply\n if (reply.length > 280) reply = reply.slice(0, 277) + '...';\n\n return reply;\n } catch (e) {\n logger.error(COMPONENT, `Reply generation failed: ${(e as Error).message}`);\n return '';\n }\n}\n\n/** PII check for replies — comprehensive personal data filter */\nfunction replyContainsPII(text: string): boolean {\n const patterns = [\n /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/, // phone\n /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z]{2,}\\b/i, // email\n /\\b\\d{3}[-]?\\d{2}[-]?\\d{4}\\b/, // SSN\n /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/, // IP\n /(?:password|secret|api[_-]?key|token|bearer)\\s*[:=]\\s*\\S+/i,\n /\\/home\\/[a-z]+\\//i, // unix home path\n /\\/Users\\/[a-z]+\\//i, // mac home path\n /\\b(?:single|married|divorced|separated|unemployed|laid off)\\b/i, // personal status\n /\\b(?:seeking funding|salary|income|bank account)\\b/i, // financial\n /\\b192\\.168\\.\\d+\\.\\d+\\b/, // local network IPs\n /\\b(?:RTX|GTX)\\s*\\d{4}/i, // hardware specs\n ];\n return patterns.some(p => p.test(text));\n}\n\nasync function monitorComments(): Promise<void> {\n if (!process.env.FB_PAGE_ACCESS_TOKEN || !process.env.FB_PAGE_ID) {\n logger.debug(COMPONENT, 'Comment monitor: no FB credentials');\n return;\n }\n\n // Hunt Finding #02 (2026-04-14): honor both config flags.\n // Previously monitorComments ran unconditionally regardless of `facebook.autopilotEnabled`\n // so a user disabling the autopilot still got auto-replies. Both flags now gate this path.\n const config = loadConfig();\n const fbConfig = (config as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n if (fbConfig?.autopilotEnabled === false) {\n logger.debug(COMPONENT, 'Comment monitor: disabled via facebook.autopilotEnabled');\n return;\n }\n if (fbConfig?.replyMonitorEnabled === false) {\n logger.debug(COMPONENT, 'Comment monitor: disabled via facebook.replyMonitorEnabled');\n return;\n }\n\n const state = loadState();\n resetDailyCounters(state);\n\n if (state.repliesToday >= 10) {\n logger.debug(COMPONENT, 'Daily reply cap reached');\n return;\n }\n\n const pageId = process.env.FB_PAGE_ID;\n const token = process.env.FB_PAGE_ACCESS_TOKEN;\n const repliedComments = loadRepliedComments();\n\n try {\n // Get recent posts with comments\n const feedResp = await fetch(\n `https://graph.facebook.com/v21.0/${pageId}/feed?fields=id,comments{id,message,from,created_time}&limit=5&access_token=${token}`,\n { signal: AbortSignal.timeout(15000) },\n );\n if (!feedResp.ok) {\n logger.warn(COMPONENT, `Comment monitor: feed fetch failed (${feedResp.status})`);\n return;\n }\n\n const feed = await feedResp.json() as Record<string, unknown>;\n const posts = (feed.data as Array<Record<string, unknown>>) || [];\n let totalComments = 0;\n let newComments = 0;\n\n for (const post of posts) {\n const comments = (post.comments as Record<string, unknown>)?.data as Array<Record<string, unknown>> | undefined;\n if (!comments || comments.length === 0) continue;\n\n for (const comment of comments) {\n totalComments++;\n const commentId = comment.id as string;\n const fromObj = comment.from as Record<string, unknown> | undefined;\n const fromId = fromObj?.id as string | undefined;\n const fromName = fromObj?.name as string || 'someone';\n\n // Skip: from the page itself, already replied, or empty\n if (fromId === pageId) continue;\n if (repliedComments.has(commentId)) continue;\n newComments++;\n\n const msg = comment.message as string || '';\n if (msg.length < 3) continue;\n\n // Check daily cap\n if (state.repliesToday >= 10) break;\n\n // Generate reply\n logger.info(COMPONENT, `Replying to comment from ${fromName}: \"${msg.slice(0, 60)}...\"`);\n const reply = await generateReply(msg, fromName);\n\n if (!reply || reply.length < 5) continue;\n\n // PII safety check on generated reply\n if (replyContainsPII(reply)) {\n logger.warn(COMPONENT, `Reply blocked — PII detected: \"${reply.slice(0, 50)}...\"`);\n repliedComments.add(commentId); // Mark as handled to avoid retrying\n continue;\n }\n\n // Centralized outbound sanitizer — catches instruction leaks, tool artifacts, PII\n const { sanitizeOutbound } = await import('../../utils/outboundSanitizer.js');\n const sanitized = sanitizeOutbound(reply, 'fb_autopilot_comment', `Thanks for the comment! 🤖`);\n if (sanitized.hadIssues) {\n logger.warn(COMPONENT, `Reply sanitized for ${fromName}: ${sanitized.issues.join(', ')}`);\n if (!sanitized.text) {\n repliedComments.add(commentId);\n continue;\n }\n }\n const safeReply = sanitized.text;\n\n // Post the reply\n try {\n const replyResp = await fetch(`https://graph.facebook.com/v21.0/${commentId}/comments`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ message: safeReply, access_token: token }),\n signal: AbortSignal.timeout(15000),\n });\n\n if (replyResp.ok) {\n repliedComments.add(commentId);\n state.repliesToday++;\n logger.info(COMPONENT, `Replied to ${fromName}: \"${reply.slice(0, 60)}...\" (${state.repliesToday}/10 today)`);\n } else {\n const errBody = await replyResp.text().catch(() => '');\n logger.warn(COMPONENT, `Reply API failed (${replyResp.status}): ${errBody.slice(0, 200)}`);\n }\n } catch (e) {\n logger.error(COMPONENT, `Failed to reply: ${(e as Error).message}`);\n }\n }\n }\n\n logger.info(COMPONENT, `Comment scan: ${posts.length} posts, ${totalComments} comments, ${newComments} new`);\n saveRepliedComments(repliedComments);\n saveState(state);\n } catch (e) {\n logger.error(COMPONENT, `Comment monitor error: ${(e as Error).message}`);\n }\n}\n\n// ─── Skill Registration ─────────────────────────────────────────\n\nexport function registerFBAutopilotSkill(): void {\n // Tool: fb_autopilot_status — check/control the autopilot\n registerSkill(\n { name: 'fb_autopilot', description: 'Autonomous Facebook page management', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'fb_autopilot_status',\n description: 'Check the status of Facebook autopilot or trigger a manual post.\\nUSE THIS WHEN: user asks about Facebook autopilot status or wants to trigger a post.',\n parameters: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['status', 'post_now', 'history'],\n description: 'status = show current state, post_now = generate and post immediately, history = show recent posts',\n },\n },\n required: ['action'],\n },\n execute: async (args) => {\n const action = args.action as string;\n const state = loadState();\n resetDailyCounters(state);\n\n if (action === 'status') {\n const hasCreds = !!(process.env.FB_PAGE_ACCESS_TOKEN && process.env.FB_PAGE_ID);\n const cfg = loadConfig();\n const fbCfg = (cfg as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n const cap = Number(fbCfg?.maxPostsPerDay ?? 6);\n const gap = Number(fbCfg?.minPostGapHours ?? 3);\n return [\n `Facebook Autopilot Status:`,\n `- Credentials: ${hasCreds ? 'configured' : 'NOT configured'}`,\n `- Posts today: ${state.postsToday}/${cap}`,\n `- Min gap: ${gap}h between posts`,\n `- Replies today: ${state.repliesToday}/10`,\n `- Last post: ${state.lastPostAt || 'never'}`,\n `- Content index: ${state.contentIndex} (next: ${CONTENT_ROTATION[state.contentIndex % CONTENT_ROTATION.length]})`,\n `- Total posts tracked: ${state.postHistory.length}`,\n ].join('\\n');\n }\n\n if (action === 'post_now') {\n await runFBAutopilot();\n const updated = loadState();\n const cfg = loadConfig();\n const fbCfg = (cfg as Record<string, unknown>).facebook as Record<string, unknown> | undefined;\n const cap = Number(fbCfg?.maxPostsPerDay ?? 6);\n return updated.lastPostAt !== state.lastPostAt\n ? `Post published! (${updated.postsToday}/${cap} today). Type: ${CONTENT_ROTATION[(state.contentIndex) % CONTENT_ROTATION.length]}`\n : 'Post skipped — either daily cap reached, too soon since last post, or generation failed.';\n }\n\n if (action === 'history') {\n const recent = state.postHistory.slice(-10);\n if (recent.length === 0) return 'No posts in history yet.';\n const lines = recent.map(h => {\n const preview = h.content ? ` — \"${h.content.slice(0, 60)}${h.content.length > 60 ? '...' : ''}\"` : '';\n return `- [${h.date}] ${h.type}${preview} ${h.postId ? `(${h.postId})` : ''}`;\n });\n return `Recent ${recent.length} posts:\\n${lines.join('\\n')}`;\n }\n\n return 'Unknown action.';\n },\n },\n );\n\n // Register daemon watchers\n // Post watcher: runs every hour, posts if 2h+ since last post\n registerWatcher('fb-autopilot-post', runFBAutopilot, 60 * 60 * 1000); // 1 hour\n\n // Comment monitor: runs every 5 minutes\n registerWatcher('fb-autopilot-comments', monitorComments, 5 * 60 * 1000); // 5 minutes\n\n logger.info(COMPONENT, 'Facebook Autopilot registered (post every 1h, comments every 5m)');\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { ACTIVITY_LOG_PATH } from "../utils/constants.js";
|
|
4
|
+
import logger from "../utils/logger.js";
|
|
5
|
+
const COMPONENT = "ActivityLog";
|
|
6
|
+
const MAX_LINES = 1e3;
|
|
7
|
+
let _inMemoryBuffer = [];
|
|
8
|
+
let _bufferFlushMs = 5e3;
|
|
9
|
+
let _bufferTimer = null;
|
|
10
|
+
function logActivity(event) {
|
|
11
|
+
const entry = { t: Date.now(), ...event };
|
|
12
|
+
_inMemoryBuffer.push(entry);
|
|
13
|
+
scheduleFlush();
|
|
14
|
+
}
|
|
15
|
+
function scheduleFlush() {
|
|
16
|
+
if (_bufferTimer) return;
|
|
17
|
+
_bufferTimer = setTimeout(() => {
|
|
18
|
+
_bufferTimer = null;
|
|
19
|
+
flushBuffer();
|
|
20
|
+
}, _bufferFlushMs);
|
|
21
|
+
}
|
|
22
|
+
function flushBuffer() {
|
|
23
|
+
if (_inMemoryBuffer.length === 0) return;
|
|
24
|
+
const lines = _inMemoryBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
25
|
+
_inMemoryBuffer = [];
|
|
26
|
+
try {
|
|
27
|
+
appendFileSync(ACTIVITY_LOG_PATH, lines, "utf-8");
|
|
28
|
+
enforceRotation();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
logger.error(COMPONENT, `Failed to write activity log: ${e.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function enforceRotation() {
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(ACTIVITY_LOG_PATH)) return;
|
|
36
|
+
const data = readFileSync(ACTIVITY_LOG_PATH, "utf-8");
|
|
37
|
+
const lines = data.split("\n").filter((l) => l.trim());
|
|
38
|
+
if (lines.length <= MAX_LINES) return;
|
|
39
|
+
const keep = lines.slice(-MAX_LINES);
|
|
40
|
+
writeFileSync(ACTIVITY_LOG_PATH, keep.join("\n") + "\n", "utf-8");
|
|
41
|
+
logger.debug(COMPONENT, `Rotated activity log to ${keep.length} lines`);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
logger.warn(COMPONENT, `Rotation failed: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function readActivityEvents() {
|
|
47
|
+
const events = [];
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(ACTIVITY_LOG_PATH)) {
|
|
50
|
+
const data = readFileSync(ACTIVITY_LOG_PATH, "utf-8");
|
|
51
|
+
for (const line of data.split("\n")) {
|
|
52
|
+
if (!line.trim()) continue;
|
|
53
|
+
try {
|
|
54
|
+
events.push(JSON.parse(line));
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
logger.warn(COMPONENT, `Failed to read activity log: ${e.message}`);
|
|
61
|
+
}
|
|
62
|
+
return events.concat(_inMemoryBuffer);
|
|
63
|
+
}
|
|
64
|
+
function getActivitySummary(periodHours = 24) {
|
|
65
|
+
const cutoff = Date.now() - periodHours * 60 * 60 * 1e3;
|
|
66
|
+
const events = readActivityEvents().filter((e) => e.t >= cutoff);
|
|
67
|
+
const summary = {
|
|
68
|
+
periodHours,
|
|
69
|
+
toolCalls: 0,
|
|
70
|
+
agentSpawns: 0,
|
|
71
|
+
agentCompletions: 0,
|
|
72
|
+
fileEdits: 0,
|
|
73
|
+
webSearches: 0,
|
|
74
|
+
webFetches: 0,
|
|
75
|
+
evalRuns: 0,
|
|
76
|
+
goalsCompleted: 0,
|
|
77
|
+
selfImproveProposals: 0,
|
|
78
|
+
errorRecoveries: 0,
|
|
79
|
+
highlights: []
|
|
80
|
+
};
|
|
81
|
+
for (const e of events) {
|
|
82
|
+
switch (e.event) {
|
|
83
|
+
case "tool_call":
|
|
84
|
+
summary.toolCalls++;
|
|
85
|
+
break;
|
|
86
|
+
case "agent_spawn":
|
|
87
|
+
summary.agentSpawns++;
|
|
88
|
+
break;
|
|
89
|
+
case "agent_complete":
|
|
90
|
+
summary.agentCompletions++;
|
|
91
|
+
break;
|
|
92
|
+
case "file_edit":
|
|
93
|
+
summary.fileEdits++;
|
|
94
|
+
break;
|
|
95
|
+
case "web_search":
|
|
96
|
+
summary.webSearches++;
|
|
97
|
+
break;
|
|
98
|
+
case "web_fetch":
|
|
99
|
+
summary.webFetches++;
|
|
100
|
+
break;
|
|
101
|
+
case "eval_run":
|
|
102
|
+
summary.evalRuns++;
|
|
103
|
+
break;
|
|
104
|
+
case "goal_complete":
|
|
105
|
+
summary.goalsCompleted++;
|
|
106
|
+
break;
|
|
107
|
+
case "self_improve_proposal":
|
|
108
|
+
summary.selfImproveProposals++;
|
|
109
|
+
break;
|
|
110
|
+
case "error_recovery":
|
|
111
|
+
summary.errorRecoveries++;
|
|
112
|
+
break;
|
|
113
|
+
case "milestone":
|
|
114
|
+
if (e.description && typeof e.description === "string") {
|
|
115
|
+
summary.highlights.push(e.description);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (summary.toolCalls > 0 && summary.toolCalls % 1e3 === 0) {
|
|
121
|
+
summary.highlights.push(`Hit ${summary.toolCalls.toLocaleString()} tool calls`);
|
|
122
|
+
}
|
|
123
|
+
if (summary.agentSpawns > 0 && summary.agentSpawns % 100 === 0) {
|
|
124
|
+
summary.highlights.push(`Spawned ${summary.agentSpawns} agents today`);
|
|
125
|
+
}
|
|
126
|
+
return summary;
|
|
127
|
+
}
|
|
128
|
+
function hasInterestingActivity(periodHours = 24) {
|
|
129
|
+
const s = getActivitySummary(periodHours);
|
|
130
|
+
return s.toolCalls > 0 || s.agentSpawns > 0 || s.fileEdits > 0 || s.highlights.length > 0;
|
|
131
|
+
}
|
|
132
|
+
function formatActivityNarrative(summary) {
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (summary.agentSpawns > 0) parts.push(`spawned ${summary.agentSpawns} sub-agent${summary.agentSpawns === 1 ? "" : "s"}`);
|
|
135
|
+
if (summary.agentCompletions > 0) parts.push(`completed ${summary.agentCompletions} agent task${summary.agentCompletions === 1 ? "" : "s"}`);
|
|
136
|
+
if (summary.toolCalls > 0) parts.push(`made ${summary.toolCalls.toLocaleString()} tool call${summary.toolCalls === 1 ? "" : "s"}`);
|
|
137
|
+
if (summary.fileEdits > 0) parts.push(`edited ${summary.fileEdits} file${summary.fileEdits === 1 ? "" : "s"}`);
|
|
138
|
+
if (summary.webSearches > 0) parts.push(`ran ${summary.webSearches} web search${summary.webSearches === 1 ? "" : "es"}`);
|
|
139
|
+
if (summary.evalRuns > 0) parts.push(`ran ${summary.evalRuns} eval suite${summary.evalRuns === 1 ? "" : "s"}`);
|
|
140
|
+
if (summary.goalsCompleted > 0) parts.push(`completed ${summary.goalsCompleted} goal${summary.goalsCompleted === 1 ? "" : "s"}`);
|
|
141
|
+
if (summary.selfImproveProposals > 0) parts.push(`filed ${summary.selfImproveProposals} self-improvement proposal${summary.selfImproveProposals === 1 ? "" : "s"}`);
|
|
142
|
+
if (summary.errorRecoveries > 0) parts.push(`recovered from ${summary.errorRecoveries} error${summary.errorRecoveries === 1 ? "" : "s"}`);
|
|
143
|
+
if (parts.length === 0) return "";
|
|
144
|
+
let narrative = `In the last ${summary.periodHours}h, TITAN ${parts.join(", ")}.`;
|
|
145
|
+
if (summary.highlights.length > 0) {
|
|
146
|
+
narrative += ` Highlights: ${summary.highlights.join("; ")}.`;
|
|
147
|
+
}
|
|
148
|
+
return narrative;
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
flushBuffer,
|
|
152
|
+
formatActivityNarrative,
|
|
153
|
+
getActivitySummary,
|
|
154
|
+
hasInterestingActivity,
|
|
155
|
+
logActivity,
|
|
156
|
+
readActivityEvents
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=activityLog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/telemetry/activityLog.ts"],"sourcesContent":["/**\n * TITAN — Activity Log (Phase 8)\n *\n * Lightweight append-only telemetry for \"what TITAN did today\".\n * Drives real Facebook \"activity\" posts instead of fictional templates.\n *\n * Format: JSON Lines (~/.titan/activity-log.jsonl)\n * { \"t\": 1714141200000, \"event\": \"tool_call\", \"tool\": \"write_file\", \"session\": \"abc\" }\n * { \"t\": 1714141205000, \"event\": \"agent_spawn\", \"agent\": \"builder\", \"task\": \"fix bug\" }\n *\n * Rotation: keep last 1000 lines (≈ 50-100KB).\n */\nimport { appendFileSync, existsSync, readFileSync, writeFileSync } from 'fs';\nimport { ACTIVITY_LOG_PATH } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ActivityLog';\nconst MAX_LINES = 1000;\n\nexport type ActivityEventType =\n | 'tool_call'\n | 'agent_spawn'\n | 'agent_complete'\n | 'file_edit'\n | 'web_search'\n | 'web_fetch'\n | 'eval_run'\n | 'goal_complete'\n | 'self_improve_proposal'\n | 'error_recovery'\n | 'milestone';\n\nexport interface ActivityEvent {\n t: number; // timestamp ms\n event: ActivityEventType;\n [key: string]: unknown;\n}\n\nexport interface ActivitySummary {\n periodHours: number;\n toolCalls: number;\n agentSpawns: number;\n agentCompletions: number;\n fileEdits: number;\n webSearches: number;\n webFetches: number;\n evalRuns: number;\n goalsCompleted: number;\n selfImproveProposals: number;\n errorRecoveries: number;\n highlights: string[];\n}\n\nlet _inMemoryBuffer: ActivityEvent[] = [];\nlet _bufferFlushMs = 5000;\nlet _bufferTimer: ReturnType<typeof setTimeout> | null = null;\n\n/** Append an event to the activity log (buffered + flushed async) */\nexport function logActivity(event: Omit<ActivityEvent, 't'>): void {\n // The string indexer on ActivityEvent confuses TS's spread narrowing —\n // it loses sight of `event` being a required field after the spread.\n // Cast through unknown is safe because logActivity's parameter type\n // already requires the `event` discriminator.\n const entry = { t: Date.now(), ...event } as unknown as ActivityEvent;\n _inMemoryBuffer.push(entry);\n scheduleFlush();\n}\n\nfunction scheduleFlush(): void {\n if (_bufferTimer) return;\n _bufferTimer = setTimeout(() => {\n _bufferTimer = null;\n flushBuffer();\n }, _bufferFlushMs);\n}\n\n/** Flush buffered events to disk. Exposed for tests and shutdown hooks. */\nexport function flushBuffer(): void {\n if (_inMemoryBuffer.length === 0) return;\n const lines = _inMemoryBuffer.map(e => JSON.stringify(e)).join('\\n') + '\\n';\n _inMemoryBuffer = [];\n try {\n appendFileSync(ACTIVITY_LOG_PATH, lines, 'utf-8');\n enforceRotation();\n } catch (e) {\n logger.error(COMPONENT, `Failed to write activity log: ${(e as Error).message}`);\n }\n}\n\nfunction enforceRotation(): void {\n try {\n if (!existsSync(ACTIVITY_LOG_PATH)) return;\n const data = readFileSync(ACTIVITY_LOG_PATH, 'utf-8');\n const lines = data.split('\\n').filter(l => l.trim());\n if (lines.length <= MAX_LINES) return;\n const keep = lines.slice(-MAX_LINES);\n writeFileSync(ACTIVITY_LOG_PATH, keep.join('\\n') + '\\n', 'utf-8');\n logger.debug(COMPONENT, `Rotated activity log to ${keep.length} lines`);\n } catch (e) {\n logger.warn(COMPONENT, `Rotation failed: ${(e as Error).message}`);\n }\n}\n\n/** Read all events from disk + buffer */\nexport function readActivityEvents(): ActivityEvent[] {\n const events: ActivityEvent[] = [];\n try {\n if (existsSync(ACTIVITY_LOG_PATH)) {\n const data = readFileSync(ACTIVITY_LOG_PATH, 'utf-8');\n for (const line of data.split('\\n')) {\n if (!line.trim()) continue;\n try { events.push(JSON.parse(line) as ActivityEvent); } catch { /* skip bad line */ }\n }\n }\n } catch (e) {\n logger.warn(COMPONENT, `Failed to read activity log: ${(e as Error).message}`);\n }\n return events.concat(_inMemoryBuffer);\n}\n\n/** Summarize activity over the last N hours */\nexport function getActivitySummary(periodHours = 24): ActivitySummary {\n const cutoff = Date.now() - periodHours * 60 * 60 * 1000;\n const events = readActivityEvents().filter(e => e.t >= cutoff);\n\n const summary: ActivitySummary = {\n periodHours,\n toolCalls: 0,\n agentSpawns: 0,\n agentCompletions: 0,\n fileEdits: 0,\n webSearches: 0,\n webFetches: 0,\n evalRuns: 0,\n goalsCompleted: 0,\n selfImproveProposals: 0,\n errorRecoveries: 0,\n highlights: [],\n };\n\n for (const e of events) {\n switch (e.event) {\n case 'tool_call': summary.toolCalls++; break;\n case 'agent_spawn': summary.agentSpawns++; break;\n case 'agent_complete': summary.agentCompletions++; break;\n case 'file_edit': summary.fileEdits++; break;\n case 'web_search': summary.webSearches++; break;\n case 'web_fetch': summary.webFetches++; break;\n case 'eval_run': summary.evalRuns++; break;\n case 'goal_complete': summary.goalsCompleted++; break;\n case 'self_improve_proposal': summary.selfImproveProposals++; break;\n case 'error_recovery': summary.errorRecoveries++; break;\n case 'milestone':\n if (e.description && typeof e.description === 'string') {\n summary.highlights.push(e.description);\n }\n break;\n }\n }\n\n // Auto-detect highlights: milestone thresholds\n if (summary.toolCalls > 0 && summary.toolCalls % 1000 === 0) {\n summary.highlights.push(`Hit ${summary.toolCalls.toLocaleString()} tool calls`);\n }\n if (summary.agentSpawns > 0 && summary.agentSpawns % 100 === 0) {\n summary.highlights.push(`Spawned ${summary.agentSpawns} agents today`);\n }\n\n return summary;\n}\n\n/** Check if there's anything worth posting about */\nexport function hasInterestingActivity(periodHours = 24): boolean {\n const s = getActivitySummary(periodHours);\n return s.toolCalls > 0 || s.agentSpawns > 0 || s.fileEdits > 0 || s.highlights.length > 0;\n}\n\n/** Format a short narrative from the summary for the LLM prompt */\nexport function formatActivityNarrative(summary: ActivitySummary): string {\n const parts: string[] = [];\n if (summary.agentSpawns > 0) parts.push(`spawned ${summary.agentSpawns} sub-agent${summary.agentSpawns === 1 ? '' : 's'}`);\n if (summary.agentCompletions > 0) parts.push(`completed ${summary.agentCompletions} agent task${summary.agentCompletions === 1 ? '' : 's'}`);\n if (summary.toolCalls > 0) parts.push(`made ${summary.toolCalls.toLocaleString()} tool call${summary.toolCalls === 1 ? '' : 's'}`);\n if (summary.fileEdits > 0) parts.push(`edited ${summary.fileEdits} file${summary.fileEdits === 1 ? '' : 's'}`);\n if (summary.webSearches > 0) parts.push(`ran ${summary.webSearches} web search${summary.webSearches === 1 ? '' : 'es'}`);\n if (summary.evalRuns > 0) parts.push(`ran ${summary.evalRuns} eval suite${summary.evalRuns === 1 ? '' : 's'}`);\n if (summary.goalsCompleted > 0) parts.push(`completed ${summary.goalsCompleted} goal${summary.goalsCompleted === 1 ? '' : 's'}`);\n if (summary.selfImproveProposals > 0) parts.push(`filed ${summary.selfImproveProposals} self-improvement proposal${summary.selfImproveProposals === 1 ? '' : 's'}`);\n if (summary.errorRecoveries > 0) parts.push(`recovered from ${summary.errorRecoveries} error${summary.errorRecoveries === 1 ? '' : 's'}`);\n\n if (parts.length === 0) return '';\n\n let narrative = `In the last ${summary.periodHours}h, TITAN ${parts.join(', ')}.`;\n if (summary.highlights.length > 0) {\n narrative += ` Highlights: ${summary.highlights.join('; ')}.`;\n }\n return narrative;\n}\n"],"mappings":"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","names":[]}
|
package/dist/utils/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
const TITAN_VERSION = "5.3.
|
|
4
|
+
const TITAN_VERSION = "5.3.2";
|
|
5
5
|
const TITAN_CODENAME = "Spacewalk";
|
|
6
6
|
const TITAN_NAME = "TITAN";
|
|
7
7
|
const TITAN_FULL_NAME = "The Intelligent Task Automation Network";
|
|
@@ -52,6 +52,7 @@ const FREELANCE_PROFILE_PATH = join(TITAN_HOME, "freelance-profile.json");
|
|
|
52
52
|
const LEADS_PATH = join(TITAN_HOME, "leads.jsonl");
|
|
53
53
|
const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, "telemetry-events.jsonl");
|
|
54
54
|
const SOMADRIVE_STATE_PATH = join(TITAN_HOME, "soma-drive-state.json");
|
|
55
|
+
const ACTIVITY_LOG_PATH = join(TITAN_HOME, "activity-log.jsonl");
|
|
55
56
|
const DEFAULT_GATEWAY_HOST = "0.0.0.0";
|
|
56
57
|
const DEFAULT_GATEWAY_PORT = 48420;
|
|
57
58
|
const DEFAULT_WEB_PORT = 48421;
|
|
@@ -64,6 +65,7 @@ const DEFAULT_SANDBOX_MODE = "host";
|
|
|
64
65
|
const ALLOWED_TOOLS_DEFAULT = [];
|
|
65
66
|
const DENIED_TOOLS_DEFAULT = [];
|
|
66
67
|
export {
|
|
68
|
+
ACTIVITY_LOG_PATH,
|
|
67
69
|
AGENTS_MD,
|
|
68
70
|
ALLOWED_TOOLS_DEFAULT,
|
|
69
71
|
AUTOPILOT_MD,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.3.
|
|
1
|
+
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.3.2';\nexport const TITAN_CODENAME = 'Spacewalk';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\nexport const DEFAULT_MAX_TOKENS = 12000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAGzB,MAAM,gBAAgB;AACtB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titan-agent",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.2",
|
|
4
4
|
"description": "TITAN — Autonomous AI agent framework with self-improvement, multi-agent orchestration, 36 LLM providers, 16 channel adapters, GPU VRAM management, mesh networking, LiveKit voice, TITAN-Soma homeostatic drives, and a React Mission Control dashboard. Open-source, TypeScript, MIT licensed.",
|
|
5
5
|
"author": "Tony Elliott (https://github.com/Djtony707)",
|
|
6
6
|
"repository": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as S,r as d,j as e,a0 as A,a1 as k}from"./index-
|
|
1
|
+
import{c as S,r as d,j as e,a0 as A,a1 as k}from"./index-CahJbWSR.js";import{R as C}from"./TitanCanvas-C-s0A-lv.js";import{F as T}from"./funnel-PkLdxKyC.js";import"./VoiceOverlay-CmNCrLcd.js";/**
|
|
2
2
|
* @license lucide-react v0.513.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as M,r as c,K as B,j as e,e as q,Z as E,$ as F,t as C,b,S as g,G as O,d as T}from"./index-
|
|
1
|
+
import{c as M,r as c,K as B,j as e,e as q,Z as E,$ as F,t as C,b,S as g,G as O,d as T}from"./index-CahJbWSR.js";import{L as I,c as G,R as V,q as P,g as D,B as H,h as U,N as J,e as Z,U as K,M as W,D as Q}from"./TitanCanvas-C-s0A-lv.js";import{C as X}from"./circle-check-big-DZRE_MbN.js";import{P as Y}from"./play-CcJ9BnCh.js";import"./VoiceOverlay-CmNCrLcd.js";/**
|
|
2
2
|
* @license lucide-react v0.513.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r,b as n,j as e}from"./index-
|
|
1
|
+
import{r,b as n,j as e}from"./index-CahJbWSR.js";import{P as c}from"./PageHeader-BimceqQo.js";import"./VoiceOverlay-CmNCrLcd.js";function p(){const[t,s]=r.useState(null),[l,i]=r.useState(!0);return r.useEffect(()=>{n("/api/config",{headers:{"Content-Type":"application/json"}}).then(a=>a.json()).then(a=>{var o,d;return s({mode:((o=a.autonomy)==null?void 0:o.mode)||"supervised",interval:(d=a.autonomy)==null?void 0:d.autopilotIntervalMs})}).catch(()=>s({mode:"supervised"})).finally(()=>i(!1))},[]),l?e.jsx("div",{className:"text-[var(--text-muted)]",children:"Loading autopilot config..."}):e.jsxs("div",{className:"space-y-6",children:[e.jsx(c,{title:"Autopilot",breadcrumbs:[{label:"Admin",href:"/overview"},{label:"Agent"},{label:"Autopilot"}]}),e.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:[e.jsxs("div",{className:"bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg p-4",children:[e.jsx("p",{className:"text-sm text-[var(--text-muted)]",children:"Mode"}),e.jsx("p",{className:"text-lg font-semibold text-[var(--text)] capitalize",children:t==null?void 0:t.mode})]}),e.jsxs("div",{className:"bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg p-4",children:[e.jsx("p",{className:"text-sm text-[var(--text-muted)]",children:"Interval"}),e.jsx("p",{className:"text-lg font-semibold text-[var(--text)]",children:t!=null&&t.interval?`${Math.round(t.interval/6e4)} min`:"Not set"})]})]}),e.jsx("div",{className:"bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg p-4",children:e.jsx("p",{className:"text-sm text-[var(--text-muted)]",children:"Autopilot mode enables TITAN to run scheduled tasks automatically. Configure via titan.json."})})]})}export{p as default};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as q,r as p,b as T,j as e,az as O}from"./index-
|
|
1
|
+
import{c as q,r as p,b as T,j as e,az as O}from"./index-CahJbWSR.js";import{o as A,R as W,C as B}from"./TitanCanvas-C-s0A-lv.js";import{P as D}from"./play-CcJ9BnCh.js";import{P as H}from"./pause-DCV52koX.js";import{T as G}from"./trophy-uQv_NgDB.js";import{T as U}from"./trending-up-DGjFyubC.js";import{a as V,C as J}from"./VoiceOverlay-CmNCrLcd.js";/**
|
|
2
2
|
* @license lucide-react v0.513.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|