Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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 s
.claude/skills/leiting-eric-daily-brief/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 105% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 159% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 149% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 112% | 0% |
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:bash # 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.logbash 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.tsscripts/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 archive| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 7,441 | 6,578 | -12% | 1 | 1 | 0% | 353 | 3,708 | +950% | 0 | 0 | — |
case-02 | fail→pass | 12,030 | 4,764 | -60% | 1 | 1 | 0% | 2,021 | 4,148 | +105% | 0 | 0 | — |
case-03 | fail→fail | 19,236 | 4,761 | -75% | 1 | 1 | 0% | 1,302 | 3,600 | +176% | 0 | 0 | — |
case-04 | fail→pass | 8,967 | 2,353 | -74% | 1 | 1 | 0% | 1,492 | 3,693 | +148% | 0 | 0 | — |
case-05 | fail→pass | 8,483 | 2,217 | -74% | 1 | 1 | 0% | 1,383 | 3,583 | +159% | 0 | 0 | — |
case-06 | pass→pass | 7,117 | 2,030 | -71% | 1 | 1 | 0% | 1,073 | 3,605 | +236% | 0 | 0 | — |
case-07 | fail→pass | 10,365 | 2,977 | -71% | 1 | 1 | 0% | 1,536 | 3,829 | +149% | 0 | 0 | — |
case-08 | fail→pass | 11,085 | 3,618 | -67% | 1 | 1 | 0% | 1,843 | 3,900 | +112% | 0 | 0 | — |
case-09 | fail→pass | 11,140 | 2,369 | -79% | 1 | 1 | 0% | 1,762 | 3,733 | +112% | 0 | 0 | — |
case-10 | fail→pass | 11,255 | 2,619 | -77% | 1 | 1 | 0% | 1,682 | 3,673 | +118% | 0 | 0 | — |
case-11 | fail→pass | 11,299 | 3,212 | -72% | 1 | 1 | 0% | 1,813 | 3,841 | +112% | 0 | 0 | — |
case-12 | pass→pass | 15,431 | 16,382 | +6% | 1 | 1 | 0% | 2,462 | 4,539 | +84% | 0 | 0 | — |
case-13 | fail→pass | 15,569 | 6,435 | -59% | 1 | 1 | 0% | 2,569 | 4,421 | +72% | 0 | 0 | — |
case-14 | fail→pass | 7,835 | 2,729 | -65% | 1 | 1 | 0% | 1,260 | 3,809 | +202% | 0 | 0 | — |
case-15 | fail→pass | 13,537 | 7,561 | -44% | 1 | 1 | 0% | 2,076 | 4,427 | +113% | 0 | 0 | — |
case-16 | fail→pass | 10,814 | 3,833 | -65% | 1 | 1 | 0% | 1,653 | 3,998 | +142% | 0 | 0 | — |
case-17 | fail→pass | 7,965 | 1,878 | -76% | 1 | 1 | 0% | 1,358 | 3,647 | +169% | 0 | 0 | — |
case-18 | fail→pass | 12,730 | 4,065 | -68% | 1 | 1 | 0% | 1,951 | 3,938 | +102% | 0 | 0 | — |
case-19 | fail→pass | 12,806 | 4,766 | -63% | 1 | 1 | 0% | 2,035 | 4,119 | +102% | 0 | 0 | — |
case-20 | pass→pass | 11,638 | 5,346 | -54% | 1 | 1 | 0% | 1,830 | 4,224 | +131% | 0 | 0 | — |
case-21 | pass→pass | 7,730 | 5,110 | -34% | 1 | 1 | 0% | 1,479 | 4,198 | +184% | 0 | 0 | — |
case-22 | pass→pass | 3,738 | 3,015 | -19% | 1 | 1 | 0% | 568 | 3,838 | +576% | 0 | 0 | — |
case-23 | pass→pass | 3,592 | 2,471 | -31% | 1 | 1 | 0% | 535 | 3,667 | +585% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +65 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.