Install
$ agentstack add skill-jamditis-claude-skills-journalism-page-monitoring Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Page monitoring methodology
Patterns for tracking web page changes, detecting content removal, and preserving important pages before they disappear.
Monitoring service comparison
Free-tier limits and retention windows shift annually — verify at the service's pricing page before relying on a specific number. The columns below reflect a 2026 snapshot.
| Service | Free Tier | Best For | History | Alert Speed | |---------|-----------|----------|---------|-------------| | Visualping | A few daily checks (free plan tightened in recent years) | Visual changes | Standard | Minutes | | ChangeTower | Yes (verify current limits) | Compliance, archiving | Multi-year on paid plans | Minutes | | Distill.io | ~5 monitors with 7-day history | Element-level tracking | Limited on free tier | Seconds | | Wachete | Limited | Login-protected pages | 12 months | Minutes | | UptimeRobot | 50 monitors at 5-minute intervals (free SMS removed) | Uptime only | 60 days | 5-min checks | | changedetection.io | Self-hosted; free | Privacy / DIY | Disk space | Configurable | | urlwatch | Self-hosted; free | Cron-driven CLI | Configurable | Configurable |
Quick-start: Monitor a page
Distill.io element monitoring
// Distill.io allows CSS/XPath selectors for precise monitoring
// Example selectors for common use cases:
// Monitor news article headlines
const newsSelector = '.article-headline, h1.title, .story-title';
// Monitor price changes
const priceSelector = '.price, .product-price, [data-price]';
// Monitor stock/availability
const availabilitySelector = '.in-stock, .availability, .stock-status';
// Monitor specific paragraph or section
const sectionSelector = '#main-content p:first-child';
// Monitor table data
const tableSelector = 'table.data-table tbody tr';
Python monitoring script
import requests
import hashlib
import json
import smtplib
from email.mime.text import MIMEText
from datetime import datetime
from pathlib import Path
from typing import Optional
from bs4 import BeautifulSoup
class PageMonitor:
"""Simple page change monitor with local storage."""
def __init__(self, storage_dir: Path):
self.storage_dir = storage_dir
self.storage_dir.mkdir(parents=True, exist_ok=True)
self.state_file = storage_dir / 'monitor_state.json'
self.state = self._load_state()
def _load_state(self) -> dict:
if self.state_file.exists():
return json.loads(self.state_file.read_text())
return {'pages': {}}
def _save_state(self):
self.state_file.write_text(json.dumps(self.state, indent=2))
def _get_page_hash(self, url: str, selector: Optional[str] = None) -> tuple[str, str]:
"""Get content hash and content for a page or element."""
response = requests.get(url, timeout=30, headers={
'User-Agent': 'Mozilla/5.0 (PageMonitor/1.0)'
})
response.raise_for_status()
if selector:
soup = BeautifulSoup(response.text, 'html.parser')
element = soup.select_one(selector)
content = element.get_text(strip=True) if element else ''
else:
content = response.text
content_hash = hashlib.sha256(content.encode()).hexdigest()
return content_hash, content
def add_page(self, url: str, name: str, selector: Optional[str] = None):
"""Add a page to monitor."""
content_hash, content = self._get_page_hash(url, selector)
self.state['pages'][url] = {
'name': name,
'selector': selector,
'last_hash': content_hash,
'last_check': datetime.now().isoformat(),
'last_content': content[:1000], # Store preview
'change_count': 0
}
self._save_state()
print(f"Added: {name} ({url})")
def check_page(self, url: str) -> Optional[dict]:
"""Check single page for changes."""
if url not in self.state['pages']:
return None
page = self.state['pages'][url]
selector = page.get('selector')
try:
new_hash, new_content = self._get_page_hash(url, selector)
except Exception as e:
return {
'url': url,
'name': page['name'],
'status': 'error',
'error': str(e)
}
changed = new_hash != page['last_hash']
result = {
'url': url,
'name': page['name'],
'status': 'changed' if changed else 'unchanged',
'previous_content': page['last_content'],
'new_content': new_content[:1000] if changed else None
}
if changed:
page['last_hash'] = new_hash
page['last_content'] = new_content[:1000]
page['change_count'] += 1
# Archive the change
archive_file = self.storage_dir / f"{hashlib.md5(url.encode()).hexdigest()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
archive_file.write_text(new_content)
page['last_check'] = datetime.now().isoformat()
self._save_state()
return result
def check_all(self) -> list[dict]:
"""Check all monitored pages."""
results = []
for url in self.state['pages']:
result = self.check_page(url)
if result:
results.append(result)
return results
# Usage
monitor = PageMonitor(Path('./page_monitor_data'))
# Add pages to monitor
monitor.add_page(
'https://example.com/important-page',
'Important Page',
selector='.main-content' # Optional: monitor specific element
)
# Check for changes
results = monitor.check_all()
for result in results:
if result['status'] == 'changed':
print(f"CHANGED: {result['name']}")
print(f" Previous: {result['previous_content'][:100]}...")
print(f" New: {result['new_content'][:100]}...")
Uptime monitoring
UptimeRobot API integration
import requests
from typing import List, Optional
class UptimeRobotClient:
"""UptimeRobot API client for monitoring page availability."""
def __init__(self, api_key: str):
self.api_key = api_key
# v2 still works as of 2026 but is in maintenance mode; v3 is
# the current REST API at https://api.uptimerobot.com/v3 with
# a different request shape (Bearer auth, JSON bodies).
self.base_url = "https://api.uptimerobot.com/v2"
def _request(self, endpoint: str, params: dict = None) -> dict:
data = {'api_key': self.api_key}
if params:
data.update(params)
response = requests.post(f"{self.base_url}/{endpoint}", data=data)
return response.json()
def get_monitors(self) -> List[dict]:
"""Get all monitors."""
result = self._request('getMonitors')
return result.get('monitors', [])
def create_monitor(self, friendly_name: str, url: str,
monitor_type: int = 1) -> dict:
"""Create a new monitor.
Types: 1=HTTP(s), 2=Keyword, 3=Ping, 4=Port
"""
return self._request('newMonitor', {
'friendly_name': friendly_name,
'url': url,
'type': monitor_type
})
def get_monitor_uptime(self, monitor_id: int,
custom_uptime_ratios: str = "7-30-90") -> dict:
"""Get uptime statistics for a monitor."""
return self._request('getMonitors', {
'monitors': monitor_id,
'custom_uptime_ratios': custom_uptime_ratios
})
def pause_monitor(self, monitor_id: int) -> dict:
"""Pause a monitor."""
return self._request('editMonitor', {
'id': monitor_id,
'status': 0
})
def resume_monitor(self, monitor_id: int) -> dict:
"""Resume a monitor."""
return self._request('editMonitor', {
'id': monitor_id,
'status': 1
})
# Usage
client = UptimeRobotClient('your-api-key')
# Create monitors for important pages
client.create_monitor('News Homepage', 'https://example-news.com')
client.create_monitor('API Status', 'https://api.example.com/health')
# Check all monitors
for monitor in client.get_monitors():
status = 'UP' if monitor['status'] == 2 else 'DOWN'
print(f"{monitor['friendly_name']}: {status}")
RSS feed generation
Generate RSS from pages without feeds
import requests
from bs4 import BeautifulSoup
from feedgen.feed import FeedGenerator
from datetime import datetime
import hashlib
class RSSGenerator:
"""Generate RSS feeds from web pages."""
def __init__(self, feed_id: str, title: str, link: str):
self.fg = FeedGenerator()
self.fg.id(feed_id)
self.fg.title(title)
self.fg.link(href=link)
self.fg.description(f'Auto-generated feed for {title}')
def add_from_page(self, url: str, item_selector: str,
title_selector: str, link_selector: str,
description_selector: Optional[str] = None):
"""Parse a page and add items to feed.
Args:
url: Page URL to parse
item_selector: CSS selector for each item container
title_selector: CSS selector for title (relative to item)
link_selector: CSS selector for link (relative to item)
description_selector: Optional CSS selector for description
"""
response = requests.get(url, timeout=30)
soup = BeautifulSoup(response.text, 'html.parser')
items = soup.select(item_selector)
for item in items[:20]: # Limit to 20 items
title_elem = item.select_one(title_selector)
link_elem = item.select_one(link_selector)
if not title_elem or not link_elem:
continue
title = title_elem.get_text(strip=True)
link = link_elem.get('href', '')
# Make absolute URL if relative
if link.startswith('/'):
from urllib.parse import urljoin
link = urljoin(url, link)
fe = self.fg.add_entry()
fe.id(hashlib.md5(link.encode()).hexdigest())
fe.title(title)
fe.link(href=link)
if description_selector:
desc_elem = item.select_one(description_selector)
if desc_elem:
fe.description(desc_elem.get_text(strip=True))
fe.published(datetime.now())
def generate_rss(self) -> str:
"""Generate RSS XML string."""
return self.fg.rss_str(pretty=True).decode()
def save_rss(self, filepath: str):
"""Save RSS feed to file."""
self.fg.rss_file(filepath)
# Example: Generate feed for a news site without RSS
rss = RSSGenerator(
'https://example.com/news',
'Example News Feed',
'https://example.com/news'
)
rss.add_from_page(
'https://example.com/news',
item_selector='.news-item',
title_selector='h2 a',
link_selector='h2 a',
description_selector='.summary'
)
# Save the feed
rss.save_rss('example_feed.xml')
Using RSS-Bridge (self-hosted)
# RSS-Bridge generates feeds for sites without them
# Supports Twitter, Instagram, YouTube, and many others
# Docker installation
docker pull rssbridge/rss-bridge
docker run -d -p 3000:80 rssbridge/rss-bridge
# Access at http://localhost:3000
# Select a bridge, enter parameters, get RSS feed URL
Social media monitoring
Twitter/X archiving with Twarc
# Twarc requires X (Twitter) API credentials.
#
# IMPORTANT (2023+): X eliminated the free Twitter API tier and the
# free academic research access program. Current X API pricing has
# shifted multiple times since the 2023 changes — the Basic / Pro /
# Enterprise tier names and pay-per-use credit model have been
# revised; check the current pricing page at
# https://developer.x.com/en/products/x-api before estimating cost.
# For one-off archiving, the snscrape / nitter / web-scraping path
# may be more cost-effective — see the web-scraping skill.
# Installation
# pip install twarc
# Configure (interactive — provides API keys + bearer token)
# twarc2 configure
import subprocess
import json
from pathlib import Path
class TwitterArchiver:
"""Archive Twitter searches and timelines."""
def __init__(self, output_dir: Path):
self.output_dir = output_dir
self.output_dir.mkdir(parents=True, exist_ok=True)
def search(self, query: str, max_results: int = 100) -> Path:
"""Search tweets and save to file."""
output_file = self.output_dir / f"search_{query.replace(' ', '_')}.jsonl"
subprocess.run([
'twarc2', 'search',
'--max-results', str(max_results),
query,
str(output_file)
], check=True)
return output_file
def get_timeline(self, username: str, max_results: int = 100) -> Path:
"""Get user timeline."""
output_file = self.output_dir / f"timeline_{username}.jsonl"
subprocess.run([
'twarc2', 'timeline',
'--max-results', str(max_results),
username,
str(output_file)
], check=True)
return output_file
def parse_archive(self, filepath: Path) -> list[dict]:
"""Parse archived tweets."""
tweets = []
with open(filepath) as f:
for line in f:
data = json.loads(line)
if 'data' in data:
tweets.extend(data['data'])
return tweets
Webhook notifications
Send alerts on changes
import requests
from datetime import datetime
from typing import Optional
class AlertManager:
"""Send alerts when monitored pages change."""
def __init__(self, slack_webhook: str = None,
discord_webhook: str = None,
email_config: dict = None):
self.slack_webhook = slack_webhook
self.discord_webhook = discord_webhook
self.email_config = email_config
def send_slack(self, message: str, channel: str = None):
"""Send Slack notification."""
if not self.slack_webhook:
return
payload = {'text': message}
if channel:
payload['channel'] = channel
requests.post(self.slack_webhook, json=payload)
def send_discord(self, message: str):
"""Send Discord notification."""
if not self.discord_webhook:
return
requests.post(self.discord_webhook, json={'content': message})
def send_email(self, subject: str, body: str, to: str):
"""Send email notification."""
if not self.email_config:
return
import smtplib
from email.mime.text import MIMEText
msg = MIMEText(body)
msg['Subject'] = subject
msg['From'] = self.email_config['from']
msg['To'] = to
with smtplib.SMTP(self.email_config['smtp_host'],
self.email_config['smtp_port']) as server:
server.starttls()
server.login(self.email_config['username'],
self.email_config['password'])
server.send_message(msg)
def alert_change(self, page_name: str, url: str,
old_content: str, new_content: str):
"""Send change alert to all configured channels."""
message = f"""
Page Changed: {page_name}
URL: {url}
Time: {datetime.now().isoformat()}
Previous content (preview):
{old_content[:200]}...
New content (preview):
{new_content[:200]}...
"""
if self.slack_webhook:
self.send_slack(message)
if self.discord_webhook:
self.send_discord(message)
Scheduled monitoring with cron
Cron setup for continuous monitorin
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: jamditis
- Source: jamditis/claude-skills-journalism
- License: MIT
- Homepage: https://skills.amditis.tech/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.