Ai-Mee Help Centre
Home
Features
How-To Guides
FAQ
Need Help?
Home
Features
How-To Guides
FAQ
Need Help?

Phase 4: Proactive Intelligence

Timeline: Week 7-9
Prerequisites: Phase 3 complete
Objective: Make the system generate content proactively, not just reactively.


4.1 — Website Monitoring

Tasks

  • [ ] Create watched_urls database table
  • [ ] Implement POST /client/:id/watch_url endpoint
  • [ ] Build content change detection (hash comparison)
  • [ ] Create monitoring cron job
  • [ ] Add sitemap parser for auto-discovery
  • [ ] Trigger GoClaw agent when changes detected

Watched URLs Table

Create migration front-end/supabase/migrations/20260310_watched_urls.sql:

CREATE TABLE IF NOT EXISTS watched_urls (
  id SERIAL PRIMARY KEY,
  customer_id INTEGER REFERENCES customer_customer(id) ON DELETE CASCADE,
  url TEXT NOT NULL,
  url_type TEXT DEFAULT 'page' CHECK (url_type IN ('page', 'sitemap', 'blog', 'news')),
  last_hash TEXT,
  last_checked_at TIMESTAMPTZ,
  last_changed_at TIMESTAMPTZ,
  check_frequency_hours INTEGER DEFAULT 6,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  metadata JSONB DEFAULT '{}'::jsonb
);

CREATE UNIQUE INDEX idx_watched_urls_unique ON watched_urls(customer_id, url);
CREATE INDEX idx_watched_urls_customer ON watched_urls(customer_id);
CREATE INDEX idx_watched_urls_next_check ON watched_urls(last_checked_at, is_active);

Watch URL Endpoint

Add to api/src/index.ts:

const WatchUrlSchema = Type.Object({
  url: Type.String({ format: 'uri' }),
  url_type: Type.Optional(Type.Union([
    Type.Literal('page'),
    Type.Literal('sitemap'),
    Type.Literal('blog'),
    Type.Literal('news')
  ])),
  check_frequency_hours: Type.Optional(Type.Number({ minimum: 1, maximum: 168 }))
});

app.post<{
  Params: { id: string },
  Body: typeof WatchUrlSchema.static
}>('/client/:id/watch_url', {
  schema: {
    body: WatchUrlSchema
  }
}, async (request, reply) => {
  const { id } = request.params;
  const { url, url_type = 'page', check_frequency_hours = 6 } = request.body;

  // Fetch initial content and hash
  const { markdown } = await scrapeUrl(url);
  const hash = crypto.createHash('sha256').update(markdown).digest('hex');

  const { data, error } = await supabase
    .from('watched_urls')
    .upsert({
      customer_id: parseInt(id),
      url,
      url_type,
      last_hash: hash,
      last_checked_at: new Date().toISOString(),
      check_frequency_hours,
      is_active: true
    }, {
      onConflict: 'customer_id,url'
    })
    .select()
    .single();

  if (error) {
    return reply.code(500).send({ error: error.message });
  }

  return data;
});

Content Change Detection

Create api/src/utils/url-monitor.ts:

import crypto from 'crypto';
import { scrapeUrl } from './scraper.js';
import { createSupabaseClient } from './supabase.js';

const supabase = createSupabaseClient();

export async function checkUrlForChanges(watchedUrl: any): Promise<boolean> {
  try {
    const { markdown } = await scrapeUrl(watchedUrl.url);
    const newHash = crypto.createHash('sha256').update(markdown).digest('hex');

    if (newHash !== watchedUrl.last_hash) {
      // Content changed!
      await supabase
        .from('watched_urls')
        .update({
          last_hash: newHash,
          last_changed_at: new Date().toISOString(),
          last_checked_at: new Date().toISOString()
        })
        .eq('id', watchedUrl.id);

      return true; // Changed
    } else {
      // No change
      await supabase
        .from('watched_urls')
        .update({ last_checked_at: new Date().toISOString() })
        .eq('id', watchedUrl.id);

      return false;
    }
  } catch (error) {
    console.error(`Error checking URL ${watchedUrl.url}:`, error);
    return false;
  }
}

export async function checkAllWatchedUrls() {
  const now = new Date();

  // Get URLs that need checking
  const { data: urls, error } = await supabase
    .from('watched_urls')
    .select('*')
    .eq('is_active', true)
    .or(`last_checked_at.is.null,last_checked_at.lt.${new Date(now.getTime() - 6 * 60 * 60 * 1000).toISOString()}`);

  if (error || !urls) {
    console.error('Error fetching watched URLs:', error);
    return;
  }

  const changedUrls: any[] = [];

  for (const url of urls) {
    const changed = await checkUrlForChanges(url);
    if (changed) {
      changedUrls.push(url);
    }
  }

  return changedUrls;
}

Monitoring Cron Job

Replace Bree with a proper Node cron or use GoClaw's cron system.

Option A: Node-based (API-side cron)

Install: pnpm add node-cron

Create api/src/scheduler/url-monitor.ts:

import cron from 'node-cron';
import { checkAllWatchedUrls } from '../utils/url-monitor.js';
import { env } from '../config/env.js';

export function startUrlMonitoring() {
  // Run every hour
  cron.schedule('0 * * * *', async () => {
    console.log('Running URL monitoring check...');
    
    const changedUrls = await checkAllWatchedUrls();
    
    if (changedUrls && changedUrls.length > 0) {
      console.log(`Found ${changedUrls.length} changed URLs`);
      
      // Trigger GoClaw agent for each change
      for (const url of changedUrls) {
        await triggerContentGeneration(url);
      }
    }
  });
}

async function triggerContentGeneration(watchedUrl: any) {
  // Call GoClaw API to trigger creator-agent
  const goclawUrl = `${env.GOCLAW_API_URL}/sessions/message`;
  
  // Get telegram_chat_id for this customer
  const { data: mapping } = await supabase
    .from('telegram_client_mapping')
    .select('telegram_chat_id')
    .eq('customer_id', watchedUrl.customer_id)
    .single();

  if (!mapping) return;

  await fetch(goclawUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      channel: 'telegram',
      chat_id: mapping.telegram_chat_id,
      message: `I noticed your website was updated: ${watchedUrl.url}. Would you like me to create content about the changes?`,
      agent_id: 'liaison-agent'
    })
  });
}

Start in api/src/server.ts:

import { startUrlMonitoring } from './scheduler/url-monitor.js';

startUrlMonitoring();

Option B: GoClaw-side cron (recommended)

Create GoClaw cron job that calls API to check URLs:

{
  "name": "website-monitor",
  "schedule": {
    "kind": "every",
    "everyMs": 3600000
  },
  "message": "Check all watched URLs for content changes. For any changes found, notify the client and offer to create content.",
  "agentId": "liaison-agent"
}

Liaison agent handles:

## Website Monitoring (Hourly Cron)

When triggered by cron:

1. Use tool: web_fetch GET http://api:8000/watched-urls/check
2. API returns list of URLs that changed
3. For each changed URL:
   - Fetch the customer's telegram_chat_id
   - Send message: "I noticed [url] was updated. Would you like me to create content about it?"
   - Wait for response
   - If yes: fetch_client_context, delegate to creator-agent

Add endpoint:

app.get('/watched-urls/check', async (request, reply) => {
  const changedUrls = await checkAllWatchedUrls();
  return changedUrls || [];
});

Sitemap Parser

Create api/src/utils/sitemap-parser.ts:

import { XMLParser } from 'fast-xml-parser';

export async function parseSitemap(sitemapUrl: string): Promise<string[]> {
  const response = await fetch(sitemapUrl);
  const xml = await response.text();

  const parser = new XMLParser();
  const parsed = parser.parse(xml);

  const urls: string[] = [];

  // Handle standard sitemap
  if (parsed.urlset?.url) {
    const urlEntries = Array.isArray(parsed.urlset.url)
      ? parsed.urlset.url
      : [parsed.urlset.url];

    for (const entry of urlEntries) {
      urls.push(entry.loc);
    }
  }

  // Handle sitemap index
  if (parsed.sitemapindex?.sitemap) {
    const sitemaps = Array.isArray(parsed.sitemapindex.sitemap)
      ? parsed.sitemapindex.sitemap
      : [parsed.sitemapindex.sitemap];

    for (const sm of sitemaps) {
      const subUrls = await parseSitemap(sm.loc);
      urls.push(...subUrls);
    }
  }

  return urls;
}

Endpoint to auto-discover from sitemap:

app.post<{ Params: { id: string }, Body: { sitemap_url: string } }>(
  '/client/:id/watch_sitemap',
  async (request, reply) => {
    const { id } = request.params;
    const { sitemap_url } = request.body;

    const urls = await parseSitemap(sitemap_url);

    // Watch all URLs found in sitemap
    const results = [];
    for (const url of urls) {
      const { data } = await supabase
        .from('watched_urls')
        .upsert({
          customer_id: parseInt(id),
          url,
          url_type: 'blog',
          is_active: true,
          check_frequency_hours: 24
        }, {
          onConflict: 'customer_id,url'
        })
        .select();

      results.push(data);
    }

    return {
      discovered: urls.length,
      watching: results.length
    };
  }
);

4.2 — Event Calendar Integration

Tasks

  • [ ] Populate marketing_calendar with holidays and events
  • [ ] Create GET /calendar/upcoming endpoint
  • [ ] Build GoClaw cron for weekly content planning
  • [ ] Integrate calendar into creator-agent workflow

Populate Calendar

Create seed file front-end/supabase/seeds/marketing_calendar.sql:

INSERT INTO marketing_calendar (event_name, event_date, event_type, industries, description, is_global) VALUES
  ('New Year''s Day', '2026-01-01', 'holiday', '{}', 'Start of the year celebrations', true),
  ('Valentine''s Day', '2026-02-14', 'holiday', '{retail,food,hospitality}', 'Romance and gift-giving', false),
  ('International Women''s Day', '2026-03-08', 'awareness_day', '{}', 'Celebrating women''s achievements', true),
  ('Earth Day', '2026-04-22', 'awareness_day', '{sustainability,environment}', 'Environmental awareness', false),
  ('Mother''s Day', '2026-05-10', 'holiday', '{retail,food,hospitality}', 'Honoring mothers', false),
  ('Father''s Day', '2026-06-21', 'holiday', '{retail,food,hospitality}', 'Honoring fathers', false),
  ('Back to School', '2026-08-15', 'custom', '{retail,education}', 'School season begins', false),
  ('Halloween', '2026-10-31', 'holiday', '{retail,food,entertainment}', 'Spooky celebrations', false),
  ('Black Friday', '2026-11-27', 'custom', '{retail,ecommerce}', 'Major shopping day', false),
  ('Cyber Monday', '2026-11-30', 'custom', '{ecommerce,technology}', 'Online shopping deals', false),
  ('Christmas', '2026-12-25', 'holiday', '{}', 'Major winter holiday', true),
  ('Small Business Saturday', '2026-11-28', 'custom', '{retail,small_business}', 'Support local businesses', false)
ON CONFLICT DO NOTHING;

Run: npx supabase db seed

Upcoming Events Endpoint

const UpcomingEventsQuerySchema = Type.Object({
  industries: Type.Optional(Type.Array(Type.String())),
  days: Type.Optional(Type.Number({ minimum: 1, maximum: 90 }))
});

app.get<{ Querystring: typeof UpcomingEventsQuerySchema.static }>(
  '/calendar/upcoming',
  {
    schema: {
      querystring: UpcomingEventsQuerySchema
    }
  },
  async (request, reply) => {
    const { industries = [], days = 14 } = request.query;

    const endDate = new Date();
    endDate.setDate(endDate.getDate() + days);

    let query = supabase
      .from('marketing_calendar')
      .select('*')
      .gte('event_date', new Date().toISOString().split('T')[0])
      .lte('event_date', endDate.toISOString().split('T')[0])
      .order('event_date', { ascending: true });

    // Filter by industries if provided
    if (industries.length > 0) {
      query = query.or(
        `industries.cs.{${industries.join(',')}},is_global.eq.true`
      );
    }

    const { data, error } = await query;

    if (error) {
      return reply.code(500).send({ error: error.message });
    }

    return data || [];
  }
);

Weekly Content Planning Cron

GoClaw cron job (Monday 9 AM):

{
  "name": "weekly-content-planning",
  "schedule": {
    "kind": "cron",
    "expr": "0 9 * * 1",
    "timezone": "America/New_York"
  },
  "message": "Check upcoming events/holidays for the next 14 days. For each active client, generate themed content batch and present for approval.",
  "agentId": "creator-agent",
  "deliver": false
}

Creator-agent skill addition:

## Weekly Content Planning (Cron)

When triggered by Monday cron:

1. Fetch all active customers
2. For each customer:
   - Call fetch_client_context
   - Call web_fetch GET /calendar/upcoming?industries=[client industries]&days=14
   - If events found:
     - Generate themed content for top 2-3 relevant events
     - Delegate results to liaison-agent with context
     - Liaison presents to client: "I noticed [Event] is coming up. Here are some post ideas..."

4.3 — Analytics Feedback Loop

Tasks

  • [ ] Build analytics collectors per platform
  • [ ] Create monthly analytics aggregation cron
  • [ ] Insert learnings into GoClaw memory
  • [ ] Update creator-agent to query memory before generation

Analytics Collectors

Install platform API clients:

cd api
pnpm add @mailchimp/mailchimp_transactional

Create api/src/integrations/analytics/:

Twitter Analytics (twitter-analytics.ts):

import { TwitterApi } from 'twitter-api-v2';

export async function fetchTwitterAnalytics(
  client: TwitterApi,
  tweetId: string
): Promise<{
  likes: number;
  retweets: number;
  replies: number;
  impressions: number;
}> {
  const tweet = await client.v2.singleTweet(tweetId, {
    'tweet.fields': ['public_metrics']
  });

  return {
    likes: tweet.data.public_metrics?.like_count || 0,
    retweets: tweet.data.public_metrics?.retweet_count || 0,
    replies: tweet.data.public_metrics?.reply_count || 0,
    impressions: tweet.data.public_metrics?.impression_count || 0
  };
}

Mailchimp Email Analytics (mailchimp-analytics.ts):

import mailchimp from '@mailchimp/mailchimp_transactional';

export async function fetchEmailAnalytics(
  apiKey: string,
  messageId: string
): Promise<{
  opens: number;
  clicks: number;
  open_rate: number;
  click_rate: number;
}> {
  const client = mailchimp(apiKey);
  
  const info = await client.messages.info({ id: messageId });
  
  return {
    opens: info.opens,
    clicks: info.clicks,
    open_rate: info.opens / Math.max(info.sends, 1),
    click_rate: info.clicks / Math.max(info.opens, 1)
  };
}

Monthly Analytics Aggregation

GoClaw cron (first day of month):

{
  "name": "monthly-analytics-review",
  "schedule": {
    "kind": "cron",
    "expr": "0 2 1 * *"
  },
  "message": "Aggregate last month's post performance. Extract insights and update client memory with learnings.",
  "agentId": "liaison-agent",
  "deliver": false
}

API endpoint to aggregate:

app.post<{ Params: { id: string } }>(
  '/client/:id/analytics/aggregate',
  async (request, reply) => {
    const { id } = request.params;

    // Fetch all published posts from last 30 days with analytics
    const { data: posts, error } = await supabase
      .from('customer_platform_post')
      .select(`
        id,
        platform,
        content,
        customer_post:customer_posts!inner(customer_id),
        analytics:post_analytics(metric_type, metric_value)
      `)
      .eq('customer_post.customer_id', parseInt(id))
      .gte('customer_post.created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());

    if (error || !posts) {
      return reply.code(500).send({ error: error?.message || 'No data' });
    }

    // Analyze patterns
    const insights = analyzePostPerformance(posts);

    return insights;
  }
);

function analyzePostPerformance(posts: any[]): Record<string, any> {
  // Group by platform
  const byPlatform = posts.reduce((acc, post) => {
    if (!acc[post.platform]) acc[post.platform] = [];
    acc[post.platform].push(post);
    return acc;
  }, {} as Record<string, any[]>);

  const insights: Record<string, any> = {};

  for (const [platform, platformPosts] of Object.entries(byPlatform)) {
    // Calculate averages
    const avgLikes = platformPosts.reduce((sum, p) =>
      sum + (p.analytics.find((a: any) => a.metric_type === 'likes')?.metric_value || 0), 0
    ) / platformPosts.length;

    // Find top performer
    const topPost = platformPosts.sort((a, b) => {
      const aLikes = a.analytics.find((m: any) => m.metric_type === 'likes')?.metric_value || 0;
      const bLikes = b.analytics.find((m: any) => m.metric_type === 'likes')?.metric_value || 0;
      return bLikes - aLikes;
    })[0];

    insights[platform] = {
      post_count: platformPosts.length,
      avg_likes: avgLikes,
      top_post_content: topPost?.content.substring(0, 100),
      top_post_performance: topPost?.analytics
    };
  }

  return insights;
}

Insert Learnings into GoClaw Memory

Liaison agent handles monthly cron:

## Monthly Analytics Review (Cron)

When triggered:

1. For each active client:
   - Call web_fetch POST /client/:id/analytics/aggregate
   - Receive insights object
   - Formulate actionable rules:
     ```
     "For client X (id: 123):
      - Twitter posts with questions get 40% more likes than statements
      - Instagram carousel posts outperform single images 2:1
      - Email subject lines under 40 chars have 25% higher open rates
      - LinkedIn posts with industry stats get 3x more shares"
     ```
   - Use the `memory_search` tool to store these as memories with tags:
     ```
     memory_save(
       content: [rules above],
       tags: ["analytics", "client_123", "best_practices"],
       importance: "high"
     )
     ```
2. Send summary to client via Telegram

Creator Agent Memory Integration

Update content-creation/SKILL.md:

## Before You Write (Updated)

1. **ALWAYS call `fetch_client_context`**
2. **Search memory for past learnings**:

memory_search(query: "client_[id] analytics best_practices", limit:10)

3. **Apply learnings to your content**:
- If memory says "questions perform better on Twitter" → use question format
- If memory says "short emails work better" → keep it under 200 words
- If memory says "industry stats boost LinkedIn" → include relevant stat

4.4 — Scheduler Overhaul

Tasks

  • [ ] Remove Bree dependency
  • [ ] Document all GoClaw cron jobs in one place
  • [ ] Set up per-client scheduling preferences
  • [ ] Enforce "max 3 unsent posts" rule

Remove Bree

cd api
pnpm remove bree
rm -rf scheduler/bree.ts

GoClaw Cron Consolidation

Create goclaw/cron-jobs.json documenting all jobs:

{
  "cron_jobs": [
    {
      "name": "approval-reminders",
      "schedule": "0 10 * * *",
      "description": "Daily 10 AM: remind clients about pending approvals",
      "agent": "liaison-agent"
    },
    {
      "name": "weekly-summary",
      "schedule": "0 9 * * 1",
      "description": "Monday 9 AM: send weekly summary to clients",
      "agent": "liaison-agent"
    },
    {
      "name": "weekly-content-planning",
      "schedule": "0 9 * * 1",
      "description": "Monday 9 AM: check events, generate themed content",
      "agent": "creator-agent"
    },
    {
      "name": "website-monitor",
      "schedule": "every 1 hour",
      "description": "Hourly: check watched URLs for changes",
      "agent": "liaison-agent"
    },
    {
      "name": "monthly-analytics-review",
      "schedule": "0 2 1 * *",
      "description": "1st of month 2 AM: aggregate analytics, update memory",
      "agent": "liaison-agent"
    }
  ]
}

Load these into GoClaw on startup or via admin API.

Per-Client Scheduling

Add to customer_customer table:

ALTER TABLE customer_customer
ADD COLUMN scheduling_preferences JSONB DEFAULT '{
  "cadence": "weekly",
  "preferred_days": ["Monday", "Wednesday", "Friday"],
  "max_unsent_posts": 3,
  "auto_publish": false,
  "timezone": "America/New_York"
}'::jsonb;

Max Unsent Posts Rule

Update generate_post endpoint:

// Before creating new post, check unsent count
const { count, error: countError } = await supabase
  .from('customer_posts')
  .select('*', { count: 'exact', head: true })
  .eq('customer_id', customer_id)
  .in('status', ['new', 'pending_review', 'approved']);

const preferences = customerData.scheduling_preferences || { max_unsent_posts: 3 };

if (count && count >= preferences.max_unsent_posts) {
  return reply.code(429).send({
    error: `Maximum of ${preferences.max_unsent_posts} unsent posts reached. Please approve or reject existing posts first.`
  });
}

New Tooling Required

  • node-cron or fast-xml-parser — Scheduling and sitemap parsing
  • crypto (built-in) — Content hashing
  • Analytics API clients (Twitter, Mailchimp)
  • Remove bree dependency

Verification Checklist

  • [ ] Watched URLs are monitored and changes trigger notifications
  • [ ] Sitemap parser discovers and watches all blog posts
  • [ ] Calendar returns upcoming events filtered by industry
  • [ ] Weekly content planning cron generates themed posts
  • [ ] Analytics collectors fetch real data from platforms
  • [ ] Monthly analytics cron updates GoClaw memory
  • [ ] Creator-agent queries memory before generating content
  • [ ] Bree is removed, all scheduling via GoClaw cron
  • [ ] Max unsent posts rule prevents queue overflow

Next Steps

Proceed to Phase 5: Dashboard Evolution to transform the Vue dashboard into an admin/analytics panel.