leiting-eric/daily-brief
Operational knowledge for the daily-brief digest pipeline (this project). RSS/API fetchers, pluggable LLM enrichment (default claude CLI on Max; also anthropic/openai/deepseek/minimax API), trading section, HTML rendering, cross-platform scheduler integration (Windows Task Scheduler / macOS launchd / Linux cron). Load when the user asks about running daily / regenerating sections / debugging a failed run / adding or disabling sources / LLM quota / scheduler / why a tab shows wrong data / why a source failed / switching LLM backend. Always prefer the documented npm commands over re-implementing logic. Diagnose by reading logs/daily-*.log first, then logs/llm-calls.jsonl for LLM-side issues.
npx skills add https://github.com/leiting-eric/DailyBrief --skill daily-brief
This project generates a single-page HTML daily digest covering tech / finance / politics / market data / community discussion. The pipeline runs locally via the OS scheduler (Windows Task Scheduler / macOS launchd / Linux cron, default 08:00 local time) and emits daily_reports/<YYYY-MM-DD>/<YYYY-MM-DD>.html + sidecar files (each date gets its own subdir). The date label uses the system local timezone by default — set REPORT_TZ (e.g. Asia/Shanghai, UTC) in .env.local to override.
Detailed architecture lives in code; this skill is a cheat sheet for operating and diagnosing, not a re-explanation of the system.
All paths in this skill are relative to the project root (the directory that contains package.json, lib/, scripts/).
Before any command, ensure the working directory is the project root. Two cases:
cd: # Cross-platform Node one-liner (prints the project root path):
node -e "const fs=require('fs'),os=require('os'),path=require('path');const cfg=path.join(os.homedir(),'.daily-brief-config');if(fs.existsSync(cfg))console.log(fs.readFileSync(cfg,'utf8').trim());else process.exit(1)"
Use the printed path: cd "$(...)" on bash / Set-Location (...) in PowerShell.
The config file is written by node scripts/install.mjs --global. If it's missing the user hasn't done a global install — tell them to run it.
| Need | Command | Cost |
|---|---|---|
| Full pipeline | npm run daily | ~5-8 min, ~6 Sonnet calls |
| Fetch sanity only (no LLM) | npm run dry-run | ~30s |
| Re-render existing sidecar | npm run render [date] | <1s |
| Re-run trading section | npm run regen-trading [date] | ~2 min, 1 LLM call |
| Top-up missing summary | npm run regen-enrich <cat:sub> [date] | ~20-40s, 1 LLM call |
| Open today's report in Chrome | npm run open | instant |
| Sonnet quota + call history | npm run quota-report | instant |
[date] defaults to today's date in the report timezone (system local, or REPORT_TZ if set). The pipeline and the OS scheduler both run in local time, so the report's date label = the date when the trigger fired in the report timezone. A user with REPORT_TZ=Asia/Shanghai whose machine fires the trigger at 23:00 UTC-8 will get a "next-day Shanghai" file, e.g. daily_reports/2026-05-17/2026-05-17.html.
<cat:sub> accepted by regen-enrich: finance:news, politics:world, tech:ai-news. Single-source X 推文 (tech:x-viral) is enriched as part of daily only — no top-up path.
| Task | File |
|---|---|
| Add / disable / re-categorize a source | sources.config.json (project root — single source of truth; lib/sources/registry.ts is just a loader) |
| Rename L1 tab labels | lib/output/render.ts CATEGORY_LABELS |
| Reorder / rename L2 subcategories | SUBCATEGORY_ORDER + SUBCATEGORY_LABELS in same file |
| Change per-source item cap | SOURCE_DISPLAY_LIMITS |
| Change merged-timeline cap | MERGED_SUBGROUP_LIMITS |
| Add a Sonnet enrichment prompt | lib/ai/enrich.ts — copy XVIRAL_SYSTEM_PROMPT pattern |
| Wire an enrichment into pipeline | scripts/daily.ts — await enrichXxx(articles) in main() |
| Add a new fetcher type | New file in lib/sources/ + branch in lib/sources/dispatch.ts |
| Adjust HTML styling | inline <style> block in renderHtml() in lib/output/render.ts |
| Change scheduler trigger time | node scripts/install.mjs --at HH:MM (re-registers) |
| Wrapper script the scheduler invokes | scripts/run-daily.mjs |
lang: "zh" in registry skip enrichment (already Chinese).summary.Order matters — top-to-bottom:
Get-ScheduledTaskInfo -TaskName DailyBrief → LastRunTime + LastTaskResult (0=success, 267009=running, else failed)launchctl list | grep com.daily-brief (PID column + last exit code)logs/cron.log node -e "const fs=require('fs'),d=new Date(),pad=n=>String(n).padStart(2,'0');console.log(fs.readFileSync('logs/daily-'+d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+'.log','utf8').split('\n').slice(-40).join('\n'))"
ls daily_reports/<date>/ (any platform) or Get-ChildItem daily_reports\<date>\ (Windows)<id> <count> or <id> FAILED — <reason>lib/sources/<source>.tsdaily.ts)npm run quota-report — per-backend summary; for claude-cli shows 5h window, for API backends shows 24h spendingclaude-cli: wait or temporarily switch via .env.local (LLM_BACKEND=openai etc.)npm run regen-enrich <cat:sub>logs/llm-calls.jsonl (legacy claude-calls.jsonl still read for backwards-compat) — grep "success":false, see errorCategory (quota / timeout / auth / other)LLM_BACKEND env in .env.local; not set → claude-clinpm run render (1 second) — often fixes display-only bugsrenderRawCategoryPanel / renderSubContent chain in render.ts is where panel structure livesenabled: false depending on current IP repnpm run daily stdout+stderr to logs/daily-<local-date>.log via stream pipes (real-time, not buffered)npm run daily is propagated to the OS schedulernpm run open detached so Chrome opens without blocking.mjs file works on Windows / macOS / Linuxlang=en param is best-effort; some slip throughlib/sources/attentionvc.ts isEnglish() — checks langsDetected (most reliable) + lang === "zxx" (image/code-only, kept)data-cat="tech" selector. Tech main panel AND community panel both had sub-content with data-cat="tech", so clicking AI 媒体 in tech panel deactivated cn-community in community panelbtn.closest('.panel') + btn.closest('.sub-content'). If regression, look at inline <script> block at end of renderHtml()lib/ai/trading-commentary.ts with stronger promptSources live in sources.config.json at the project root. lib/sources/registry.ts only loads + validates that JSON at module-init; never hardcode sources in TS.
id, name, type (rss/api/scrape), url, category, subcategory?, enabled?, useCurl?, lang?, locales?useCurl: true for sources behind Cloudflare-style TLS-fingerprint blockslang: "zh" (or "en") for sources already in a specific language — enrich skips them when REPORT_LOCALE matcheslocales: ["zh"|"en"] filters which REPORT_LOCALE keeps the source. Omit → bothenabled: false + a notes field explaining why — don't deletenpm run sources to see the table by category with current enable/filter statusL1 tabs in order: tech / trading / politics / finance / community
技术动态 (tech)
L2: GitHub Trending (per-source, cap 20)
L2: X 推文 (single source attentionvc-ai, cap 20, preserve fetch order)
L2: AI 媒体 (merged 7 RSS sources, cap 15, summary)
市场行情 (trading)
asset-group tabs: macro / 美股 / 加密 / 中港 / 商品外汇
时政观察 (politics:world)
merged single timeline, cap 15, summary, sports filtered
财经要点 (finance:news)
merged single timeline, cap 12, summary
社区讨论 (community)
Source tabs: V2EX / LinuxDo (cap 10 each)
Note: cn-community is registered under category=tech but rendered as its
own L1 panel — see TECH_MAIN_SUBS vs TECH_COMMUNITY_SUBS in render.ts
scripts/install.mjs registers the daily trigger via the OS-native scheduler:
| OS | Mechanism | Wake-from-sleep |
|---|---|---|
| Windows | Task Scheduler "DailyBrief" (WakeToRun, AllowStartIfOnBatteries, StartWhenAvailable) + power-plan wake timers | ✓ wakes laptop |
| macOS | launchd plist ~/Library/LaunchAgents/com.daily-brief.plist | ✗ doesn't wake; configure pmset separately if needed |
| Linux | crontab entry tagged # daily-brief | ✗ cron doesn't fire while suspended — run skipped |
Common:
--at HH:MM to change)node scripts/install.mjs [--at HH:MM] [--global]node scripts/uninstall.mjsGet-ScheduledTask DailyBrief | fl or taskschd.msc GUIconsole.log debugging that won't survive — use structured logger or write to logs/process.exit(1) deep in a fetcher; let daily.ts's per-source try/catch handle itrunLlm (lib/ai/llm.ts) by importing a specific backend directly — that defeats the LLM_BACKEND switch and pins call sites to one providerclaude-cli to keep using it. Switching to API costs them money<date>.json and <date>.md for archiveTake leiting-eric/daily-brief from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.