RSS Tech News Aggregator with AI Classification — Electron Desktop Widget
TechPulse is a desktop widget (Electron) that aggregates RSS feeds from tech news sites, classifies each article into categories using AI (Claude API or any local OpenAI-compatible runtime), and displays them in a compact, always-on-top interface.
Target machine: Intel Core Ultra 5 238V (Lunar Lake), Intel AI Boost NPU, Intel Arc 130V GPU, Windows 11.
Use OpusPlan mode as default (/model opusplan). This automatically routes:
- Opus → planning, architecture decisions, reasoning
- Sonnet → code generation, implementation, edits
Below is the explicit assignment for each project task. When working on a specific task, switch to the recommended model if not already using OpusPlan.
| Task | Why Opus |
|---|---|
| AI classification prompt design & iteration | Prompt engineering requires deep reasoning about edge cases, category boundaries, multilingual titles |
| AI Provider orchestrator logic | Complex fallback chains, error handling, auto-detection of runtimes — needs careful state management |
| IPC bridge architecture | Security-sensitive: context isolation, preload scripts, type-safe channel definitions |
| SQLite schema design & migrations | Data modeling with deduplication constraints, indexes, cross-feed normalization |
| Electron window management | Widget mode (alwaysOnTop, frameless, tray), window state persistence, edge cases on Windows |
| Debugging complex issues | Multi-process debugging (Main ↔ Renderer), race conditions in RSS refresh + AI classification pipeline |
| Security review | API key storage, CSP headers, contextIsolation, no nodeIntegration in renderer |
| Performance optimization | Batch sizing, virtual scrolling, SQLite query optimization, memory leaks in long-running widget |
| Architecture decisions & trade-offs | When facing a "how should we..." question, always use Opus |
| Task | Why Sonnet |
|---|---|
| Project scaffolding | electron-vite setup, package.json, tsconfig, vite.config — well-known patterns |
| React components (UI) | CategoryTabs, ArticleList, ArticleCard, FeedManager, SettingsPanel, StatusBar — standard React + Tailwind |
| CSS / Tailwind styling | Visual styling, dark/light theme, animations, responsive widget layout |
| RSS Service implementation | rss-parser integration, fetch + parse — straightforward async code |
| Claude Provider implementation | Anthropic SDK call — simple API wrapper |
| Local Provider implementation | fetch() to OpenAI-compatible endpoint — REST client pattern |
| Database service (CRUD) | better-sqlite3 queries, insert/update/delete — routine DB operations |
| Scheduler service | node-cron setup — minimal logic |
| IPC handler implementations | Once bridge is designed (Opus), implementing each handler is mechanical |
| Feed defaults & presets | Static data: RSS URLs, local runtime presets — config files |
| Electron-builder config | YAML config for packaging — known pattern |
| Unit tests | Jest/Vitest test files — pattern-based generation |
| Documentation & README | Markdown generation from known context |
| Bug fixes (simple) | Typos, missing imports, straightforward logic errors |
| Refactoring | Extract function, rename, move file — mechanical transformations |
New task arrives
│
├── "How should we design/architect X?" ──→ OPUS
├── "Why is X broken / not working?"
│ ├── Simple (missing import, typo) ──→ SONNET
│ └── Complex (race condition, state) ──→ OPUS
├── "Implement X component/service" ──→ SONNET
├── "Write the prompt for AI classification" ──→ OPUS
├── "Add a button / style / animation" ──→ SONNET
├── "Review security of X" ──→ OPUS
└── Default ──→ SONNET (90% of tasks)
- Runtime: Electron 33+ (with Vite)
- Frontend: React 18 + TypeScript + Tailwind CSS
- Build: electron-vite (Vite for both main & renderer)
- RSS: rss-parser (npm)
- AI Cloud: @anthropic-ai/sdk (Claude Sonnet API)
- AI Local: Any OpenAI-compatible REST endpoint (Ollama, OpenVINO OVMS, LM Studio, GAIA, Foundry Local, llama.cpp)
- Database: better-sqlite3
- Scheduler: node-cron
- Packaging: electron-builder
techpulse/
├── CLAUDE.md ← You are here
├── package.json
├── vite.config.ts
├── electron-builder.yml
├── src/
│ ├── main/ # Electron Main Process
│ │ ├── index.ts # App entry, window creation, tray
│ │ ├── ipc-handlers.ts # IPC bridge definitions
│ │ ├── services/
│ │ │ ├── rss.service.ts # Fetch + parse RSS feeds
│ │ │ ├── ai.service.ts # AI orchestrator (provider switching, fallback)
│ │ │ ├── claude.provider.ts # Anthropic API provider
│ │ │ ├── local.provider.ts # Unified local provider (OpenAI-compat)
│ │ │ ├── scheduler.service.ts # Cron-based refresh
│ │ │ └── db.service.ts # SQLite operations
│ │ └── utils/
│ │ ├── dedup.ts # Article deduplication (URL normalization)
│ │ ├── feed-defaults.ts # Default RSS feed list
│ │ └── local-presets.ts # Runtime presets (Ollama, OVMS, LM Studio...)
│ ├── renderer/ # React Frontend
│ │ ├── App.tsx
│ │ ├── index.html
│ │ ├── components/
│ │ │ ├── CategoryTabs.tsx
│ │ │ ├── ArticleList.tsx
│ │ │ ├── ArticleCard.tsx
│ │ │ ├── FeedManager.tsx
│ │ │ ├── SettingsPanel.tsx
│ │ │ ├── StatusBar.tsx
│ │ │ └── TitleBar.tsx # Custom frameless titlebar
│ │ ├── hooks/
│ │ │ ├── useArticles.ts
│ │ │ ├── useFeeds.ts
│ │ │ └── useSettings.ts
│ │ └── styles/
│ │ └── globals.css
│ ├── shared/ # Types shared Main ↔ Renderer
│ │ ├── types.ts
│ │ └── categories.ts
│ └── preload/
│ └── index.ts # Secure context bridge
└── resources/
├── icon.png
└── tray-icon.png
4 tables: feeds, articles, categories, settings.
Key constraints:
articleshas UNIQUE(feed_id, guid) for per-feed deduplicationarticles.categorystores the slug fromcategories.slugarticles.ai_providertracks which provider classified ('claude' | 'local')settingsis key-value with these critical keys:ai_provider: 'claude' | 'local'local_preset: 'ollama' | 'openvino' | 'lmstudio' | 'gaia' | 'foundry' | 'llamacpp' | 'custom'local_api_url: URL of local runtimelocal_api_format: 'ollama' | 'openai'local_model_name: model identifier on the runtime
Two providers, one interface:
interface AIProvider {
name: 'claude' | 'local';
classify(articles: { id: number; title: string; description?: string }[]): Promise<ClassificationResult[]>;
isAvailable(): Promise<boolean>;
}- ClaudeProvider: Uses @anthropic-ai/sdk, calls Claude Sonnet
- LocalProvider: Unified REST client, speaks both Ollama native API and OpenAI-compatible API
- Has presets for: Ollama (:11434), OpenVINO OVMS (:8000), LM Studio (:1234), GAIA (:8899), Foundry (:5272), llama.cpp (:8080)
- User can also set a custom endpoint
If primary provider fails → try the other. Log the fallback. Never silently fail.
ai-ml, hardware, os, security, software, cloud, mobile, gaming, business, science
Default batch size: 25 articles per API call. Classify only unclassified articles (WHERE category IS NULL).
width: 420, height: 680(default widget size)frame: false(custom titlebar)alwaysOnTop: true(widget mode)resizable: true(min 320x400, max 600xscreen)contextIsolation: true,nodeIntegration: false
- Context menu: Show/Hide, Refresh, Settings, Quit
- Tooltip: article count summary
- All Main ↔ Renderer communication via
contextBridge.exposeInMainWorld - No
remotemodule - API keys stored in SQLite (local file), never in renderer process
- CSP headers in index.html
- Strict mode enabled
- No
any— use proper types fromsrc/shared/types.ts - Async/await everywhere (no raw .then())
- Error handling: try/catch with typed errors, never swallow errors silently
- Functional components only, with hooks
- Tailwind for all styling (no separate CSS modules)
- No inline styles except dynamic values (opacity, colors from DB)
- Files: kebab-case for utils, PascalCase for components (
ArticleCard.tsx,dedup.ts) - Variables/functions: camelCase
- Types/Interfaces: PascalCase, prefixed with 'I' only for interfaces if ambiguous
- DB columns: snake_case
- IPC channels:
domain:actionformat (articles:get-by-category,feeds:add)
- Conventional commits:
feat:,fix:,refactor:,docs:,chore: - One feature per commit
- No large binary files (use .gitignore for node_modules, dist, *.db)
- Scaffold Electron + Vite + React + Tailwind project → Sonnet
- Design & create SQLite schema (feeds, articles, categories, settings) → Opus then Sonnet
- Implement db.service.ts (all CRUD operations) → Sonnet
- Implement rss.service.ts (fetch + parse with rss-parser) → Sonnet
- Design AI classification prompt → Opus
- Implement claude.provider.ts → Sonnet
- Implement ai.service.ts (orchestrator) → Opus (fallback logic)
- Implement IPC handlers → Opus (design) then Sonnet (implementation)
- Build preload/index.ts (context bridge) → Sonnet
- Build UI: TitleBar, CategoryTabs, ArticleList, ArticleCard, StatusBar → Sonnet
- Wire up UI hooks (useArticles, useFeeds, useSettings) → Sonnet
- Test end-to-end: fetch RSS → classify → display → click opens browser → Opus (debug)
- Implement local.provider.ts (unified, with presets) → Opus (design) then Sonnet (code)
- Build SettingsPanel with provider toggle, presets, test connection → Sonnet
- Build FeedManager (add/remove/toggle feeds) → Sonnet
- Implement scheduler.service.ts (node-cron auto-refresh) → Sonnet
- Implement dedup.ts (cross-feed URL normalization) → Sonnet
- Add Tray icon + context menu → Sonnet
- Dark/light/system theme support → Sonnet
- Test & optimize OpenVINO OVMS on Intel NPU → Opus (troubleshooting)
- Auto-detect running local runtimes at startup → Opus (logic) then Sonnet (code)
- Add animations & transitions → Sonnet
- Desktop notifications for new articles → Sonnet
- Keyboard shortcuts → Sonnet
- OPML import/export → Sonnet
- electron-builder packaging config → Sonnet
- Security review of entire app → Opus
Ars Technica https://feeds.arstechnica.com/arstechnica/index
The Verge https://www.theverge.com/rss/index.xml
TechCrunch https://techcrunch.com/feed/
Hacker News https://hnrss.org/frontpage
Tom's Hardware https://www.tomshardware.com/feeds/all
Bleeping Computer https://www.bleepingcomputer.com/feed/
The Register https://www.theregister.com/headlines.atom
9to5Mac https://9to5mac.com/feed/
Android Authority https://www.androidauthority.com/feed/
Phoronix https://www.phoronix.com/rss.php
VentureBeat https://venturebeat.com/feed/
Wired https://www.wired.com/feed/rss
MIT Tech Review https://www.technologyreview.com/feed/
KrebsOnSecurity https://krebsonsecurity.com/feed/
const LOCAL_PRESETS = {
ollama: { url: 'http://localhost:11434', format: 'ollama', model: 'mistral' },
openvino: { url: 'http://localhost:8000', format: 'openai', model: 'OpenVINO/Phi-3.5-mini-instruct' },
lmstudio: { url: 'http://localhost:1234', format: 'openai', model: 'loaded-model' },
gaia: { url: 'http://localhost:8899', format: 'openai', model: 'llama3' },
foundry: { url: 'http://localhost:5272', format: 'openai', model: 'phi-3.5-mini' },
llamacpp: { url: 'http://localhost:8080', format: 'openai', model: 'default' },
};# Development
npm run dev # Start Electron + Vite HMR
# Build & Package
npm run build # Compile TypeScript + bundle
npm run package # electron-builder → installer
# Database
# Schema auto-created on first run by db.service.ts- Language: Code and comments in English. UI text in English (i18n later).
- No overengineering: Start simple, iterate. No Redux, no Prisma — just React hooks + better-sqlite3.
- Widget first: The app must feel light. No heavy frameworks. Fast startup.
- Offline-capable: Cache articles in SQLite. If no AI provider available, show articles without categories.
- Full architecture document: See
architecture-rss-aggregator.mdfor detailed schemas, wireframes, pipeline diagrams, and cost analysis.