[teamai] Push 1 resource(s) from root

This commit is contained in:
2026-09-10 16:51:06 +08:00
parent f3388c50e2
commit 100097e85d
10 changed files with 1168 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
root
+121
View File
@@ -0,0 +1,121 @@
---
name: ak-rss-digest
description: Curate a Chinese reading digest from a fixed bundle of RSS and Atom feeds, with a strong preference for AI agent thinking, frontier AI commentary, deep interviews, and non-boring high-signal essays. Use when Codex needs to pull the latest week's posts by default, or a specific day's posts when explicitly requested, summarize them, score each article on a 10-point scale, and output only the posts scoring above 7 in a concise Chinese daily-brief style.
---
# AK RSS Digest
## Overview
Use this skill to build a current reading list from the feed bundle in `references/feeds.opml`.
Default to the most recent 7 days ending on the current date in `Asia/Shanghai`, and narrow to a single day only when the user explicitly asks for it.
## Workflow
1. Run `python3 scripts/fetch_today_feed_items.py --format json` to collect entries from the configured feeds. This defaults to the most recent 7 days.
2. Treat feed-level network failures as non-fatal. Continue with the feeds that succeeded and mention major failures only when they materially reduce coverage.
3. Read the structured output and discard obvious mismatches before opening article pages. Reject items that are clearly raw research papers, release notes, changelogs, benchmark dumps, or narrowly technical implementation logs without broader implications.
4. Open the remaining candidate links when the feed summary is too thin to judge the article well. Skim for thesis, novelty, readability, and whether the piece offers strong perspective rather than just information.
5. Score every serious candidate on the rubric below. Output only items with a score strictly greater than `7.0`.
6. If nothing clears the threshold, say so directly instead of padding the output with mediocre picks.
## Selection Heuristics
Prefer articles with at least one of these traits:
- Fresh thinking about AI agents, agent tooling, agent UX, multi-agent workflows, evaluation, deployment, or failure modes.
- Strong interviews or conversations with operators, founders, researchers, or engineers who reveal how frontier work is actually being done.
- Essays that synthesize a new direction, new constraint, or strategic implication in AI, software, or adjacent technology.
- Pieces that are readable and idea-dense for a general technical audience, not just specialists in one subfield.
Penalize heavily or reject:
- Pure technical papers and paper summaries with little interpretive value.
- Vendor marketing, launch fluff, SEO writing, or obvious news rewrites.
- Narrow implementation diaries that do not connect to broader product, research, or ecosystem questions.
- Dry reference material that is correct but not worth a strong recommendation.
## Scoring Rubric
- `9-10`: Exceptional fit. Strong signal, strong writing, original insight, and clearly valuable for someone tracking AI agents or adjacent frontier shifts.
- `8-8.9`: Good recommendation. Worth reading, clear point of view, and relevant enough to the target taste profile.
- `7-7.9`: Borderline. Useful but not compelling enough for the final digest. Do not output it.
- `5-6.9`: Competent but dry, derivative, too narrow, or not aligned with the target taste profile.
- `<5`: Irrelevant, low-signal, or actively unsuitable.
When scoring, weigh these dimensions:
- Relevance to AI agents, frontier AI, deep operator insight, or adjacent strategic technology discussion.
- Originality of the article's argument or reporting.
- Readability and ability to hold attention.
- Practical usefulness for someone trying to keep up with meaningful new directions.
## Output Format
Write the final answer in Simplified Chinese.
For each article that scores above `7`, include exactly these elements with Chinese labels:
- `标题`: original article title.
- `评分`: `x/10`, use one decimal place when helpful.
- `推荐语`: one or two sentences explaining why this is worth reading.
- `摘要`: exactly two sentences summarizing the article.
- `链接`: canonical article URL.
Use a concise tone that reads like a curated daily brief, not a formal report:
- Prefer short, direct sentences over explanatory padding.
- Lead with why the article is worth the user's time.
- Keep each item compact and scannable.
- Avoid English field names such as `Title`, `Score`, or `Recommendation`.
Use this structure for the final answer:
```markdown
本期从最近一周的 RSS 里筛出几篇值得看的文章,重点偏 AI agent、前沿判断和不太枯燥的深度内容。
- 标题:文章标题
评分:8.7/10
推荐语:1-2 句话,先说为什么值得看。
摘要:严格两句话,讲清核心观点和价值。
链接:文章链接
```
If nothing qualifies, say so directly in Chinese, for example:
```markdown
这周没有筛到真正值得推荐的文章。现有更新要么偏技术细节,要么信息密度不够,没有过 7 分线。
```
## Resources
- `scripts/fetch_today_feed_items.py`
Use this script to fetch the configured feeds and return recent entries as structured JSON or Markdown.
- `references/feeds.opml`
Use this as the source of truth for the feed bundle. Keep the workflow anchored to this file unless the user explicitly asks to change the feed list.
## Command Examples
Fetch the latest week of entries in Shanghai time:
```bash
python3 scripts/fetch_today_feed_items.py --format json
```
Fetch a single day explicitly:
```bash
python3 scripts/fetch_today_feed_items.py --date 2026-03-17 --days 1 --timezone Asia/Shanghai --format json
```
Fetch the latest posts from the past week:
```bash
python3 scripts/fetch_today_feed_items.py --days 7 --limit 30 --format json
```
Inspect a quick Markdown view instead of JSON:
```bash
python3 scripts/fetch_today_feed_items.py --format markdown
```
+7
View File
@@ -0,0 +1,7 @@
interface:
display_name: "RSS Agent Digest"
short_description: "Daily RSS picks for AI agent readers"
default_prompt: "Use $ak-rss-digest to pull the latest week's configured RSS posts, score them, and return only the strongest recommendations in Chinese."
policy:
allow_implicit_invocation: true
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head><title>RSS Feeds</title></head>
<body>
<outline type="rss" text="simonwillison.net" title="simonwillison.net" xmlUrl="https://simonwillison.net/atom/everything/" htmlUrl="https://simonwillison.net"/>
<outline type="rss" text="daringfireball.net" title="daringfireball.net" xmlUrl="https://daringfireball.net/feeds/main" htmlUrl="https://daringfireball.net"/>
<outline type="rss" text="krebsonsecurity.com" title="krebsonsecurity.com" xmlUrl="https://krebsonsecurity.com/feed/" htmlUrl="https://krebsonsecurity.com"/>
<outline type="rss" text="antirez.com" title="antirez.com" xmlUrl="http://antirez.com/rss" htmlUrl="http://antirez.com"/>
<outline type="rss" text="jeffgeerling.com" title="jeffgeerling.com" xmlUrl="https://www.jeffgeerling.com/blog.xml" htmlUrl="https://jeffgeerling.com"/>
<outline type="rss" text="GitHub Weekly" title="GitHub Weekly" xmlUrl="https://itcoffee66.github.io/githubweekly/feed.xml" htmlUrl="https://itcoffee66.github.io/githubweekly/"/>
</body>
</opml>
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>Blog Feeds</title>
</head>
<body>
<outline text="Blogs" title="Blogs">
<outline type="rss" text="simonwillison.net" title="simonwillison.net" xmlUrl="https://simonwillison.net/atom/everything/" htmlUrl="https://simonwillison.net"/>
<outline type="rss" text="jeffgeerling.com" title="jeffgeerling.com" xmlUrl="https://www.jeffgeerling.com/blog.xml" htmlUrl="https://jeffgeerling.com"/>
<outline type="rss" text="seangoedecke.com" title="seangoedecke.com" xmlUrl="https://www.seangoedecke.com/rss.xml" htmlUrl="https://seangoedecke.com"/>
<outline type="rss" text="krebsonsecurity.com" title="krebsonsecurity.com" xmlUrl="https://krebsonsecurity.com/feed/" htmlUrl="https://krebsonsecurity.com"/>
<outline type="rss" text="daringfireball.net" title="daringfireball.net" xmlUrl="https://daringfireball.net/feeds/main" htmlUrl="https://daringfireball.net"/>
<outline type="rss" text="ericmigi.com" title="ericmigi.com" xmlUrl="https://ericmigi.com/rss.xml" htmlUrl="https://ericmigi.com"/>
<outline type="rss" text="antirez.com" title="antirez.com" xmlUrl="http://antirez.com/rss" htmlUrl="http://antirez.com"/>
<outline type="rss" text="idiallo.com" title="idiallo.com" xmlUrl="https://idiallo.com/feed.rss" htmlUrl="https://idiallo.com"/>
<outline type="rss" text="maurycyz.com" title="maurycyz.com" xmlUrl="https://maurycyz.com/index.xml" htmlUrl="https://maurycyz.com"/>
<outline type="rss" text="pluralistic.net" title="pluralistic.net" xmlUrl="https://pluralistic.net/feed/" htmlUrl="https://pluralistic.net"/>
<outline type="rss" text="shkspr.mobi" title="shkspr.mobi" xmlUrl="https://shkspr.mobi/blog/feed/" htmlUrl="https://shkspr.mobi"/>
<outline type="rss" text="lcamtuf.substack.com" title="lcamtuf.substack.com" xmlUrl="https://lcamtuf.substack.com/feed" htmlUrl="https://lcamtuf.substack.com"/>
<outline type="rss" text="mitchellh.com" title="mitchellh.com" xmlUrl="https://mitchellh.com/feed.xml" htmlUrl="https://mitchellh.com"/>
<outline type="rss" text="dynomight.net" title="dynomight.net" xmlUrl="https://dynomight.net/feed.xml" htmlUrl="https://dynomight.net"/>
<outline type="rss" text="utcc.utoronto.ca/~cks" title="utcc.utoronto.ca/~cks" xmlUrl="https://utcc.utoronto.ca/~cks/space/blog/?atom" htmlUrl="https://utcc.utoronto.ca/~cks"/>
<outline type="rss" text="xeiaso.net" title="xeiaso.net" xmlUrl="https://xeiaso.net/blog.rss" htmlUrl="https://xeiaso.net"/>
<outline type="rss" text="devblogs.microsoft.com/oldnewthing" title="devblogs.microsoft.com/oldnewthing" xmlUrl="https://devblogs.microsoft.com/oldnewthing/feed" htmlUrl="https://devblogs.microsoft.com/oldnewthing"/>
<outline type="rss" text="righto.com" title="righto.com" xmlUrl="https://www.righto.com/feeds/posts/default" htmlUrl="https://righto.com"/>
<outline type="rss" text="lucumr.pocoo.org" title="lucumr.pocoo.org" xmlUrl="https://lucumr.pocoo.org/feed.atom" htmlUrl="https://lucumr.pocoo.org"/>
<outline type="rss" text="skyfall.dev" title="skyfall.dev" xmlUrl="https://skyfall.dev/rss.xml" htmlUrl="https://skyfall.dev"/>
<outline type="rss" text="garymarcus.substack.com" title="garymarcus.substack.com" xmlUrl="https://garymarcus.substack.com/feed" htmlUrl="https://garymarcus.substack.com"/>
<outline type="rss" text="overreacted.io" title="overreacted.io" xmlUrl="https://overreacted.io/rss.xml" htmlUrl="https://overreacted.io"/>
<outline type="rss" text="timsh.org" title="timsh.org" xmlUrl="https://timsh.org/rss/" htmlUrl="https://timsh.org"/>
<outline type="rss" text="johndcook.com" title="johndcook.com" xmlUrl="https://www.johndcook.com/blog/feed/" htmlUrl="https://johndcook.com"/>
<outline type="rss" text="gilesthomas.com" title="gilesthomas.com" xmlUrl="https://gilesthomas.com/feed/rss.xml" htmlUrl="https://gilesthomas.com"/>
<outline type="rss" text="matklad.github.io" title="matklad.github.io" xmlUrl="https://matklad.github.io/feed.xml" htmlUrl="https://matklad.github.io"/>
<outline type="rss" text="derekthompson.org" title="derekthompson.org" xmlUrl="https://www.theatlantic.com/feed/author/derek-thompson/" htmlUrl="https://derekthompson.org"/>
<outline type="rss" text="evanhahn.com" title="evanhahn.com" xmlUrl="https://evanhahn.com/feed.xml" htmlUrl="https://evanhahn.com"/>
<outline type="rss" text="terriblesoftware.org" title="terriblesoftware.org" xmlUrl="https://terriblesoftware.org/feed/" htmlUrl="https://terriblesoftware.org"/>
<outline type="rss" text="rakhim.exotext.com" title="rakhim.exotext.com" xmlUrl="https://rakhim.exotext.com/rss.xml" htmlUrl="https://rakhim.exotext.com"/>
<outline type="rss" text="joanwestenberg.com" title="joanwestenberg.com" xmlUrl="https://joanwestenberg.com/rss" htmlUrl="https://joanwestenberg.com"/>
<outline type="rss" text="xania.org" title="xania.org" xmlUrl="https://xania.org/feed" htmlUrl="https://xania.org"/>
<outline type="rss" text="micahflee.com" title="micahflee.com" xmlUrl="https://micahflee.com/feed/" htmlUrl="https://micahflee.com"/>
<outline type="rss" text="nesbitt.io" title="nesbitt.io" xmlUrl="https://nesbitt.io/feed.xml" htmlUrl="https://nesbitt.io"/>
<outline type="rss" text="construction-physics.com" title="construction-physics.com" xmlUrl="https://www.construction-physics.com/feed" htmlUrl="https://construction-physics.com"/>
<outline type="rss" text="tedium.co" title="tedium.co" xmlUrl="https://feed.tedium.co/" htmlUrl="https://tedium.co"/>
<outline type="rss" text="susam.net" title="susam.net" xmlUrl="https://susam.net/feed.xml" htmlUrl="https://susam.net"/>
<outline type="rss" text="entropicthoughts.com" title="entropicthoughts.com" xmlUrl="https://entropicthoughts.com/feed.xml" htmlUrl="https://entropicthoughts.com"/>
<outline type="rss" text="buttondown.com/hillelwayne" title="buttondown.com/hillelwayne" xmlUrl="https://buttondown.com/hillelwayne/rss" htmlUrl="https://buttondown.com/hillelwayne"/>
<outline type="rss" text="dwarkesh.com" title="dwarkesh.com" xmlUrl="https://www.dwarkeshpatel.com/feed" htmlUrl="https://dwarkesh.com"/>
<outline type="rss" text="borretti.me" title="borretti.me" xmlUrl="https://borretti.me/feed.xml" htmlUrl="https://borretti.me"/>
<outline type="rss" text="wheresyoured.at" title="wheresyoured.at" xmlUrl="https://www.wheresyoured.at/rss/" htmlUrl="https://wheresyoured.at"/>
<outline type="rss" text="jayd.ml" title="jayd.ml" xmlUrl="https://jayd.ml/feed.xml" htmlUrl="https://jayd.ml"/>
<outline type="rss" text="minimaxir.com" title="minimaxir.com" xmlUrl="https://minimaxir.com/index.xml" htmlUrl="https://minimaxir.com"/>
<outline type="rss" text="geohot.github.io" title="geohot.github.io" xmlUrl="https://geohot.github.io/blog/feed.xml" htmlUrl="https://geohot.github.io"/>
<outline type="rss" text="paulgraham.com" title="paulgraham.com" xmlUrl="http://www.aaronsw.com/2002/feeds/pgessays.rss" htmlUrl="https://paulgraham.com"/>
<outline type="rss" text="filfre.net" title="filfre.net" xmlUrl="https://www.filfre.net/feed/" htmlUrl="https://filfre.net"/>
<outline type="rss" text="blog.jim-nielsen.com" title="blog.jim-nielsen.com" xmlUrl="https://blog.jim-nielsen.com/feed.xml" htmlUrl="https://blog.jim-nielsen.com"/>
<outline type="rss" text="jyn.dev" title="jyn.dev" xmlUrl="https://jyn.dev/atom.xml" htmlUrl="https://jyn.dev"/>
<outline type="rss" text="geoffreylitt.com" title="geoffreylitt.com" xmlUrl="https://www.geoffreylitt.com/feed.xml" htmlUrl="https://geoffreylitt.com"/>
<outline type="rss" text="downtowndougbrown.com" title="downtowndougbrown.com" xmlUrl="https://www.downtowndougbrown.com/feed/" htmlUrl="https://downtowndougbrown.com"/>
<outline type="rss" text="brutecat.com" title="brutecat.com" xmlUrl="https://brutecat.com/rss.xml" htmlUrl="https://brutecat.com"/>
<outline type="rss" text="eli.thegreenplace.net" title="eli.thegreenplace.net" xmlUrl="https://eli.thegreenplace.net/feeds/all.atom.xml" htmlUrl="https://eli.thegreenplace.net"/>
<outline type="rss" text="abortretry.fail" title="abortretry.fail" xmlUrl="https://www.abortretry.fail/feed" htmlUrl="https://abortretry.fail"/>
<outline type="rss" text="fabiensanglard.net" title="fabiensanglard.net" xmlUrl="https://fabiensanglard.net/rss.xml" htmlUrl="https://fabiensanglard.net"/>
<outline type="rss" text="oldvcr.blogspot.com" title="oldvcr.blogspot.com" xmlUrl="https://oldvcr.blogspot.com/feeds/posts/default" htmlUrl="https://oldvcr.blogspot.com"/>
<outline type="rss" text="bogdanthegeek.github.io" title="bogdanthegeek.github.io" xmlUrl="https://bogdanthegeek.github.io/blog/index.xml" htmlUrl="https://bogdanthegeek.github.io"/>
<outline type="rss" text="hugotunius.se" title="hugotunius.se" xmlUrl="https://hugotunius.se/feed.xml" htmlUrl="https://hugotunius.se"/>
<outline type="rss" text="gwern.net" title="gwern.net" xmlUrl="https://gwern.substack.com/feed" htmlUrl="https://gwern.net"/>
<outline type="rss" text="berthub.eu" title="berthub.eu" xmlUrl="https://berthub.eu/articles/index.xml" htmlUrl="https://berthub.eu"/>
<outline type="rss" text="chadnauseam.com" title="chadnauseam.com" xmlUrl="https://chadnauseam.com/rss.xml" htmlUrl="https://chadnauseam.com"/>
<outline type="rss" text="simone.org" title="simone.org" xmlUrl="https://simone.org/feed/" htmlUrl="https://simone.org"/>
<outline type="rss" text="it-notes.dragas.net" title="it-notes.dragas.net" xmlUrl="https://it-notes.dragas.net/feed/" htmlUrl="https://it-notes.dragas.net"/>
<outline type="rss" text="beej.us" title="beej.us" xmlUrl="https://beej.us/blog/rss.xml" htmlUrl="https://beej.us"/>
<outline type="rss" text="hey.paris" title="hey.paris" xmlUrl="https://hey.paris/index.xml" htmlUrl="https://hey.paris"/>
<outline type="rss" text="danielwirtz.com" title="danielwirtz.com" xmlUrl="https://danielwirtz.com/rss.xml" htmlUrl="https://danielwirtz.com"/>
<outline type="rss" text="matduggan.com" title="matduggan.com" xmlUrl="https://matduggan.com/rss/" htmlUrl="https://matduggan.com"/>
<outline type="rss" text="refactoringenglish.com" title="refactoringenglish.com" xmlUrl="https://refactoringenglish.com/index.xml" htmlUrl="https://refactoringenglish.com"/>
<outline type="rss" text="worksonmymachine.substack.com" title="worksonmymachine.substack.com" xmlUrl="https://worksonmymachine.substack.com/feed" htmlUrl="https://worksonmymachine.substack.com"/>
<outline type="rss" text="philiplaine.com" title="philiplaine.com" xmlUrl="https://philiplaine.com/index.xml" htmlUrl="https://philiplaine.com"/>
<outline type="rss" text="steveblank.com" title="steveblank.com" xmlUrl="https://steveblank.com/feed/" htmlUrl="https://steveblank.com"/>
<outline type="rss" text="bernsteinbear.com" title="bernsteinbear.com" xmlUrl="https://bernsteinbear.com/feed.xml" htmlUrl="https://bernsteinbear.com"/>
<outline type="rss" text="danieldelaney.net" title="danieldelaney.net" xmlUrl="https://danieldelaney.net/feed" htmlUrl="https://danieldelaney.net"/>
<outline type="rss" text="troyhunt.com" title="troyhunt.com" xmlUrl="https://www.troyhunt.com/rss/" htmlUrl="https://troyhunt.com"/>
<outline type="rss" text="herman.bearblog.dev" title="herman.bearblog.dev" xmlUrl="https://herman.bearblog.dev/feed/" htmlUrl="https://herman.bearblog.dev"/>
<outline type="rss" text="tomrenner.com" title="tomrenner.com" xmlUrl="https://tomrenner.com/index.xml" htmlUrl="https://tomrenner.com"/>
<outline type="rss" text="blog.pixelmelt.dev" title="blog.pixelmelt.dev" xmlUrl="https://blog.pixelmelt.dev/rss/" htmlUrl="https://blog.pixelmelt.dev"/>
<outline type="rss" text="martinalderson.com" title="martinalderson.com" xmlUrl="https://martinalderson.com/feed.xml" htmlUrl="https://martinalderson.com"/>
<outline type="rss" text="danielchasehooper.com" title="danielchasehooper.com" xmlUrl="https://danielchasehooper.com/feed.xml" htmlUrl="https://danielchasehooper.com"/>
<outline type="rss" text="chiark.greenend.org.uk/~sgtatham" title="chiark.greenend.org.uk/~sgtatham" xmlUrl="https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/feed.xml" htmlUrl="https://chiark.greenend.org.uk/~sgtatham"/>
<outline type="rss" text="grantslatton.com" title="grantslatton.com" xmlUrl="https://grantslatton.com/rss.xml" htmlUrl="https://grantslatton.com"/>
<outline type="rss" text="experimental-history.com" title="experimental-history.com" xmlUrl="https://www.experimental-history.com/feed" htmlUrl="https://experimental-history.com"/>
<outline type="rss" text="anildash.com" title="anildash.com" xmlUrl="https://anildash.com/feed.xml" htmlUrl="https://anildash.com"/>
<outline type="rss" text="aresluna.org" title="aresluna.org" xmlUrl="https://aresluna.org/main.rss" htmlUrl="https://aresluna.org"/>
<outline type="rss" text="michael.stapelberg.ch" title="michael.stapelberg.ch" xmlUrl="https://michael.stapelberg.ch/feed.xml" htmlUrl="https://michael.stapelberg.ch"/>
<outline type="rss" text="miguelgrinberg.com" title="miguelgrinberg.com" xmlUrl="https://blog.miguelgrinberg.com/feed" htmlUrl="https://miguelgrinberg.com"/>
<outline type="rss" text="keygen.sh" title="keygen.sh" xmlUrl="https://keygen.sh/blog/feed.xml" htmlUrl="https://keygen.sh"/>
<outline type="rss" text="mjg59.dreamwidth.org" title="mjg59.dreamwidth.org" xmlUrl="https://mjg59.dreamwidth.org/data/rss" htmlUrl="https://mjg59.dreamwidth.org"/>
<outline type="rss" text="computer.rip" title="computer.rip" xmlUrl="https://computer.rip/rss.xml" htmlUrl="https://computer.rip"/>
</outline>
</body>
</opml>
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>Blog Feeds</title>
</head>
<body>
<outline text="Blogs" title="Blogs">
<outline type="rss" text="simonwillison.net" title="simonwillison.net" xmlUrl="https://simonwillison.net/atom/everything/" htmlUrl="https://simonwillison.net"/>
<outline type="rss" text="jeffgeerling.com" title="jeffgeerling.com" xmlUrl="https://www.jeffgeerling.com/blog.xml" htmlUrl="https://jeffgeerling.com"/>
<outline type="rss" text="seangoedecke.com" title="seangoedecke.com" xmlUrl="https://www.seangoedecke.com/rss.xml" htmlUrl="https://seangoedecke.com"/>
<outline type="rss" text="krebsonsecurity.com" title="krebsonsecurity.com" xmlUrl="https://krebsonsecurity.com/feed/" htmlUrl="https://krebsonsecurity.com"/>
<outline type="rss" text="daringfireball.net" title="daringfireball.net" xmlUrl="https://daringfireball.net/feeds/main" htmlUrl="https://daringfireball.net"/>
<outline type="rss" text="ericmigi.com" title="ericmigi.com" xmlUrl="https://ericmigi.com/rss.xml" htmlUrl="https://ericmigi.com"/>
<outline type="rss" text="antirez.com" title="antirez.com" xmlUrl="http://antirez.com/rss" htmlUrl="http://antirez.com"/>
<outline type="rss" text="idiallo.com" title="idiallo.com" xmlUrl="https://idiallo.com/feed.rss" htmlUrl="https://idiallo.com"/>
<outline type="rss" text="maurycyz.com" title="maurycyz.com" xmlUrl="https://maurycyz.com/index.xml" htmlUrl="https://maurycyz.com"/>
<outline type="rss" text="pluralistic.net" title="pluralistic.net" xmlUrl="https://pluralistic.net/feed/" htmlUrl="https://pluralistic.net"/>
<outline type="rss" text="shkspr.mobi" title="shkspr.mobi" xmlUrl="https://shkspr.mobi/blog/feed/" htmlUrl="https://shkspr.mobi"/>
<outline type="rss" text="lcamtuf.substack.com" title="lcamtuf.substack.com" xmlUrl="https://lcamtuf.substack.com/feed" htmlUrl="https://lcamtuf.substack.com"/>
<outline type="rss" text="mitchellh.com" title="mitchellh.com" xmlUrl="https://mitchellh.com/feed.xml" htmlUrl="https://mitchellh.com"/>
<outline type="rss" text="dynomight.net" title="dynomight.net" xmlUrl="https://dynomight.net/feed.xml" htmlUrl="https://dynomight.net"/>
<outline type="rss" text="utcc.utoronto.ca/~cks" title="utcc.utoronto.ca/~cks" xmlUrl="https://utcc.utoronto.ca/~cks/space/blog/?atom" htmlUrl="https://utcc.utoronto.ca/~cks"/>
<outline type="rss" text="xeiaso.net" title="xeiaso.net" xmlUrl="https://xeiaso.net/blog.rss" htmlUrl="https://xeiaso.net"/>
<outline type="rss" text="devblogs.microsoft.com/oldnewthing" title="devblogs.microsoft.com/oldnewthing" xmlUrl="https://devblogs.microsoft.com/oldnewthing/feed" htmlUrl="https://devblogs.microsoft.com/oldnewthing"/>
<outline type="rss" text="righto.com" title="righto.com" xmlUrl="https://www.righto.com/feeds/posts/default" htmlUrl="https://righto.com"/>
<outline type="rss" text="lucumr.pocoo.org" title="lucumr.pocoo.org" xmlUrl="https://lucumr.pocoo.org/feed.atom" htmlUrl="https://lucumr.pocoo.org"/>
<outline type="rss" text="skyfall.dev" title="skyfall.dev" xmlUrl="https://skyfall.dev/rss.xml" htmlUrl="https://skyfall.dev"/>
<outline type="rss" text="garymarcus.substack.com" title="garymarcus.substack.com" xmlUrl="https://garymarcus.substack.com/feed" htmlUrl="https://garymarcus.substack.com"/>
<outline type="rss" text="overreacted.io" title="overreacted.io" xmlUrl="https://overreacted.io/rss.xml" htmlUrl="https://overreacted.io"/>
<outline type="rss" text="timsh.org" title="timsh.org" xmlUrl="https://timsh.org/rss/" htmlUrl="https://timsh.org"/>
<outline type="rss" text="johndcook.com" title="johndcook.com" xmlUrl="https://www.johndcook.com/blog/feed/" htmlUrl="https://johndcook.com"/>
<outline type="rss" text="gilesthomas.com" title="gilesthomas.com" xmlUrl="https://gilesthomas.com/feed/rss.xml" htmlUrl="https://gilesthomas.com"/>
<outline type="rss" text="matklad.github.io" title="matklad.github.io" xmlUrl="https://matklad.github.io/feed.xml" htmlUrl="https://matklad.github.io"/>
<outline type="rss" text="derekthompson.org" title="derekthompson.org" xmlUrl="https://www.theatlantic.com/feed/author/derek-thompson/" htmlUrl="https://derekthompson.org"/>
<outline type="rss" text="evanhahn.com" title="evanhahn.com" xmlUrl="https://evanhahn.com/feed.xml" htmlUrl="https://evanhahn.com"/>
<outline type="rss" text="terriblesoftware.org" title="terriblesoftware.org" xmlUrl="https://terriblesoftware.org/feed/" htmlUrl="https://terriblesoftware.org"/>
<outline type="rss" text="rakhim.exotext.com" title="rakhim.exotext.com" xmlUrl="https://rakhim.exotext.com/rss.xml" htmlUrl="https://rakhim.exotext.com"/>
<outline type="rss" text="joanwestenberg.com" title="joanwestenberg.com" xmlUrl="https://joanwestenberg.com/rss" htmlUrl="https://joanwestenberg.com"/>
<outline type="rss" text="xania.org" title="xania.org" xmlUrl="https://xania.org/feed" htmlUrl="https://xania.org"/>
<outline type="rss" text="micahflee.com" title="micahflee.com" xmlUrl="https://micahflee.com/feed/" htmlUrl="https://micahflee.com"/>
<outline type="rss" text="nesbitt.io" title="nesbitt.io" xmlUrl="https://nesbitt.io/feed.xml" htmlUrl="https://nesbitt.io"/>
<outline type="rss" text="construction-physics.com" title="construction-physics.com" xmlUrl="https://www.construction-physics.com/feed" htmlUrl="https://construction-physics.com"/>
<outline type="rss" text="tedium.co" title="tedium.co" xmlUrl="https://feed.tedium.co/" htmlUrl="https://tedium.co"/>
<outline type="rss" text="susam.net" title="susam.net" xmlUrl="https://susam.net/feed.xml" htmlUrl="https://susam.net"/>
<outline type="rss" text="entropicthoughts.com" title="entropicthoughts.com" xmlUrl="https://entropicthoughts.com/feed.xml" htmlUrl="https://entropicthoughts.com"/>
<outline type="rss" text="buttondown.com/hillelwayne" title="buttondown.com/hillelwayne" xmlUrl="https://buttondown.com/hillelwayne/rss" htmlUrl="https://buttondown.com/hillelwayne"/>
<outline type="rss" text="dwarkesh.com" title="dwarkesh.com" xmlUrl="https://www.dwarkeshpatel.com/feed" htmlUrl="https://dwarkesh.com"/>
<outline type="rss" text="borretti.me" title="borretti.me" xmlUrl="https://borretti.me/feed.xml" htmlUrl="https://borretti.me"/>
<outline type="rss" text="wheresyoured.at" title="wheresyoured.at" xmlUrl="https://www.wheresyoured.at/rss/" htmlUrl="https://wheresyoured.at"/>
<outline type="rss" text="jayd.ml" title="jayd.ml" xmlUrl="https://jayd.ml/feed.xml" htmlUrl="https://jayd.ml"/>
<outline type="rss" text="minimaxir.com" title="minimaxir.com" xmlUrl="https://minimaxir.com/index.xml" htmlUrl="https://minimaxir.com"/>
<outline type="rss" text="geohot.github.io" title="geohot.github.io" xmlUrl="https://geohot.github.io/blog/feed.xml" htmlUrl="https://geohot.github.io"/>
<outline type="rss" text="paulgraham.com" title="paulgraham.com" xmlUrl="http://www.aaronsw.com/2002/feeds/pgessays.rss" htmlUrl="https://paulgraham.com"/>
<outline type="rss" text="filfre.net" title="filfre.net" xmlUrl="https://www.filfre.net/feed/" htmlUrl="https://filfre.net"/>
<outline type="rss" text="blog.jim-nielsen.com" title="blog.jim-nielsen.com" xmlUrl="https://blog.jim-nielsen.com/feed.xml" htmlUrl="https://blog.jim-nielsen.com"/>
<outline type="rss" text="jyn.dev" title="jyn.dev" xmlUrl="https://jyn.dev/atom.xml" htmlUrl="https://jyn.dev"/>
<outline type="rss" text="geoffreylitt.com" title="geoffreylitt.com" xmlUrl="https://www.geoffreylitt.com/feed.xml" htmlUrl="https://geoffreylitt.com"/>
<outline type="rss" text="downtowndougbrown.com" title="downtowndougbrown.com" xmlUrl="https://www.downtowndougbrown.com/feed/" htmlUrl="https://downtowndougbrown.com"/>
<outline type="rss" text="brutecat.com" title="brutecat.com" xmlUrl="https://brutecat.com/rss.xml" htmlUrl="https://brutecat.com"/>
<outline type="rss" text="eli.thegreenplace.net" title="eli.thegreenplace.net" xmlUrl="https://eli.thegreenplace.net/feeds/all.atom.xml" htmlUrl="https://eli.thegreenplace.net"/>
<outline type="rss" text="abortretry.fail" title="abortretry.fail" xmlUrl="https://www.abortretry.fail/feed" htmlUrl="https://abortretry.fail"/>
<outline type="rss" text="fabiensanglard.net" title="fabiensanglard.net" xmlUrl="https://fabiensanglard.net/rss.xml" htmlUrl="https://fabiensanglard.net"/>
<outline type="rss" text="oldvcr.blogspot.com" title="oldvcr.blogspot.com" xmlUrl="https://oldvcr.blogspot.com/feeds/posts/default" htmlUrl="https://oldvcr.blogspot.com"/>
<outline type="rss" text="bogdanthegeek.github.io" title="bogdanthegeek.github.io" xmlUrl="https://bogdanthegeek.github.io/blog/index.xml" htmlUrl="https://bogdanthegeek.github.io"/>
<outline type="rss" text="hugotunius.se" title="hugotunius.se" xmlUrl="https://hugotunius.se/feed.xml" htmlUrl="https://hugotunius.se"/>
<outline type="rss" text="gwern.net" title="gwern.net" xmlUrl="https://gwern.substack.com/feed" htmlUrl="https://gwern.net"/>
<outline type="rss" text="berthub.eu" title="berthub.eu" xmlUrl="https://berthub.eu/articles/index.xml" htmlUrl="https://berthub.eu"/>
<outline type="rss" text="chadnauseam.com" title="chadnauseam.com" xmlUrl="https://chadnauseam.com/rss.xml" htmlUrl="https://chadnauseam.com"/>
<outline type="rss" text="simone.org" title="simone.org" xmlUrl="https://simone.org/feed/" htmlUrl="https://simone.org"/>
<outline type="rss" text="it-notes.dragas.net" title="it-notes.dragas.net" xmlUrl="https://it-notes.dragas.net/feed/" htmlUrl="https://it-notes.dragas.net"/>
<outline type="rss" text="beej.us" title="beej.us" xmlUrl="https://beej.us/blog/rss.xml" htmlUrl="https://beej.us"/>
<outline type="rss" text="hey.paris" title="hey.paris" xmlUrl="https://hey.paris/index.xml" htmlUrl="https://hey.paris"/>
<outline type="rss" text="danielwirtz.com" title="danielwirtz.com" xmlUrl="https://danielwirtz.com/rss.xml" htmlUrl="https://danielwirtz.com"/>
<outline type="rss" text="matduggan.com" title="matduggan.com" xmlUrl="https://matduggan.com/rss/" htmlUrl="https://matduggan.com"/>
<outline type="rss" text="refactoringenglish.com" title="refactoringenglish.com" xmlUrl="https://refactoringenglish.com/index.xml" htmlUrl="https://refactoringenglish.com"/>
<outline type="rss" text="worksonmymachine.substack.com" title="worksonmymachine.substack.com" xmlUrl="https://worksonmymachine.substack.com/feed" htmlUrl="https://worksonmymachine.substack.com"/>
<outline type="rss" text="philiplaine.com" title="philiplaine.com" xmlUrl="https://philiplaine.com/index.xml" htmlUrl="https://philiplaine.com"/>
<outline type="rss" text="steveblank.com" title="steveblank.com" xmlUrl="https://steveblank.com/feed/" htmlUrl="https://steveblank.com"/>
<outline type="rss" text="bernsteinbear.com" title="bernsteinbear.com" xmlUrl="https://bernsteinbear.com/feed.xml" htmlUrl="https://bernsteinbear.com"/>
<outline type="rss" text="danieldelaney.net" title="danieldelaney.net" xmlUrl="https://danieldelaney.net/feed" htmlUrl="https://danieldelaney.net"/>
<outline type="rss" text="troyhunt.com" title="troyhunt.com" xmlUrl="https://www.troyhunt.com/rss/" htmlUrl="https://troyhunt.com"/>
<outline type="rss" text="herman.bearblog.dev" title="herman.bearblog.dev" xmlUrl="https://herman.bearblog.dev/feed/" htmlUrl="https://herman.bearblog.dev"/>
<outline type="rss" text="tomrenner.com" title="tomrenner.com" xmlUrl="https://tomrenner.com/index.xml" htmlUrl="https://tomrenner.com"/>
<outline type="rss" text="blog.pixelmelt.dev" title="blog.pixelmelt.dev" xmlUrl="https://blog.pixelmelt.dev/rss/" htmlUrl="https://blog.pixelmelt.dev"/>
<outline type="rss" text="martinalderson.com" title="martinalderson.com" xmlUrl="https://martinalderson.com/feed.xml" htmlUrl="https://martinalderson.com"/>
<outline type="rss" text="danielchasehooper.com" title="danielchasehooper.com" xmlUrl="https://danielchasehooper.com/feed.xml" htmlUrl="https://danielchasehooper.com"/>
<outline type="rss" text="chiark.greenend.org.uk/~sgtatham" title="chiark.greenend.org.uk/~sgtatham" xmlUrl="https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/feed.xml" htmlUrl="https://chiark.greenend.org.uk/~sgtatham"/>
<outline type="rss" text="grantslatton.com" title="grantslatton.com" xmlUrl="https://grantslatton.com/rss.xml" htmlUrl="https://grantslatton.com"/>
<outline type="rss" text="experimental-history.com" title="experimental-history.com" xmlUrl="https://www.experimental-history.com/feed" htmlUrl="https://experimental-history.com"/>
<outline type="rss" text="anildash.com" title="anildash.com" xmlUrl="https://anildash.com/feed.xml" htmlUrl="https://anildash.com"/>
<outline type="rss" text="aresluna.org" title="aresluna.org" xmlUrl="https://aresluna.org/main.rss" htmlUrl="https://aresluna.org"/>
<outline type="rss" text="michael.stapelberg.ch" title="michael.stapelberg.ch" xmlUrl="https://michael.stapelberg.ch/feed.xml" htmlUrl="https://michael.stapelberg.ch"/>
<outline type="rss" text="miguelgrinberg.com" title="miguelgrinberg.com" xmlUrl="https://blog.miguelgrinberg.com/feed" htmlUrl="https://miguelgrinberg.com"/>
<outline type="rss" text="keygen.sh" title="keygen.sh" xmlUrl="https://keygen.sh/blog/feed.xml" htmlUrl="https://keygen.sh"/>
<outline type="rss" text="mjg59.dreamwidth.org" title="mjg59.dreamwidth.org" xmlUrl="https://mjg59.dreamwidth.org/data/rss" htmlUrl="https://mjg59.dreamwidth.org"/>
<outline type="rss" text="computer.rip" title="computer.rip" xmlUrl="https://computer.rip/rss.xml" htmlUrl="https://computer.rip"/>
</outline>
<outline type="rss" text="GitHub Weekly" title="GitHub Weekly" xmlUrl="https://itcoffee66.github.io/githubweekly/feed.xml" htmlUrl="https://itcoffee66.github.io/githubweekly/"/>
</body>
</opml>
@@ -0,0 +1,229 @@
{
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/22/openai-cyberattack/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/22/are-ai-labs-pelicanmaxxing/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/21/nativ/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/21/cat-and-thariq/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/20/cheap-reverse-engineering/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/20/afraid-of-chinese-models/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/20/sam-altman/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/19/ai-mania/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/19/claude-code-in-bun-in-rust/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/18/sqlite-query-explainer/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/18/claude-make-fable-5-permanent/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/18/quixote/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/17/kimi-k3/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/17/llm-cliche-highlighter/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/17/spot-birds-not-golf/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/16/firefox-in-webassembly/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/16/kimi-k3/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/16/bad-codex-bug/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/16/inkling/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/16/mermaid-ascii/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/16/linus-torvalds/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/16/grok-mermaid/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/15/grok-build/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/15/claude-web-fetch-exfiltration/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/14/github-changeling/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/14/pedalican/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/14/lobsters-sqlite/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/14/armin-ronacher/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/14/datasette/#atom-everything": true,
"Simon Willison's Weblog|https://simonwillison.net/2026/Jul/14/uvx-github-actions-cache/#atom-everything": true,
"Daring Fireball|https://daringfireball.net/2026/07/ec_google_guidance_android_ai_and_search_sharing": true,
"Daring Fireball|https://workos.com/blog/management-mcp-server?utm_source=daringfireball&utm_medium=newsletter&utm_campaign=q32026": true,
"Daring Fireball|https://stratechery.com/2026/whos-afraid-of-chinese-models/": true,
"Daring Fireball|https://paper.design/?utm_source=df": true,
"Daring Fireball|https://9to5mac.com/2026/07/17/investigation-reveals-dozens-of-disguised-gambling-apps-on-the-app-store-in-brazil/": true,
"Daring Fireball|https://daringfireball.net/2026/07/mornings_in_cupertino_have_the_aroma_of_napalm_once_again": true,
"Daring Fireball|https://www.ft.com/content/1b8c9d52-88a9-426b-ba47-f1811f859166?syn-25a6b1a6=1": true,
"Daring Fireball|https://thenewthings.com/p/apple-big-ai-book-slop-problem": true,
"Daring Fireball|https://www.cnbc.com/2026/07/02/alphabet-google-android-eu-antitrust-fine-4-1-billion-euro-appeal.html": true,
"Daring Fireball|https://about.roblox.com/newsroom/2026/07/build-without-limits-on-roblox": true,
"Daring Fireball|https://9to5mac.com/2026/07/17/apple-raises-prices-for-apple-music-and-apple-one-subscriptions/": true,
"Daring Fireball|https://www.wired.com/story/openai-reorg-greg-brockman-product/": true,
"Daring Fireball|https://spyglass.org/chatgpt-brings-back-chatgpt/": true,
"Daring Fireball|https://x.com/thsottiaux/status/2077928427936710901": true,
"Daring Fireball|https://lore.kernel.org/linux-media/CAHk-=wi4zC+Ze8e+p3tMv8TtG_80KzsZ1syL9anBtmEh5Z40vg@mail.gmail.com/": true,
"Daring Fireball|https://www.barebones.com/products/bbedit/bbedit16.html": true,
"Daring Fireball|https://github.com/ActionRetro/ArtfulType": true,
"Daring Fireball|https://www.threads.com/@joannastern/post/Da5rgIvjrLM": true,
"Daring Fireball|https://www.theverge.com/policy/965792/google-epic-withdraw-injunction-third-party-app-stores-coming-google-play?view_token=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IkZpdmhlVXFoV0giLCJwIjoiL3BvbGljeS85NjU3OTIvZ29vZ2xlLWVwaWMtd2l0aGRyYXctaW5qdW5jdGlvbi10aGlyZC1wYXJ0eS1hcHAtc3RvcmVzLWNvbWluZy1nb29nbGUtcGxheSIsImV4cCI6MTc4NDczNTA1NSwiaWF0IjoxNzg0MzAzMDU1fQ.zPHCDeRVkCOK73sdt6bKC2evAofTI582EsJ0N-rk79g": true,
"Daring Fireball|https://environment.ec.europa.eu/news/commission-adds-exemptions-portable-battery-removal-rules-2026-07-14_en": true,
"Daring Fireball|https://mastodon.social/@quicheindustries/116918456229212087": true,
"Daring Fireball|https://dithering.passport.online/member/episode/apple-sues-open-ai": true,
"Daring Fireball|https://www.bloomberg.com/news/articles/2026-07-14/openai-says-it-s-not-aware-of-any-evidence-that-apple-lawsuit-has-merit": true,
"Daring Fireball|https://www.nbcnews.com/tech/apple/apple-openai-lawsuit-suit-trade-product-hardware-email-sam-altman-rcna587376": true,
"Daring Fireball|https://parakeet.co/blog/the-shape-of-apps/": true,
"Daring Fireball|https://openai.com/supply/co-lab/work-louder/": true,
"Daring Fireball|https://www.bloomberg.com/news/articles/2026-07-14/openai-s-first-device-will-be-moveable-screenless-speaker-built-as-ai-companion?accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJTdWJzY3JpYmVyR2lmdGVkQXJ0aWNsZSIsImlhdCI6MTc4NDA2MjAxMywiZXhwIjoxNzg0NjY2ODEzLCJhcnRpY2xlSWQiOiJUSTYwSllUOU5KTFMwMCIsImJjb25uZWN0SWQiOiJDNEVEQ0FFMUZBMDU0MEJFQTI0QTlGMjExQzFFOTA4MCJ9.DfRN0afk0TFIaHFw9zEKYjehnfMsZfKC7gPoVos8WPI&leadSource=article-gifting": true,
"Daring Fireball|https://mobiledevmemo.com/did-apple-just-signal-a-third-party-expansion-of-apple-ads/": true,
"Daring Fireball|https://techcrunch.com/2026/07/15/apple-quietly-reveals-how-its-maps-ads-will-differ-from-googles/": true,
"Daring Fireball|https://www.scmp.com/tech/policy/article/3360685/china-approves-apple-intelligence-phones-alibaba-baidu-emerging-partners": true,
"Daring Fireball|https://arstechnica.com/tech-policy/2025/08/elon-musk-sues-apple-openai-to-block-exclusive-iphone-chatgpt-integration/": true,
"Daring Fireball|https://workos.com/pipes?utm_source=daringfireball&utm_medium=newsletter&utm_campaign=q32026": true,
"Daring Fireball|https://pfandrade.me/blog/swiftui-mac-assed-wwdc27-update/": true,
"Daring Fireball|https://grumpy.website/1723": true,
"Daring Fireball|https://tonsky.me/blog/every-frame-perfect/": true,
"Daring Fireball|https://github.com/insidegui/TwoMillionKit": true,
"Daring Fireball|https://x.com/sama/status/2075982617976230043": true,
"Daring Fireball|https://morphing.cloud/lunacy/": true,
"Daring Fireball|https://morphing.cloud/hypercard/": true,
"Daring Fireball|https://help.openai.com/en/articles/20001276-moving-to-the-new-chatgpt-desktop-app": true,
"Daring Fireball|https://help.openai.com/en/articles/20001275-chatgpt-work-and-codex": true,
"Daring Fireball|https://www.threads.com/@benedictevans/post/Dano_uvDr8F": true,
"Daring Fireball|https://daringfireball.net/2026/07/exactly_like_om_malik": true,
"Daring Fireball|https://www.bloomberg.com/news/articles/2026-07-11/openai-engineer-s-lol-moment-set-stage-for-legal-fight-with-apple?accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJTdWJzY3JpYmVyR2lmdGVkQXJ0aWNsZSIsImlhdCI6MTc4Mzc3OTcwMCwiZXhwIjoxNzg0Mzg0NTAwLCJhcnRpY2xlSWQiOiJUSFpEVUhLR0lGUEMwMCIsImJjb25uZWN0SWQiOiJDNEVEQ0FFMUZBMDU0MEJFQTI0QTlGMjExQzFFOTA4MCJ9.lF9LVTMyaJOToYhpwph5JjsSJEjdBGXLbenBKQdpHhc&leadSource=uverify%20wall": true,
"Daring Fireball|https://daringfireball.net/2026/07/ternus_apple_slippery_slope": true,
"Daring Fireball|https://daringfireball.net/2026/07/whats_good_for_the_ios_goose_is_often_not_good_for_the_macos_gander": true,
"Daring Fireball|https://daringfireball.net/2026/07/eliminate_app_icon_squircle_jail": true,
"Krebs on Security|https://krebsonsecurity.com/2026/07/lg-to-ban-residential-proxies-from-smart-tv-apps/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/07/microsoft-patches-a-record-570-security-flaws/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/07/lessons-learned-from-cisas-recent-github-leak/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/07/felons-fraudsters-flog-offensive-cybersecurity-startup/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/07/fbi-seizes-netnut-proxy-platform-popa-botnet/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/06/scattered-spider-hackers-plead-guilty-on-day-1-of-trial/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/06/popa-botnet-linked-to-publicly-traded-israeli-firm/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/06/who-runs-the-ransomware-group-the-gentlemen/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/06/a-record-breaking-patch-tuesday-for-june-2026/": true,
"Krebs on Security|https://krebsonsecurity.com/2026/06/hackers-used-metas-ai-support-bot-to-seize-instagram-accounts/": true,
"<antirez>|http://antirez.com/news/170": true,
"<antirez>|http://antirez.com/news/169": true,
"<antirez>|http://antirez.com/news/168": true,
"<antirez>|http://antirez.com/news/167": true,
"<antirez>|http://antirez.com/news/166": true,
"<antirez>|http://antirez.com/news/165": true,
"<antirez>|http://antirez.com/news/164": true,
"<antirez>|http://antirez.com/news/163": true,
"<antirez>|http://antirez.com/news/162": true,
"<antirez>|http://antirez.com/news/161": true,
"<antirez>|http://antirez.com/news/160": true,
"<antirez>|http://antirez.com/news/159": true,
"<antirez>|http://antirez.com/news/158": true,
"<antirez>|http://antirez.com/news/157": true,
"<antirez>|http://antirez.com/news/156": true,
"<antirez>|http://antirez.com/news/155": true,
"<antirez>|http://antirez.com/news/154": true,
"<antirez>|http://antirez.com/news/153": true,
"<antirez>|http://antirez.com/news/152": true,
"<antirez>|http://antirez.com/news/151": true,
"<antirez>|http://antirez.com/news/150": true,
"<antirez>|http://antirez.com/news/149": true,
"<antirez>|http://antirez.com/news/148": true,
"<antirez>|http://antirez.com/news/147": true,
"<antirez>|http://antirez.com/news/146": true,
"<antirez>|http://antirez.com/news/145": true,
"<antirez>|http://antirez.com/news/144": true,
"<antirez>|http://antirez.com/news/143": true,
"<antirez>|http://antirez.com/news/142": true,
"<antirez>|http://antirez.com/news/141": true,
"<antirez>|http://antirez.com/news/140": true,
"<antirez>|http://antirez.com/news/139": true,
"<antirez>|http://antirez.com/news/138": true,
"<antirez>|http://antirez.com/news/137": true,
"<antirez>|http://antirez.com/news/136": true,
"<antirez>|http://antirez.com/news/135": true,
"<antirez>|http://antirez.com/news/134": true,
"<antirez>|http://antirez.com/news/133": true,
"<antirez>|http://antirez.com/news/132": true,
"<antirez>|http://antirez.com/news/131": true,
"<antirez>|http://antirez.com/news/130": true,
"<antirez>|http://antirez.com/news/129": true,
"<antirez>|http://antirez.com/news/128": true,
"<antirez>|http://antirez.com/news/127": true,
"<antirez>|http://antirez.com/news/126": true,
"<antirez>|http://antirez.com/news/125": true,
"<antirez>|http://antirez.com/news/124": true,
"<antirez>|http://antirez.com/news/123": true,
"<antirez>|http://antirez.com/news/122": true,
"<antirez>|http://antirez.com/news/121": true,
"<antirez>|http://antirez.com/news/120": true,
"<antirez>|http://antirez.com/news/119": true,
"<antirez>|http://antirez.com/news/118": true,
"<antirez>|http://antirez.com/news/117": true,
"<antirez>|http://antirez.com/news/116": true,
"<antirez>|http://antirez.com/news/115": true,
"<antirez>|http://antirez.com/news/114": true,
"<antirez>|http://antirez.com/news/113": true,
"<antirez>|http://antirez.com/news/112": true,
"<antirez>|http://antirez.com/news/111": true,
"<antirez>|http://antirez.com/news/110": true,
"<antirez>|http://antirez.com/news/109": true,
"<antirez>|http://antirez.com/news/108": true,
"<antirez>|http://antirez.com/news/107": true,
"<antirez>|http://antirez.com/news/106": true,
"<antirez>|http://antirez.com/news/105": true,
"<antirez>|http://antirez.com/news/104": true,
"<antirez>|http://antirez.com/news/103": true,
"<antirez>|http://antirez.com/news/102": true,
"<antirez>|http://antirez.com/news/101": true,
"<antirez>|http://antirez.com/news/100": true,
"<antirez>|http://antirez.com/news/99": true,
"<antirez>|http://antirez.com/news/98": true,
"<antirez>|http://antirez.com/news/97": true,
"<antirez>|http://antirez.com/news/96": true,
"<antirez>|http://antirez.com/news/95": true,
"<antirez>|http://antirez.com/news/94": true,
"<antirez>|http://antirez.com/news/93": true,
"<antirez>|http://antirez.com/news/92": true,
"<antirez>|http://antirez.com/news/91": true,
"<antirez>|http://antirez.com/news/90": true,
"<antirez>|http://antirez.com/news/89": true,
"<antirez>|http://antirez.com/news/88": true,
"<antirez>|http://antirez.com/news/87": true,
"<antirez>|http://antirez.com/news/86": true,
"<antirez>|http://antirez.com/news/85": true,
"<antirez>|http://antirez.com/news/84": true,
"<antirez>|http://antirez.com/news/83": true,
"<antirez>|http://antirez.com/news/82": true,
"<antirez>|http://antirez.com/news/81": true,
"<antirez>|http://antirez.com/news/80": true,
"<antirez>|http://antirez.com/news/79": true,
"<antirez>|http://antirez.com/news/78": true,
"<antirez>|http://antirez.com/news/77": true,
"<antirez>|http://antirez.com/news/76": true,
"<antirez>|http://antirez.com/news/75": true,
"<antirez>|http://antirez.com/news/74": true,
"<antirez>|http://antirez.com/news/73": true,
"<antirez>|http://antirez.com/news/72": true,
"<antirez>|http://antirez.com/news/71": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/open-sauce-gps-time-badge/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/quadrf-can-spot-drones-and-see-wifi-through-my-wall/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/special-value-pi-4-extremely-short-lived/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/apply-lut-color-grade-with-ffmpeg/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/framework-10g-ethernet-module-usb-c-complexity/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/power-on-your-mac-remotely/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/i-tested-every-ip-kvm/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/its-hard-to-justify-framework-12/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/tuning-in-fm-radio-on-a-3d-printer-heatbed/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/i-patched-iozone-for-better-disk-benchmarks-on-modern-macos/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/news-about-raspberry-pi-6-and-microcontroller-development/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/wi-wi-is-wireless-time-sync-less-than-5ns/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/bambu-lab-abusing-open-source-social-contract/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/homepod-mini-feels-like-magic--but-it-s-just-good-timing/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/deskpi-super4c-sbc-cluster/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/raspberry-pi-connect-may-control-windows-soon/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/new-10-gbe-usb-adapters-cooler-smaller-cheaper/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/arm-mainboard-for-framework-laptop/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/build-your-own-dial-up-isp-with-a-raspberry-pi/": true,
"Jeff Geerling|https://www.jeffgeerling.com/blog/2026/dram-pricing-is-killing-the-hobbyist-sbc-market/": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/122.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/121.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/120.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/119.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/118.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/117.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/116.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/115.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/114.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/113.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/112.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/111.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/110.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/109.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/108.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/107.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/106.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/105.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/104.html": true,
"Github Weekly|https://itcoffee66.github.io/githubweekly/103.html": true
}
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""RSS fetcher with dedup. Subprocess isolation per feed for true timeout."""
import argparse
import json
import os
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
DEFAULT_OPML = SKILL_DIR / "references" / "feeds.opml"
DEFAULT_STATE = SKILL_DIR / "references" / "last_seen.json"
FETCHER_CODE = r"""
import sys, json, time
import requests, feedparser
from datetime import datetime, timezone
try:
resp = requests.get(sys.argv[1], timeout=(5, 15),
headers={'User-Agent': 'ak-rss-digest/1.0',
'Accept': 'application/rss+xml, application/atom+xml, text/xml'})
resp.raise_for_status()
parsed = feedparser.parse(resp.content)
entries = []
for e in parsed.entries:
link = e.get('link', '')
if not link: continue
pub = None
for df in ['published_parsed', 'updated_parsed']:
dt = e.get(df)
if dt: pub = datetime(*dt[:6], tzinfo=timezone.utc); break
import re
summary = re.sub(r'<[^>]+>', '', e.get('summary','') or e.get('description','') or '').strip()[:300]
entries.append({
'title': e.get('title','(untitled)'), 'url': link,
'published': pub.isoformat() if pub else None, 'summary': summary,
})
print(json.dumps({'status': 'ok', 'feed_title': parsed.feed.get('title',''), 'entries': entries}))
except Exception as ex:
print(json.dumps({'status': 'error', 'error': str(ex)}))
"""
def load_opml(path):
tree = ET.parse(path)
feeds = []
for o in tree.iter("outline"):
if "xmlUrl" in o.attrib:
feeds.append({
"name": o.attrib.get("text") or o.attrib.get("title") or "",
"url": o.attrib["xmlUrl"],
"site": o.attrib.get("htmlUrl", ""),
})
return feeds
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--days", type=int, default=7)
parser.add_argument("--timeout", type=int, default=20)
parser.add_argument("--feeds", default=str(DEFAULT_OPML))
parser.add_argument("--state", default=str(DEFAULT_STATE))
parser.add_argument("--no-dedup", action="store_true")
args = parser.parse_args()
if not os.path.exists(args.feeds):
print(json.dumps({"error": "OPML not found"}))
sys.exit(1)
feeds = load_opml(args.feeds)
cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
# Load last_seen
last_seen = {}
if not args.no_dedup and os.path.exists(args.state):
with open(args.state) as f:
last_seen = json.load(f)
all_entries = []
errors = []
new_seen = {}
ok = err = 0
for feed in feeds:
try:
result = subprocess.run(
[sys.executable, "-c", FETCHER_CODE, feed["url"]],
capture_output=True, text=True, timeout=args.timeout
)
data = json.loads(result.stdout)
if data["status"] != "ok":
err += 1
errors.append({"feed": feed["name"], "error": data.get("error", "unknown")})
continue
ok += 1
feed_title = data.get("feed_title", feed["name"])
for e in data["entries"]:
key = f'{feed_title}|{e["url"]}'
new_seen[key] = True
if not args.no_dedup and key in last_seen:
continue
pub = e.get("published")
if pub:
pub_dt = datetime.fromisoformat(pub)
if pub_dt < cutoff:
continue
all_entries.append({
"source": feed_title,
"title": e["title"],
"url": e["url"],
"published": pub,
"summary": e["summary"],
})
except subprocess.TimeoutExpired:
err += 1
errors.append({"feed": feed["name"], "error": "timeout"})
except Exception as ex:
err += 1
errors.append({"feed": feed["name"], "error": str(ex)})
# Save state
if not args.no_dedup:
os.makedirs(os.path.dirname(args.state) or ".", exist_ok=True)
with open(args.state, "w") as f:
json.dump(new_seen, f, ensure_ascii=False, indent=2)
all_entries.sort(key=lambda e: e.get("published") or "", reverse=True)
new_by_source = {}
for e in all_entries:
s = e["source"]
new_by_source[s] = new_by_source.get(s, 0) + 1
output = {
"total_feeds": len(feeds),
"ok": ok, "errors": err,
"total_entries": len(all_entries),
"has_new": len(all_entries) > 0,
"new_by_source": new_by_source,
"error_details": errors[:5] if errors else None,
"entries": all_entries[:500],
}
print(json.dumps(output, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,383 @@
#!/usr/bin/env python3
import argparse
import concurrent.futures
import datetime as dt
import html
import json
import re
import signal
import sys
import xml.etree.ElementTree as ET
from email.utils import parsedate_to_datetime
from pathlib import Path
import feedparser
USER_AGENT = "Mozilla/5.0 (compatible; ak-rss-digest/1.0; +https://openai.com)"
ACCEPT = "application/rss+xml, application/atom+xml, application/xml, text/xml, */*;q=0.8"
DEFAULT_TIMEOUT = 15
DEFAULT_WORKERS = 5
DEFAULT_TZ = "Asia/Shanghai"
DEFAULT_DAYS = 7
def local_name(tag):
return tag.rsplit("}", 1)[-1]
def strip_html(raw):
if not raw:
return ""
text = re.sub(r"<[^>]+>", " ", raw)
text = html.unescape(text)
return re.sub(r"\s+", " ", text).strip()
def first_text(parent, names):
for child in parent:
if local_name(child.tag) in names:
text = "".join(child.itertext()).strip()
if text:
return text
return ""
def first_link(entry):
for child in entry:
if local_name(child.tag) != "link":
continue
href = child.attrib.get("href")
rel = child.attrib.get("rel", "alternate")
if href and rel == "alternate":
return href
text = "".join(child.itertext()).strip()
if text:
return text
if href:
return href
return ""
def parse_datetime(raw):
if not raw:
return None
value = raw.strip()
try:
parsed = parsedate_to_datetime(value)
if parsed.tzinfo is None:
return parsed.replace(tzinfo=dt.timezone.utc)
return parsed
except (TypeError, ValueError, IndexError):
pass
normalized = value.replace("Z", "+00:00")
try:
parsed = dt.datetime.fromisoformat(normalized)
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=dt.timezone.utc)
return parsed
def load_feeds(opml_path):
root = ET.parse(opml_path).getroot()
feeds = []
for outline in root.findall(".//outline[@type='rss']"):
feeds.append(
{
"name": outline.attrib.get("text") or outline.attrib.get("title") or "",
"xml_url": outline.attrib["xmlUrl"],
"html_url": outline.attrib.get("htmlUrl", ""),
}
)
return feeds
def fetch_url(url, timeout):
"""Fetch feed content via feedparser, which handles DNS/TLS timeouts properly."""
parsed = feedparser.parse(
url,
agent=USER_AGENT,
request_headers={"Accept": ACCEPT},
)
# Reconstruct raw feed content for XML parsing
# feedparser stores full content in 'headers' and parsed entries
raw = b""
if hasattr(parsed, "headers") and parsed.headers:
content_type = parsed.headers.get("content-type", "")
else:
content_type = ""
final_url = parsed.get("href", url)
return raw, final_url, content_type
def parse_atom(root, feed_meta):
feed_title = first_text(root, {"title"}) or feed_meta["name"]
entries = []
for entry in root:
if local_name(entry.tag) != "entry":
continue
published_raw = first_text(entry, {"published", "updated", "issued", "created"})
entries.append(
{
"feed_name": feed_title,
"feed_url": feed_meta["xml_url"],
"site_url": feed_meta["html_url"],
"title": first_text(entry, {"title"}) or "(untitled)",
"link": first_link(entry),
"published_raw": published_raw,
"published_at": parse_datetime(published_raw),
"summary": strip_html(first_text(entry, {"summary", "content"})),
}
)
return entries
def parse_rss(root, feed_meta):
channel = None
if local_name(root.tag) == "rss":
for child in root:
if local_name(child.tag) == "channel":
channel = child
break
elif local_name(root.tag) in {"RDF", "rdf"}:
channel = root
else:
channel = root
feed_title = first_text(channel, {"title"}) or feed_meta["name"]
entries = []
for item in channel.iter():
if local_name(item.tag) != "item":
continue
published_raw = first_text(item, {"pubDate", "published", "date", "updated"})
entries.append(
{
"feed_name": feed_title,
"feed_url": feed_meta["xml_url"],
"site_url": feed_meta["html_url"],
"title": first_text(item, {"title"}) or "(untitled)",
"link": first_link(item) or first_text(item, {"guid"}),
"published_raw": published_raw,
"published_at": parse_datetime(published_raw),
"summary": strip_html(first_text(item, {"description", "encoded", "content", "summary"})),
}
)
return entries
def parse_feed(content, feed_meta):
root = ET.fromstring(content)
tag = local_name(root.tag)
if tag == "feed":
return parse_atom(root, feed_meta)
if tag in {"rss", "RDF", "rdf"}:
return parse_rss(root, feed_meta)
raise ValueError(f"Unsupported feed root tag: {root.tag}")
def fetch_feed(feed_meta, timeout):
"""Fetch and parse a feed using feedparser (handles timeouts reliably)."""
try:
parsed = feedparser.parse(
feed_meta["xml_url"],
agent=USER_AGENT,
request_headers={"Accept": ACCEPT},
)
if parsed.bozo and not parsed.entries:
raise Exception(f"Feed parse error: {parsed.bozo_exception}")
entries = []
for entry in parsed.entries:
entries.append({
"feed_name": parsed.feed.get("title", feed_meta["name"]),
"feed_url": feed_meta["xml_url"],
"site_url": feed_meta["html_url"],
"title": entry.get("title", "(untitled)"),
"link": entry.get("link", ""),
"published_raw": entry.get("published", ""),
"published_at": parse_datetime(entry.get("published", "")),
"summary": strip_html(entry.get("summary", "") or entry.get("description", "")),
})
return {
"feed": feed_meta,
"status": "ok",
"final_url": parsed.get("href", feed_meta["xml_url"]),
"content_type": parsed.headers.get("content-type", "") if hasattr(parsed, "headers") and parsed.headers else "",
"entries": entries,
}
except Exception as exc:
return {
"feed": feed_meta,
"status": "error",
"error": f"{type(exc).__name__}: {exc}",
"entries": [],
}
def serialize_item(item, target_tz):
published_at = item["published_at"]
published_local = published_at.astimezone(target_tz) if published_at else None
return {
"feed_name": item["feed_name"],
"feed_url": item["feed_url"],
"site_url": item["site_url"],
"title": item["title"],
"link": item["link"],
"published_raw": item["published_raw"],
"published_at": published_at.isoformat() if published_at else None,
"published_local": published_local.isoformat() if published_local else None,
"summary": item["summary"],
}
def format_markdown(payload):
target_label = payload.get("target_date")
if payload.get("days", 1) > 1:
target_label = f"{payload['target_date']} minus {payload['days'] - 1} day(s)"
lines = [
f"# RSS items for {target_label} ({payload['timezone']})",
"",
f"- Feeds checked: {payload['feed_count']}",
f"- Feeds failed: {len(payload['errors'])}",
f"- Matching items: {len(payload['items'])}",
"",
]
if payload["errors"]:
lines.extend(["## Feed errors", ""])
for error in payload["errors"]:
lines.append(f"- {error['feed_name']}: {error['error']}")
lines.append("")
if payload["items"]:
lines.extend(["## Items", ""])
for item in payload["items"]:
lines.append(f"### {item['title']}")
lines.append(f"- Feed: {item['feed_name']}")
lines.append(f"- Published: {item['published_local'] or item['published_raw'] or 'unknown'}")
lines.append(f"- Link: {item['link']}")
if item["summary"]:
lines.append(f"- Summary: {item['summary']}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def main():
skill_dir = Path(__file__).resolve().parent.parent
parser = argparse.ArgumentParser(description="Fetch recent items from the configured RSS bundle.")
parser.add_argument("--feeds-file", default=str(skill_dir / "references" / "feeds.opml"))
parser.add_argument("--date", help="Target end date in YYYY-MM-DD. Default: current date in target timezone.")
parser.add_argument(
"--days",
type=int,
default=DEFAULT_DAYS,
help=f"Number of days to include ending on --date. Default: {DEFAULT_DAYS}.",
)
parser.add_argument("--limit", type=int, help="Optional maximum number of items to return after sorting.")
parser.add_argument("--timezone", default=DEFAULT_TZ)
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
parser.add_argument("--workers", type=int, default=10)
parser.add_argument("--format", choices=("json", "markdown"), default="json")
args = parser.parse_args()
if args.days < 1:
raise SystemExit("--days must be at least 1")
if args.limit is not None and args.limit < 1:
raise SystemExit("--limit must be at least 1")
try:
target_tz = dt.ZoneInfo(args.timezone)
except Exception as exc:
raise SystemExit(f"Invalid timezone '{args.timezone}': {exc}")
if args.date:
target_date = dt.date.fromisoformat(args.date)
else:
target_date = dt.datetime.now(target_tz).date()
start_date = target_date - dt.timedelta(days=args.days - 1)
feeds = load_feeds(args.feeds_file)
results = []
overall_timeout = max(args.timeout * 2, 60)
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as executor:
futures = [executor.submit(fetch_feed, feed, args.timeout) for feed in feeds]
try:
for future in concurrent.futures.as_completed(futures, timeout=overall_timeout):
try:
results.append(future.result(timeout=args.timeout))
except Exception as exc:
# Shouldn't happen with our fetch_feed, but safety net
pass
except concurrent.futures.TimeoutError:
# Collect whatever completed, mark rest as errors
for future in futures:
if future.done():
try:
results.append(future.result(timeout=0))
except Exception:
pass
else:
future.cancel()
items = []
errors = []
for result in results:
if result["status"] != "ok":
errors.append(
{
"feed_name": result["feed"]["name"],
"feed_url": result["feed"]["xml_url"],
"error": result["error"],
}
)
continue
for entry in result["entries"]:
published_at = entry["published_at"]
if not published_at:
continue
published_local = published_at.astimezone(target_tz)
published_date = published_local.date()
if published_date < start_date or published_date > target_date:
continue
if not entry["link"]:
continue
items.append(serialize_item(entry, target_tz))
items.sort(
key=lambda item: (
item["published_local"] or "",
item["feed_name"].lower(),
item["title"].lower(),
),
reverse=True,
)
errors.sort(key=lambda err: err["feed_name"].lower())
if args.limit is not None:
items = items[: args.limit]
payload = {
"start_date": start_date.isoformat(),
"target_date": target_date.isoformat(),
"days": args.days,
"timezone": args.timezone,
"feed_count": len(feeds),
"items": items,
"errors": errors,
}
if args.format == "markdown":
sys.stdout.write(format_markdown(payload))
else:
json.dump(payload, sys.stdout, ensure_ascii=True, indent=2)
sys.stdout.write("\n")
if __name__ == "__main__":
if not hasattr(dt, "ZoneInfo"):
from zoneinfo import ZoneInfo # type: ignore
dt.ZoneInfo = ZoneInfo # type: ignore[attr-defined]
main()
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Wrapper for ak-rss-digest: add state-based deduplication on top of the real fetch script."""
import json
import subprocess
import sys
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REAL_SCRIPT = os.path.join(SCRIPT_DIR, "fetch_today_feed_items.py")
STATE_FILE = os.path.join(os.path.dirname(SCRIPT_DIR), "references", "last_seen.json")
# Run the real script
result = subprocess.run(
[sys.executable, REAL_SCRIPT] + sys.argv[1:],
capture_output=True, text=True
)
if result.returncode != 0:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
data = json.loads(result.stdout)
entries = data.get("entries", [])
# Load last seen
last_seen = {}
if os.path.exists(STATE_FILE):
with open(STATE_FILE) as f:
last_seen = json.load(f)
# Filter: keep only entries not seen before (by URL)
new_entries = []
new_seen = {}
for e in entries:
url = e.get("url", "")
source = e.get("source", "")
if url:
key = f"{source}|{url}"
new_seen[key] = True
if key not in last_seen:
new_entries.append(e)
# Group new entries by source
new_by_source = {}
for e in new_entries:
src = e.get("source", "unknown")
new_by_source[src] = new_by_source.get(src, 0) + 1
# Output filtered results
output = {
**data,
"entries": new_entries,
"has_new": len(new_entries) > 0,
"new_by_source": new_by_source,
"total_fetched": len(entries),
}
# Save state
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(new_seen, f, ensure_ascii=False, indent=2)
print(json.dumps(output, ensure_ascii=False, indent=2))