GitHub Releases currently provide macOS and Linux archives only.
- Download the archive for your platform from GitHub Releases, for example
news-briefing_<tag>_<os>_<arch>.tar.gz. - Extract it into a working directory.
- Copy
configs/config.example.yamltoconfigs/config.yaml. - Fill in
configs/config.yamlfor your sources and AI CLI. - Run
./news-briefing --helpfrom that working directory. - Add
.envonly if you plan to use email sending.
Note: the binary currently reads configs/config.yaml and .env from the current working directory. Adding the binary to PATH does not remove that requirement.
Requires Go 1.25 or newer.
cp configs/config.example.yaml configs/config.yaml
./build.sh
./news-briefing --helpBefore running non-help commands such as run, fill in configs/config.yaml for your sources and AI CLI. Add .env when email sending is enabled.
At the moment, you only need to put the email auth code there:
EMAIL_SMTP_AUTH_CODE=your-email-auth-codeRules:
- keep only sensitive values in
.env - keep structured configuration in YAML
Example fetch config:
fetch:
timeout: 30s
retry_times: 3
retry_wait_time: 200msNotes:
timeout: HTTP fetch timeoutretry_times: total fetch attempts for news sources and Watch pagesretry_wait_time: wait duration between failed fetch attempts
Example email delivery config:
email:
smtp_host: smtp.example.com
smtp_port: 465
from: from@example.com
to: to@example.com
timeout: 3s
retry_times: 3
retry_wait_time: 500ms
use_proxy: falseNotes:
timeout: SMTP connect/send timeoutretry_times: total email send attemptsretry_wait_time: wait duration between email send retriesuse_proxy: whether email sending should use the configured SOCKS5 proxy
email.* only affects email delivery; source fetching and Watch pages use the fetch.* settings above.
Repository config layout:
configs/config.example.yaml: template config committed to gitconfigs/config.yaml: real local config, not committed to git
Initialization:
cp configs/config.example.yaml configs/config.yamlThen edit configs/config.yaml and fill in:
- email addresses
- sources and keywords
- scheduled trigger times in
schedule - output directory in
output.dir - proxy settings
- AI CLI command and batch flags (default direct
codex execwith isolated non-interactive flags)
Example source categories:
sources:
- name: Example AI Feed
url: https://example.com/ai.rss
type: rss
category: AI/科技
- name: Example Open Source Feed
url: https://example.com/open-source.rss
type: rss
category: 开源工具
- name: Example Startup Feed
url: https://example.com/startups.rss
type: rss
category: 商业/公司
- name: Example World Feed
url: https://example.com/world.rss
type: rss
category: 国际政治
- name: Protected RSSHub Feed
url: https://rsshub.example.com/example/route
type: rss
category: 新闻财经
rsshub_access_key_env: RSSHUB_ACCESS_KEYNotes:
categorycan be any string- grouped output follows the first-appearance order of
sources - if a runtime category is not present in config, it is appended after configured categories
- remote RSSHub sources must use HTTPS; with
rsshub_access_key_env, the master key stays in.envand requests carry only a route-derived access code
The program reads only configs/config.yaml by default.
Make sure the AI CLI configured in configs/config.yaml is available and already logged in. This project runs large models through a locally logged-in AI CLI and does not require storing API keys inside this repository. Default template example:
ai:
command: codex
args:
- exec
- --ignore-user-config
- --ephemeral
- --skip-git-repo-check
- --sandbox
- read-only
- --color
- never
- --disable
- apps
- --disable
- plugins
- --disable
- remote_plugin
models:
default: gpt-5.6-sol
translation: gpt-5.3-codex-spark
append_system_prompt: trueThe runner sends each prompt to codex exec over stdin and appends the - input marker automatically. Do not put -p, --model, or the trailing - into args: Codex uses -p for config profiles, while the runner selects models.default for summaries and deep dives and models.translation for translation. When append_system_prompt is enabled, the runner maps the batch-only instructions to a per-run developer_instructions override.
--ignore-user-config and the three feature disables keep this unattended batch job isolated from personal MCP servers, apps, and plugins. They do not modify the user's ~/.codex/config.toml or affect Codex App features such as Computer Use.
output:
dir: output
mode: translated_only
include_filtered_articles: falseAllowed mode values:
original_only— show only the original/raw article blocktranslated_only— show only the Chinese AI-generated blockbilingual_translated_first— show Chinese first, then the original blockbilingual_original_first— show the original block first, then the Chinese block
include_filtered_articles defaults to false. When set to true, run, regen, and serve append in-window candidates that did not match any keyword to the end of the briefing as a Chinese-translated appendix. These candidates are not included in the AI summary and are not written to the seen state.
This setting affects:
runregenserve(it only reuses the briefing output pipeline; it does not add a server endpoint)fetch --zhdeep
Exception:
- plain
./news-briefing fetchstill prints the raw article list directly and does not useoutput.mode
Build first:
./build.shCommon commands:
./news-briefing --help
./news-briefing run
./news-briefing run --raw
./news-briefing run --no-email
./news-briefing regen --from "2026-03-18 08:00" --to "2026-03-18 14:00"
./news-briefing regen --from "2026-03-18 08:00" --to "2026-03-18 14:00" --period 1400 --ignore-seen --send-email
./news-briefing fetch
./news-briefing fetch --zh
./news-briefing deep "OpenAI"
./news-briefing deep "Claude" --ignore-seen
./news-briefing resend-md --file output/26.04.13-晚间-1800.md
./news-briefing serveGenerate a regular briefing:
- fetch news
- generate a Chinese summary and 3 follow-up directions worth tracking
- print to terminal
- write Markdown
- send email by default
Optional flags:
--raw: also print the raw article list--no-email: skip email delivery
Regenerate a briefing for an explicit time window. Useful for backfilling, reruns, and resending:
./news-briefing regen --from "2026-03-18 08:00" --to "2026-03-18 14:00"
./news-briefing regen --from "2026-03-18 08:00" --to "2026-03-18 14:00" --ignore-seen
./news-briefing regen --from "2026-03-18 08:00" --to "2026-03-18 14:00" --period 1400 --ignore-seen --send-emailRules:
--from/--toare required and parsed inschedule_timezone; ifschedule_timezoneis not configured, the system local timezone is used. Format:YYYY-MM-DD HH:MM--tomust be later than or equal to--from--periodis optional; when omitted, it defaults to theHHMMof--to--ignore-seenskips the persisted seen-state filter and only keeps in-batch deduplication--send-emailis off by default and only sends mail when explicitly passedregenstill writes a Markdown file by default
Fetch news without generating a summary:
./news-briefing fetch: print the raw article list./news-briefing fetch --zh: additionally call the configured AI CLI to output Chinese translation
Generate a topic deep-dive pack around a keyword or company. The deep commands emitted by the daily briefing also include --ignore-seen by default so historical seen-state does not hide useful follow-up items too early:
./news-briefing deep "OpenAI"
./news-briefing deep "Claude" --ignore-seen
./news-briefing deep "Claude" --from "2026-03-28 00:00" --to "2026-03-29 23:59"
./news-briefing deep "Claude" --from "2026-03-28 00:00" --to "2026-03-29 23:59" --ignore-seenRules:
--from/--toare optional and parsed inschedule_timezone; when omitted, the system local timezone is used. Format:YYYY-MM-DD HH:MM--from/--tomust be omitted together or provided together--tomust be later than or equal to--from- when
--from/--toare omitted,deepreads from the unread pool; if only--ignore-seenis passed, it uses the most recent 12-hour window --ignore-seenskips the persisted seen-state filter and only keeps in-batch deduplication
Resend email from an existing Markdown file without fetching news again and without regenerating AI output:
./news-briefing resend-md --file output/26.04.13-晚间-1800.mdRules:
--fileis required- the file must be a
.mdfile - the file path must stay under
output.dir - the recipient uses the current
email.to - the email subject is derived from the Markdown filename; for example,
26.04.13-晚间-1800.mdbecomes国际资讯简报 26.04.13 晚间 18:00
Daemon mode. Runs automatically based on schedule in configs/config.yaml and uses the same output pipeline as run.
Current example template:
schedule:
- "0 8 * * *"
- "0 18 * * *"
schedule_delay: 10mThe scheduled fetch window is derived by taking the current trigger time and walking back to the previous planned time point in the current schedule. schedule_delay: 10m anchors actual execution at around 08:10 / 18:10; if cron fires late, the scheduler waits only for the remaining time and runs immediately once that target has passed. Window boundaries remain anchored at 08:00 / 18:00 so upstream local fetching can finish writing data first.
When the service actually observes an 08:00 / 18:00 trigger, it records that window in the single long-lived state file output/state/briefing-scheduler.json. A watcher exists only for a recorded pending, waiting_x, or running window; it checks once per minute by default and stops immediately at done / failed. If X is still running for the same window with a fresh heartbeat, the window moves to waiting_x. The watcher takes over when X reaches a terminal state or its heartbeat is more than three minutes stale. The briefing run also refreshes its heartbeat every minute and can be recovered after three stale minutes following a restart. Cron, the X callback, and the watcher atomically contend for the same per-window lease through a short-lived file lock, and an old lease cannot overwrite a takeover. The same record stores confirmed email delivery time so recovery does not resend an already confirmed message. The transient .lock and atomic-write temp file exist only while updating state; they are not persistent markers.
Note: serve restores only unfinished windows already present in that state file. It does not infer or backfill a trigger that the service missed entirely. For example, if it is stopped at 07:50 and starts at 08:01, it will not invent the 08:00 window; use regen manually when needed.
Recommendation: after changing cron / schedule, if you suspect a gap, use the built-in regen --from --to command to backfill that window manually, for example:
./news-briefing regen --from "2026-03-18 08:00" --to "2026-03-18 14:00"If you only move a future time point later or otherwise adjust a not-yet-triggered slot, you usually will not create a gap, but it is still worth checking the generated output on the day of the change.
With the default output.dir=output:
- briefing Markdown:
output/26.03.18-午间-1400.md - deep-dive material:
output/deep/26.03.18-topic.md - seen state:
output/state/seen.json
Example email subjects:
- regular briefing email:
[资讯简报] 26.03.18 午间 14:00 resend-mdemail:国际资讯简报 26.03.18 午间 14:00
./start.sh
./stop.sh
./restart.shWhen using macOS launchd to supervise the service, use these helpers instead. They assume ~/Library/LaunchAgents/com.news-briefing.briefing.plist and the Label com.news-briefing.briefing:
./launch-start.sh
./launch-stop.sh
./launch-restart.sh
./launch-status.shlaunchd can supervise a long-running ./news-briefing serve process. The actual trigger times still come from schedule in configs/config.yaml.
Create ~/Library/LaunchAgents/com.news-briefing.briefing.plist and have it execute ./news-briefing serve from this repository. Because the program reads configs/config.yaml and .env from the current working directory, set WorkingDirectory explicitly in the plist.
Minimal example:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.news-briefing.briefing</string>
<key>WorkingDirectory</key>
<string>/absolute/path/to/news-briefing</string>
<key>ProgramArguments</key>
<array>
<string>/absolute/path/to/news-briefing/news-briefing</string>
<string>serve</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/absolute/path/to/news-briefing/logs/out.log</string>
<key>StandardErrorPath</key>
<string>/absolute/path/to/news-briefing/logs/err.log</string>
</dict>
</plist>Common commands:
mkdir -p logs
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.news-briefing.briefing.plist 2>/dev/null || true
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.news-briefing.briefing.plist
launchctl kickstart -k gui/$(id -u)/com.news-briefing.briefing
launchctl print gui/$(id -u)/com.news-briefing.briefingIf macOS repeatedly asks for Automation, Accessibility, or browser-control permissions after rebuilding ./news-briefing, sign the local binary with a stable local Code Signing certificate.
This is optional and local-only. The repository does not include certificates, private keys, personal Keychain paths, or signing secrets, and public release artifacts are unchanged.
- Open Keychain Access.
- Choose Certificate Assistant > Create a Certificate.
- Name it, for example,
News Briefing Local Code Signing. - Set Certificate Type to
Code Signingand store it in the login keychain. - Trust the certificate for code signing if macOS asks.
- Build with the local identity:
NEWS_BRIEFING_CODESIGN_IDENTITY="News Briefing Local Code Signing" ./build.shTo make local signing mandatory for your scheduled setup:
NEWS_BRIEFING_CODESIGN_IDENTITY="News Briefing Local Code Signing" \
NEWS_BRIEFING_CODESIGN_REQUIRED=1 \
./build.shAfter the first signed build, restart launchd and approve the macOS permission prompts once:
./launch-restart.shStop and unload:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.news-briefing.briefing.plistgo test ./...
./build.shLicensed under the MIT License. See LICENSE.