blogwatcher
Monitor blogs and RSS/Atom feeds for updates using the blogwatcher-cli tool. Add blogs, scan for new articles, track read status, and filter by category.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Monitor blogs and RSS/Atom feeds for updates using the blogwatcher-cli tool. Add blogs, scan for new articles, track read status, and filter by category.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Comprehensive wiki health checking, maintenance, and remediation — duplicate detection, entity dedup/disambiguation, link auditing/fixing, page splitting, source-linking lint, wikilink remediation, tag taxonomy audit and normalization, pre-commit enforcement, language enforcement (JP→EN bulk translation, detection regex, cron-assisted migration), and decision-matrix-driven cleanup.
Comprehensive wiki maintenance: daily structural health checks (index reconciliation, log separator fixes, pipeline watchdog alerts), comparison page updates (adding items to multi-section comparison tables), and page relocation (moving/renaming pages while maintaining link integrity).
Pre-flight checklist and procedures for archiving, deleting, or migrating Hermes skills. Prevents accidental removal of cron-referenced skills. Covers the 3-layer skill structure and config.yaml management. Includes skill inventory management, promotion workflows, and archival conventions.
Karpathy's LLM Wiki: build/query interlinked markdown KB.
Deep analysis of blog authors' recent thoughts, philosophy, and positions. Goes beyond entity page creation to extract cited ideological positions, track thought evolution, and enable ongoing RSS monitoring for thought updates.
Query the blogwatcher-cli SQLite database for RSS scan results. Use pre-verified column names and query templates to avoid errors.
| name | blogwatcher |
| description | Monitor blogs and RSS/Atom feeds for updates using the blogwatcher-cli tool. Add blogs, scan for new articles, track read status, and filter by category. |
| version | 2.2.0 |
| author | JulienTant (fork of Hyaxia/blogwatcher) |
| license | MIT |
| metadata | {"hermes":{"tags":["RSS","Blogs","Feed-Reader","Monitoring"],"homepage":"https://github.com/JulienTant/blogwatcher-cli"}} |
| prerequisites | {"commands":["blogwatcher-cli"]} |
Track blog and RSS/Atom feed updates with the blogwatcher-cli tool. Supports automatic feed discovery, HTML scraping fallback, OPML import, and read/unread article management.
Pick one method:
go install github.com/JulienTant/blogwatcher-cli/cmd/blogwatcher-cli@latestdocker run --rm -v blogwatcher-cli:/data ghcr.io/julientant/blogwatcher-clicurl -sL https://github.com/JulienTant/blogwatcher-cli/releases/latest/download/blogwatcher-cli_linux_amd64.tar.gz | tar xz -C ~/bin blogwatcher-cli (use ~/bin/ if /usr/local/bin/ has permission issues)curl -sL https://github.com/JulienTant/blogwatcher-cli/releases/latest/download/blogwatcher-cli_linux_arm64.tar.gz | tar xz -C /usr/local/bin blogwatcher-clicurl -sL https://github.com/JulienTant/blogwatcher-cli/releases/latest/download/blogwatcher-cli_darwin_arm64.tar.gz | tar xz -C /usr/local/bin blogwatcher-clicurl -sL https://github.com/JulienTant/blogwatcher-cli/releases/latest/download/blogwatcher-cli_darwin_amd64.tar.gz | tar xz -C /usr/local/bin blogwatcher-cliAll releases: https://github.com/JulienTant/blogwatcher-cli/releases
By default the database lives at ~/.blogwatcher-cli/blogwatcher-cli.db. In Docker this is lost on container restart. Use BLOGWATCHER_DB or a volume mount to persist it:
# Named volume (simplest)
docker run --rm -v blogwatcher-cli:/data -e BLOGWATCHER_DB=/data/blogwatcher-cli.db ghcr.io/julientant/blogwatcher-cli scan
# Host bind mount
docker run --rm -v /path/on/host:/data -e BLOGWATCHER_DB=/data/blogwatcher-cli.db ghcr.io/julientant/blogwatcher-cli scan
If upgrading from Hyaxia/blogwatcher, move your database:
mv ~/.blogwatcher/blogwatcher.db ~/.blogwatcher-cli/blogwatcher-cli.db
The binary name changed from blogwatcher to blogwatcher-cli.
blogwatcher-cli add "My Blog" https://example.comblogwatcher-cli add "My Blog" https://example.com --feed-url https://example.com/feed.xmlblogwatcher-cli add "My Blog" https://example.com --scrape-selector "article h2 a"blogwatcher-cli blogsblogwatcher-cli remove "My Blog" --yesblogwatcher-cli import subscriptions.opmlblogwatcher-cli scanblogwatcher-cli scan "My Blog"blogwatcher-cli articlesblogwatcher-cli articles --allblogwatcher-cli articles --blog "My Blog"blogwatcher-cli articles --category "Engineering"blogwatcher-cli read 1blogwatcher-cli unread 1blogwatcher-cli read-allblogwatcher-cli read-all --blog "My Blog" --yesAll flags can be set via environment variables with the BLOGWATCHER_ prefix:
| Variable | Description |
|---|---|
BLOGWATCHER_DB | Path to SQLite database file |
BLOGWATCHER_WORKERS | Number of concurrent scan workers (default: 8) |
BLOGWATCHER_SILENT | Only output "scan done" when scanning |
BLOGWATCHER_YES | Skip confirmation prompts |
BLOGWATCHER_CATEGORY | Default filter for articles by category |
$ blogwatcher-cli blogs
Tracked blogs (1):
xkcd
URL: https://xkcd.com
Feed: https://xkcd.com/atom.xml
Last scanned: 2026-04-03 10:30
$ blogwatcher-cli scan
Scanning 1 blog(s)...
xkcd
Source: RSS | Found: 4 | New: 4
Found 4 new article(s) total!
$ blogwatcher-cli articles
Unread articles (2):
[1] [new] Barrel - Part 13
Blog: xkcd
URL: https://xkcd.com/3095/
Published: 2026-04-02
Categories: Comics, Science
[2] [new] Volcano Fact
Blog: xkcd
URL: https://xkcd.com/3094/
Published: 2026-04-01
Categories: Comics
Reddit subreddits expose RSS feeds at: https://www.reddit.com/r/SUBREDDIT/.rss
These work like any other blog feed — add with blogwatcher-cli add "r/Name" "https://www.reddit.com/r/Name/.rss"
YouTube channels expose standard Atom/RSS feeds at https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID. These work with blogwatcher-cli like any other feed — each new video becomes a discoverable article with its description as content. The daily blog-ingest cron job picks up new videos automatically.
For deep transcript ingestion of individual videos, use the youtube-content skill workflow (yt-dlp → transcript → structured raw article → entity/concept enrichment).
When the user asks to add a YouTube channel to monitoring:
Get the channel ID via yt-dlp:
/opt/data/bin/yt-dlp --print channel_id --playlist-end 1 "https://www.youtube.com/@HANDLE"
Test the RSS feed:
curl -s "https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID" | head -20
Add to OPML in the "YouTube Channels" section (create the section if it doesn't exist):
<outline text="YouTube Channels" title="YouTube Channels">
<outline type="rss" text="Channel Name" title="Channel Name" xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID" htmlUrl="https://www.youtube.com/@HANDLE" />
</outline>
Insert before </body> in config/feeds/blogs.opml. Validate: python3 -c "import xml.etree.ElementTree as ET; ET.parse('config/feeds/blogs.opml'); print('OK')"
Add to blogwatcher DB with explicit --feed-url (auto-discovery won't work on YouTube's JS-heavy pages):
blogwatcher-cli add "Channel Name" "https://www.youtube.com/@HANDLE" --feed-url "https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID"
Verify scan works:
blogwatcher-cli scan "Channel Name"
# Expected: Source: RSS | Found: N | New: N
Mark initial backlog as read (prevents re-processing hundreds of historical videos):
blogwatcher-cli read-all --blog "Channel Name" --yes
Create wiki entity page for the channel under wiki/entities/<channel-name>.md:
type: entity, tags: [youtube, …]wiki/index.md (alphabetical order) and wiki/log.mdCommit config + wiki:
cd ~/ai-topics && git add config/feeds/blogs.opml wiki/entities/ wiki/index.md wiki/log.md
git commit -m "config: add Channel Name YouTube channel to RSS monitoring + entity page"
git push
yt-dlp --print channel_id to resolve. Example: @aidotengineer → UCLKPca3kwwd-B59HNr-_lvA.youtube-content skill on individual videos.blog-triage step filters by importance before wiki ingestion.<outline text="YouTube Channels"> as a top-level group, not mixed into "Individual Blogs" or "Company Tech Blogs". This keeps the OPML organized by source type./opt/data/bin/blogwatcher-cli (Hyaxia/blogwatcher v0.0.2, NOT JulienTant fork)~/ai-topics/config/feeds/blogs.opml (~87 blogs as of 2026-05-08)
<outline text="Individual Blogs"> (84 personal blogs), <outline text="Company Tech Blogs"> (RSS + sitemap-based), and <outline text="YouTube Channels"> (YouTube RSS feeds)type="rss" for RSS-based or type="sitemap" for sitemap-based (e.g., Anthropic Engineering)/opt/data/.blogwatcher/blogwatcher.db (NOT ~/.blogwatcher-cli/ — uses the original Hyaxia/blogwatcher path)HOME=/opt/data/.hermes/home but DB is at /opt/data/.blogwatcher/. The daily_inbox_collect.py script must use PROFILE_ROOT instead of Path.home() for DB path resolution, and run_blogwatcher_scan() must set HOME=str(PROFILE_ROOT) in the subprocess env. See blogwatcher-db skill's references/cron-home-fix.md for the exact patch.python3 -c "import sqlite3; conn = sqlite3.connect('/opt/data/.blogwatcher/blogwatcher.db'); ..."blog-ingest (Job ID: 1bf4c6492c1e) runs at 0 7 * * * UTC = JST 16:00 — RSS blog scan via blogwatchersitemap-monitor (Job ID: c7890b6e0e19) runs at 0 6 * * * UTC = JST 15:00 — sitemap-based monitoring for corporate blogs without RSSYouTube channels expose RSS feeds at a predictable URL pattern and can be monitored via blogwatcher just like blogs. Use this workflow when the user asks to add a YouTube channel to monitoring.
https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
To find the channel ID:
/opt/data/bin/yt-dlp --print channel_id --playlist-end 1 "https://www.youtube.com/@channelhandle" 2>/dev/null
Add a new "YouTube Channels" section to config/feeds/blogs.opml before </body>:
<outline text="YouTube Channels" title="YouTube Channels">
<outline type="rss" text="Channel Name" title="Channel Name" xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID" htmlUrl="https://www.youtube.com/@channelhandle" />
</outline>
blogwatcher-cli add "Channel Name" "https://www.youtube.com/@channelhandle" \
--feed-url "https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID"
blogwatcher-cli scan "Channel Name" # Verify scan works
blogwatcher-cli read-all --blog "Channel Name" --yes # Mark initial backlog as read
Create an entity page under wiki/entities/ with channel overview, key facts, content themes, notable talks, and monitoring info. The RSS feed URL and blogwatcher pipeline details should be documented in the Monitoring section.
When the user asks "Xのブログが監視対象に含まれているか確認し、なければ追加して" (check if X blog is monitored, add if not):
Check OPML: grep -i "keyword" /opt/data/ai-topics/config/feeds/blogs.opml
Discover RSS feed: If not in OPML, find the feed URL.
<link> tags → try root-level paths → try blog-subdirectory pathscurl -sI "https://example.com/rss.xml" | head -1 # ← TRY FIRST
curl -sI "https://example.com/feed.xml" | head -1
curl -sI "https://example.com/atom.xml" | head -1
curl -sI "https://example.com/index.xml" | head -1
curl -sI "https://example.com/blog/rss.xml" | head -1
curl -sI "https://example.com/blog/feed.xml" | head -1
curl -sI "https://example.com/feed" | head -1
references/feed-discovery-patterns.md for platform-specific patterns, or references/corporate-blog-tracking.md for corporate blog-specific approaches (sitemap, scrape-selector).curl -s "https://example.com/sitemap.xml" | grep -i "blog\|engineering"
Verify feed content: curl -s "FEED_URL" | head -30
<rss, <feed, <channel>, <item> elements)--scrape-selectorInsert into OPML: Add an <outline> entry in the correct group.
<outline text="Individual Blogs"> group (lines ~7-109)<outline text="Company Tech Blogs"> group (lines ~110-135)type="rss"type="sitemap" and xmlUrl pointing to the sitemap URL<outline type="rss" text="Blog Name" title="Blog Name" xmlUrl="FEED_URL" htmlUrl="SITE_URL" /><outline type="sitemap" text="Blog Name" title="Blog Name" xmlUrl="SITEMAP_URL" htmlUrl="SITE_URL" />grep -n "Blog Name" blogs.opmlAdd to blogwatcher DB AND scan + cleanup:
# Add (RSS):
blogwatcher-cli add "Blog Name" "https://example.com/blog" --feed-url "FEED_URL"
# OR Add (scrape):
blogwatcher-cli add "Blog Name" "https://example.com/blog" --scrape-selector "article a"
# Verify scan works:
blogwatcher-cli scan "Blog Name"
# Mark initial backlog as read (FOUND: N / NEW: N → all historical):
blogwatcher-cli read-all --blog "Blog Name" --yes
Commit config change:
cd ~/ai-topics && git add config/feeds/blogs.opml && git commit -m "config: add Blog Name RSS feed" && git push
(If author has existing wiki entity page) — Enrich the entity page:
search_files path=~/wiki/entities pattern="author-name" — if found, enrich the existing page## Monitoring
| Source | Method | Status |
|--------|--------|--------|
| RSS Feed | [feed-url](FEED_URL) | ✅ Active (added YYYY-MM-DD; RSS → blogwatcher → daily `blog-ingest` pipeline) |
| X/Twitter | [@handle](https://x.com/handle) | Tracked in `x-accounts.yaml` (if applicable) |
curl -s "FEED_URL" | head -30) for significant recent posts. If something substantial is found (major technical post, research result, framework release), add a relevant section to the entity page with key details. Don't add trivial posts.updated: date, add new sources, add any new tagsexecute_code + Python with open() prepend to avoid --- anchor trap — see wiki-graph-health skill §H5)cd ~/ai-topics && git add config/feeds/ wiki/ && git commit -m "config+wiki: add Blog Name RSS + entity enrichment" && git push/blog/rss.xml rather than root-level paths. The HTML <head> may not include a <link type="application/rss+xml"> tag even though the feed exists.sierra.ai/rss.xml) and OpenAI (developers.openai.com/rss.xml) both serve RSS from the domain root. Always check root-level paths (/rss.xml, /feed.xml) BEFORE checking blog subdirectories.builders.ramp.com, not ramp.com/blog). Always check the company's job postings, GitHub org, or search "company engineering blog" to discover these before concluding no blog exists.--scrape-selector, do NOT add it to the OPML. OPML requires xmlUrl. Track it only in the blogwatcher DB. Consider migrating to sitemap-based monitoring (see Sitemap-Based Monitoring section) for greater reliability.read-all --blog "Name" --yes to prevent the daily scan from re-processing hundreds of historical articles.lastmod dates, migrate to sitemap_monitor.py. Remove from blogwatcher DB, add to sitemap_monitor SOURCES list, and add to OPML Company group with type="sitemap".config/feeds/: Adding a blog is a config change, not a wiki change. Do not git add wiki/ unless you also made wiki changes this session.Corporate tech blogs (Anthropic Engineering, OpenAI Developers, Sierra, etc.) often have non-standard RSS setups. Follow this extended discovery workflow:
For single-blog discovery, use the workflow below. For batch discovery across dozens of URLs (e.g., when ingesting a curated company list), use the parallel curl approach in references/rss-batch-discovery.md.
<link> tags for application/rss+xml or application/atom+xml/blog/:
curl -sI "https://example.com/rss.xml" | head -1 # ← TRY THIS FIRST
curl -sI "https://example.com/feed.xml" | head -1
curl -sI "https://example.com/atom.xml" | head -1
curl -sI "https://example.com/index.xml" | head -1
curl -sI "https://example.com/blog/rss.xml" | head -1
curl -sI "https://example.com/blog/feed.xml" | head -1
lastmod dates:
curl -s "https://example.com/sitemap.xml" | grep -i "blog\|engineering"
--scrape-selector if no RSS exists.| Blog | RSS URL | Discovery Method |
|---|---|---|
| OpenAI Developers Blog | https://developers.openai.com/rss.xml | <link rel="alternate"> in HTML head (root-level, not /blog/rss.xml) |
| Sierra Blog | https://sierra.ai/rss.xml | Root-level trial-and-error (not advertised in HTML) |
| Anthropic Engineering | NONE | HTML scrape-selector (article a) — sitemap at anthropic.com/sitemap.xml has all /engineering/ URLs |
When no RSS feed exists, use blogwatcher-cli's --scrape-selector:
blogwatcher-cli add "Blog Name" "https://example.com/blog" --scrape-selector "article a"
Selector guidance:
article a) and verify with a scanblogwatcher-cli scan "Blog Name"article a, a[href^="/blog/"], h2 a, .post-title aSuccessful examples:
article a → Anthropic Engineering (found 27 articles)article h2 a → typical blog listing patternAfter adding a new blog, the first scan finds ALL historical articles. Mark them as read so future scans only highlight new content:
blogwatcher-cli read-all --blog "Blog Name" --yes
For high-value corporate blogs without RSS (e.g., Anthropic Engineering), sitemap monitoring is more reliable than HTML scraping. The sitemap_monitor.py script handles this automatically.
Script: ~/ai-topics/scripts/sitemap_monitor.py (source of truth) → ~/.hermes/scripts/sitemap_monitor.py (cron execution copy)
How it works:
sitemap.xml/engineering/)~/.hermes/processed_{name}.jsonwiki/raw/articles/~/.hermes/cron/data/sitemap_monitor/latest.jsonCron integration: sitemap-monitor job (ID: c7890b6e0e19) runs daily at 0 6 * * * UTC (JST 15:00) with no_agent=True. Raw articles are later picked up by the dreaming pipeline (18:00 UTC).
Single script, multiple companies: The SOURCES list supports unlimited companies in one script. Each source gets its own state_file for independent deduplication. No per-company script needed.
SOURCE config template (each company gets one entry):
{
"name": "Company Blog", # Display name for logging
"url": "https://company.com/blog", # Blog homepage
"sitemap_url": "https://company.com/sitemap.xml", # Sitemap URL
"url_pattern": "/blog/", # URL filter (e.g. "/blog/", "/engineering/", "/news/")
"file_prefix": "company-name", # Used in raw article filenames: {date}_{prefix}_{slug}.md
"title_suffixes": [" | Company", " - Company"], # Patterns to strip from <title> tags
"state_file": os.path.expanduser("~/.hermes/processed_company_blog.json"),
}
Field notes:
file_prefix — required. Prevents filename collisions between sources.title_suffixes — optional but recommended. Many corporate blogs append " | Company Name" to <title>. Stripping these keeps downloaded article titles clean.url_pattern — must match blog post URLs exactly. If the blog is at /blog/post-slug, use /blog/. If at /news/post-slug, use /news/. Too broad a pattern (e.g., /) matches every page on the site.state_file — one per source, unique paths. Two sources sharing one file corrupt dedup.Adding a new sitemap-based source:
sitemap_monitor.py — add an entry to the SOURCES list following the template above. Verify url_pattern by checking the sitemap.type="sitemap"~/.hermes/scripts/cd ~/ai-topics && git add config/feeds/ scripts/ && git commit && git pushWhen ingesting a large list of companies (e.g., from Paraform Talent Density Index, YC batches, startup rankings), prioritise by tier:
Tier 1 — AI core companies (monitor with RSS or sitemap immediately): Frontier labs, AI infrastructure, coding agents, embedding/voice models. These blogs produce content directly relevant to the wiki domain.
Tier 2 — Important but lower AI density: Autonomous vehicles, defense AI, legal AI, recruiting AI, dev-tool-adjacent. Add to sitemap monitor in the next batch. Worth monitoring but yield is lower.
Tier 3 — Non-AI companies on the list: E-commerce, wholesale marketplaces, billing platforms, generic analytics. Skip or add scrape-selector if they occasionally publish AI content.
Tier X — No blog found:
Some companies simply don't have a tech blog. Mark as no_blog in the entity page and move on.
Batch ingestion workflow:
references/rss-batch-discovery.md on all blog URLs → get RSS found/not-found splitblogwatcher-cli add commands)sitemap_monitor.py SOURCEStype="sitemap"# 1. Remove from blogwatcher DB
blogwatcher-cli remove "Anthropic Engineering" --yes
# 2. Add to sitemap_monitor.py SOURCES list
# 3. Add to OPML Company group with type="sitemap"
# 4. Run sitemap_monitor.py to seed the state file
/opt/data/.hermes/venv/bin/python sitemap_monitor.py
This avoids the fragility of CSS selectors breaking on site redesigns.
When a reference article or newsletter recommends blogs to add, follow this workflow to keep both the blogwatcher DB and the OPML in sync.
web_extract or curl./opt/data/bin/blogwatcher-cli blogs — produce a list of missing blogs./opt/data/bin/blogwatcher-cli add "Blog Name" "https://example.com"
blogwatcher-cli auto-discovers RSS at scan time, but the DB stores feed_url=None until then.references/feed-discovery-patterns.md for platform-specific patterns.
# Quick check: look for <link> tags
curl -sL https://example.com | grep -iE '(rss|atom|feed)' | grep -oP 'href="[^"]*\.(xml|rss|atom)"'
~/ai-topics/config/feeds/blogs.opml with proper <outline> entries including the discovered xmlUrl. Use patch to insert entries before the closing </outline> tag:
<outline type="rss" text="Blog Name" title="Blog Name" xmlUrl="https://example.com/feed.xml" htmlUrl="https://example.com/" />
python3 -c "import xml.etree.ElementTree as ET; ET.parse('~/ai-topics/config/feeds/blogs.opml')"
cd ~/ai-topics && git add config/feeds/blogs.opml && git commit -m "wiki: add N blogs from <source>" && git push
feed_url=None until the next scan. The OPML should still have the explicit xmlUrl to serve as the source of truth.feeds/posts/default) rather than the blog name. The ?alt=rss query param also works.https://name.substack.com/feed and https://name.substack.com/feed.xml.--scrape-selector if the site has a predictable HTML structure.See references/feed-discovery-patterns.md for detailed platform feed URL patterns and the full ClickHouse curated list feed table.
Common issues when scanning feeds:
www. prefix or different path).curl -sL <blog_url> and looking for <link type="application/rss+xml"> or <link type="application/atom+xml"> tags.Quarto blogs with feed: true in _quarto.yml generate a <link rel="alternate" type="application/rss+xml" href="index.xml"> tag in the HTML <head>. However, the actual index.xml file may not exist on the deployed gh-pages branch — the Quarto build sometimes omits it even though it is configured.
Key insight: This is a false positive, not a regular broken feed. The HTML actively advertises an RSS feed that was never produced by the build.
Detection workflow:
curl -sI https://blog.example.com/index.xml | head -5 — if 404, the feed does not exist despite the HTML <link> tag.curl -sL "https://api.github.com/repos/owner/repo/git/trees/gh-pages?recursive=1" | python3 -c "import sys,json; [print(t['path']) for t in json.load(sys.stdin).get('tree',[]) if 'xml' in t['path'].lower()]" — if index.xml is absent from the tree, the build never produced it._quarto.yml listing section with feed: true + URL base path.Action: If confirmed broken, consider:
--scrape-selector fallback if the blog has predictable HTML structureblogwatcher-cli maintains its own SQLite database at ~/.blogwatcher-cli/blogwatcher-cli.db. This is separate from the OPML file. Two critical implications:
blogwatcher-cli import is additive.If a feed fails in the daily RSS scan, check the blogwatcher-cli DB, not just the OPML:
# Check if a blog is in the DB
blogwatcher-cli blogs | grep "failing-blog.com"
# Remove from DB
blogwatcher-cli remove "failing-blog.com" --yes
Sometimes blogwatcher-cli blogs or blogwatcher-cli articles return empty or garbled output (encoding issues, terminal rendering problems). Fall back to direct SQLite queries:
# List all tracked blogs with last scan date
sqlite3 ~/.blogwatcher-cli/blogwatcher-cli.db \
"SELECT name, url, feed_url, last_scanned FROM blogs ORDER BY name;"
# List articles for a specific blog (newest first)
sqlite3 ~/.blogwatcher-cli/blogwatcher-cli.db \
"SELECT a.title, a.url, a.published_date, a.is_read FROM articles a JOIN blogs b ON a.blog_id = b.id WHERE b.name = 'blogname.com' ORDER BY a.published_date DESC LIMIT 20;"
# Count articles per blog
sqlite3 ~/.blogwatcher-cli/blogwatcher-cli.db \
"SELECT b.name, COUNT(a.id) as article_count FROM blogs b LEFT JOIN articles a ON b.id = a.blog_id GROUP BY b.name ORDER BY article_count DESC;"
# Find unread article count
sqlite3 ~/.blogwatcher-cli/blogwatcher-cli.db \
"SELECT COUNT(*) FROM articles WHERE is_read = 0;"
# Check DB schema
sqlite3 ~/.blogwatcher-cli/blogwatcher-cli.db ".schema"
Tables: blogs (id, name, url, feed_url, scrape_selector, last_scanned), articles (id, blog_id, title, url, published_date, discovered_date, is_read, categories), schema_migrations.
# 1. Remove the broken entry from the DB (not just OPML)
blogwatcher-cli remove "broken-blog.com" --yes
# 2. Verify the correct feed URL
curl -sL -o /dev/null -w "%{http_code}" "https://blog.com/feed.xml"
# 3. Re-add with correct URL
blogwatcher-cli add "broken-blog.com" "https://blog.com" --feed-url "https://blog.com/correct-feed.xml"
When a tracked blog's content is rarely or never relevant to the wiki domain (e.g., no AI/LLM content from a systems-programming blog), exclude it from RSS monitoring while preserving any wiki entity pages. This is distinct from broken-feed removal — the feed works, but the signal-to-noise ratio is too low.
Verify the blog is in OPML and DB: grep -n "blog-name" ~/ai-topics/config/feeds/blogs.opml and /opt/data/bin/blogwatcher-cli blogs | grep "blog-name"
Remove from OPML: Use patch to delete the <outline> line. After removal, validate:
python3 -c "import xml.etree.ElementTree as ET; ET.parse('/opt/data/ai-topics/config/feeds/blogs.opml'); print('OPML OK')"
Remove from blogwatcher DB (always do both — OPML and DB are independent):
/opt/data/bin/blogwatcher-cli remove "Blog Name" --yes
Check for entity page: search_files path=~/wiki/entities pattern="author-or-domain-name"
Update entity page (if one exists):
## Monitoring
| Source | Method | Status |
|--------|--------|--------|
| RSS Feed | — | ❌ Excluded (YYYY-MM-DD) — AI content is rare; RSS removed from blogwatcher + OPML to reduce noise |
updated: date in frontmatterUpdate wiki/index.md: Append exclusion note to the entity's index entry (e.g., RSS excluded (YYYY-MM-DD, AI content rare))
Update wiki/log.md: Prepend a log entry documenting what was removed and why. Use python3 prepend to avoid --- frontmatter anchor traps:
python3 -c "
with open('/opt/data/wiki/log.md') as f: content = f.read()
entry = '## [YYYY-MM-DD] config | RSS exclusion — Name\n\n...\n\n'
with open('/opt/data/wiki/log.md', 'w') as f: f.write(entry + content)
"
Commit and push:
cd ~/ai-topics && git add config/feeds/blogs.opml wiki/ && git commit -m "config: exclude Blog Name from RSS monitoring (off-domain content)" && git push
OPML and blogwatcher DB are independent — removing from one does NOT remove from the other. Always do both steps.
Don't delete entity pages — the exclusion is at the RSS level only. The entity page records that the person/org exists and why they're excluded.
Don't use blogwatcher-cli read-all before removing — since the blog is being removed entirely, there's no need to mark old articles as read.
Exclusion is reversible — if the author later starts writing AI content, re-add by following the Check-and-Add workflow. The entity page's Monitoring section documents the exclusion reason and date, making it easy to revisit.
Commit includes wiki/ when entity/index/log were updated, unlike simple OPML-only config commits.
Auto-discovers RSS/Atom feeds from blog homepages when no --feed-url is provided.
Falls back to HTML scraping if RSS fails and --scrape-selector is configured.
Categories from RSS/Atom feeds are stored and can be used to filter articles.
Import blogs in bulk from OPML files exported by Feedly, Inoreader, NewsBlur, etc.
Database stored at ~/.blogwatcher-cli/blogwatcher-cli.db by default (override with --db or BLOGWATCHER_DB).
Use blogwatcher-cli <command> --help to discover all flags and options.
blogwatcher-cli does NOT follow HTTP redirects — this is the #1 cause of scan failures when importing from OPML files with outdated URLs.