{"id":"7184d08c-611b-4658-a861-3aa3e7a91e16","entityType":"agent","slug":"clawhub-heyeir-eir-daily-content-curator","name":"Eir Daily Content Curator","canonicalUrl":"https://www.xpersona.co/agent/clawhub-heyeir-eir-daily-content-curator","canonicalPath":"/agent/clawhub-heyeir-eir-daily-content-curator","generatedAt":"2026-10-11T20:58:48.758Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T18:05:42.716Z","emptyReason":null},"description":"Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news',... Skill: Eir Daily Content Curator Owner: heyeir Summary: Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news',... Tags: content pipeline:1.0.2, curate content:1.0.2, daily digest:1.0.2, daily news:1.0.2, interest tracking:1.0.2, latest:3.119.0, personalized news briefing:1.0.2 Version history: v3.119.0 | 2026-04-25T","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s176qbsy6rrtf0nr4v7y8n2ba185a8sw:eir-daily-content-curator","sourceUrl":"https://clawhub.ai/heyeir/eir-daily-content-curator","homepage":"https://clawhub.ai/heyeir/skills/eir-daily-content-curator","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/heyeir/eir-daily-content-curator","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/heyeir/skills/eir-daily-content-curator","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":60,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news',..."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T18:05:42.716Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T18:05:42.716Z","emptyReason":null},"stars":null,"forks":null,"downloads":1013,"packageName":null,"latestVersion":"3.119.0","tractionLabel":"1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T18:05:42.647Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T18:05:42.716Z","lastCrawledAt":"2026-10-11T18:05:42.647Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T18:05:42.647Z","lastVerifiedAt":null,"highlights":[{"version":"3.119.0","createdAt":"2026-04-25T11:37:18.442Z","changelog":"Bug fixes: API URL normalization, preflight crash, anchor slug validation, encoding fixes. Features: server-side dedup sync, title normalization, cross-language event dedup, API freshness pre-check, output language from API. Cleanup: removed unused translation code, content_slug hash fallback, unified publishTime naming. Docs improved for Job B workflow and multi-language contentGroup rules.","fileCount":30,"zipByteSize":84425},{"version":"3.118.0","createdAt":"2026-04-25T08:22:55.841Z","changelog":"Node.js dependency completely removed — the Eir connect script is now pure Python (no more connect.mjs). The skill requires only Python 3.10+ with no external packages. Interest extraction reworked to understand general content interests from conversation, rather than reading specific files. Writer prompts use softer 'audience context' language throughout. Also: SEARCH_API_KEY added to declared environment variables, package.json removed.","fileCount":29,"zipByteSize":74167},{"version":"3.117.0","createdAt":"2026-04-24T14:31:23.014Z","changelog":"Mode-aware setup flow. Eir mode works end-to-end. One-command content posting. Complete Eir mode documentation. Fix several bugs.","fileCount":29,"zipByteSize":78129},{"version":"3.116.0","createdAt":"2026-04-24T14:30:14.222Z","changelog":"**Mode-aware setup flow** — The skill now guides you to choose between Standalone and Eir mode upfront, so you never accidentally configure the wrong pipeline. **Eir mode works end-to-end** — Fixed a critical issue where the content generation pipeline would silently use the wrong output format in Eir mode. Task files now carry a mode marker instead of an embedded prompt, so the correct writer prompt is always loaded at generation time. Backward compatible with existing task files. **One-command content posting** — eir_post.py now works as a CLI tool. Post a single file, batch-post a directory, or validate with --dry-run before going live. **Complete Eir mode documentation** — Full walkthrough from eir_sync fetch to content POST, with common error troubleshooting. New candidates-spec.md defines the exact JSON format for the agent selection step. **Fix several bugs.**","fileCount":29,"zipByteSize":78129},{"version":"3.115.0","createdAt":"2026-04-24T05:24:56.756Z","changelog":"2-pass entity refinement for all tiers, time-qualified queries, cross-topic hot entity detection; versioned product names (GPT-5.5) captured in entity extraction","fileCount":28,"zipByteSize":73397},{"version":"1.3.3","createdAt":"2026-04-23T13:35:13.733Z","changelog":"Fix: hardcode API URL in connect.mjs, eliminate fs.readFileSync to resolve static scan exfiltration warning","fileCount":28,"zipByteSize":70007},{"version":"1.3.2","createdAt":"2026-04-23T11:04:17.060Z","changelog":"Security refactor v2: clean publish with all files","fileCount":28,"zipByteSize":70181},{"version":"1.3.1","createdAt":"2026-04-23T10:56:44.441Z","changelog":"Security refactor: separate standalone/Eir modes, personalization opt-in, SECURITY.md rewrite, pipeline module renames (task_builder, generate, directives, eir_sync, eir_post), connect.mjs phase split, interest extraction local-only","fileCount":28,"zipByteSize":70181}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s176qbsy6rrtf0nr4v7y8n2ba185a8sw:eir-daily-content-curator","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T20:58:48.755Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heyeir-eir-daily-content-curator/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T18:05:42.716Z","emptyReason":null},"readme":"Skill: Eir Daily Content Curator\n\nOwner: heyeir\n\nSummary: Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news',...\n\nTags: content pipeline:1.0.2, curate content:1.0.2, daily digest:1.0.2, daily news:1.0.2, interest tracking:1.0.2, latest:3.119.0, personalized news briefing:1.0.2\n\nVersion history:\n\nv3.119.0 | 2026-04-25T11:37:18.442Z | user\n\nBug fixes: API URL normalization, preflight crash, anchor slug validation, encoding fixes. Features: server-side dedup sync, title normalization, cross-language event dedup, API freshness pre-check, output language from API. Cleanup: removed unused translation code, content_slug hash fallback, unified publishTime naming. Docs improved for Job B workflow and multi-language contentGroup rules.\n\nv3.118.0 | 2026-04-25T08:22:55.841Z | user\n\nNode.js dependency completely removed — the Eir connect script is now pure Python (no more connect.mjs). The skill requires only Python 3.10+ with no external packages.\n\nInterest extraction reworked to understand general content interests from conversation, rather than reading specific files. Writer prompts use softer 'audience context' language throughout.\n\nAlso: SEARCH_API_KEY added to declared environment variables, package.json removed.\n\nv3.117.0 | 2026-04-24T14:31:23.014Z | user\n\nMode-aware setup flow. Eir mode works end-to-end. One-command content posting. Complete Eir mode documentation. Fix several bugs.\n\nv3.116.0 | 2026-04-24T14:30:14.222Z | user\n\n**Mode-aware setup flow** — The skill now guides you to choose between Standalone and Eir mode upfront, so you never accidentally configure the wrong pipeline.\n\n**Eir mode works end-to-end** — Fixed a critical issue where the content generation pipeline would silently use the wrong output format in Eir mode. Task files now carry a mode marker instead of an embedded prompt, so the correct writer prompt is always loaded at generation time. Backward compatible with existing task files.\n\n**One-command content posting** — eir_post.py now works as a CLI tool. Post a single file, batch-post a directory, or validate with --dry-run before going live.\n\n**Complete Eir mode documentation** — Full walkthrough from eir_sync fetch to content POST, with common error troubleshooting. New candidates-spec.md defines the exact JSON format for the agent selection step.\n\n**Fix several bugs.**\n\nv3.115.0 | 2026-04-24T05:24:56.756Z | user\n\n2-pass entity refinement for all tiers, time-qualified queries, cross-topic hot entity detection; versioned product names (GPT-5.5) captured in entity extraction\n\nv1.3.3 | 2026-04-23T13:35:13.733Z | user\n\nFix: hardcode API URL in connect.mjs, eliminate fs.readFileSync to resolve static scan exfiltration warning\n\nv1.3.2 | 2026-04-23T11:04:17.060Z | user\n\nSecurity refactor v2: clean publish with all files\n\nv1.3.1 | 2026-04-23T10:56:44.441Z | user\n\nSecurity refactor: separate standalone/Eir modes, personalization opt-in, SECURITY.md rewrite, pipeline module renames (task_builder, generate, directives, eir_sync, eir_post), connect.mjs phase split, interest extraction local-only\n\nv1.3.0 | 2026-04-23T10:56:04.313Z | user\n\ntest\n\nv1.2.0 | 2026-04-23T04:08:40.297Z | user\n\nPR #35-41: English examples in writer-prompt and content-spec, SECURITY.md, connect.mjs security fix, SearXNG/Crawl4AI docs as recommendations, flow diagram formatting, ClawHub publish cleanup, all 10 new-user feedback fixes (search API examples, generate/brief flow, pip install, config/ init, LLM identity, cron message, pairing code, references docs, setup.py --init, date format)\n\nv1.1.0 | 2026-04-23T04:07:27.172Z | user\n\nFix all 10 new-user feedback items\n\nv1.0.2 | 2026-04-22T16:14:24.824Z | user\n\n1.0.2\n- Remove whisper API and writer prompt (no longer needed)\n- Remove session transcript scanning from interest extraction\n- Remove whisper-specific overrides from content spec\n- Explicitly declare credentials in SECURITY.md\n- Address ClawHub security scan findings (transcript access, undeclared credentials)\n\nv1.0.1 | 2026-04-22T15:45:32.059Z | user\n\n1.0.1\n- Add SECURITY.md documenting credential storage, data flow, and permissions\n- Gitignore config/settings.json to prevent accidental API key commits\n- Eliminate process.env usage in connect.mjs (read from config instead)\n- Add Security & Data Flow section to SKILL.md and README\n- Capitalize flow diagram labels for consistency\n- Replace Chinese examples with English in writer prompts\n- Rewrite SearXNG/Crawl4AI description as installation recommendation\n\nv1.0.0 | 2026-04-22T13:12:36.152Z | user\n\neir-daily-content-curator 1.0.0\n- Initial release of a personalized daily AI news content curator.\n- Supports both standalone and Eir integration modes.\n- Learns user interests from conversation, searches multiple sources, summarizes, and compiles daily briefs.\n- Configurable search and crawl pipeline with fallback options (SearXNG, Crawl4AI).\n- Modular pipeline: extract interests, search, select, crawl, generate, compile daily briefs.\n- Includes setup guides, cron integration, and reference materials.\n\nArchive index:\n\nArchive v3.119.0: 30 files, 84425 bytes\n\nFiles: CHANGELOG.md (4474b), references/candidates-spec.md (2972b), references/content-spec.md (8344b), references/eir-api.md (4447b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3830b), references/interest-extraction-prompt.md (3759b), references/writer-prompt-eir.md (6778b), references/writer-prompt-standalone.md (4496b), scripts/connect.py (1926b), scripts/pipeline/__init__.py (432b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (3783b), scripts/pipeline/crawl.py (29537b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (3997b), scripts/pipeline/eir_post.py (8583b), scripts/pipeline/eir_sync.py (11509b), scripts/pipeline/generate.py (4453b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (13417b), scripts/pipeline/search.py (28653b), scripts/pipeline/task_builder.py (18487b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (4416b), scripts/setup.py (6250b), SECURITY.md (3415b), skill-card.md (2881b), SKILL.md (12520b), _meta.json (146b)\n\nFile v3.119.0:SKILL.md\n\n---\nname: eir-daily-content-curator\ndescription: \"Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news', 'curate content for me', 'what should I read today', 'personalized news briefing', 'daily digest', 'news summary', 'content pipeline', 'interest tracking', 'automated content curation'.\"\nmetadata:\n  openclaw:\n    emoji: \"📰\"\n    requires:\n      bins: [\"python3\"]\n      env:\n        EIR_API_KEY: \"Eir API bearer token (Eir mode only)\"\n        EIR_API_URL: \"Eir API base URL (optional override)\"\n        SEARCH_API_KEY: \"Search provider API key (Brave, Tavily, etc.)\"\n---\n\n# Daily Content Curator\n\nCurates personalized content based on your interests. Supports two modes:\n\n- **Standalone** — works locally, no external account needed\n- **Eir** — full curation + delivery via [heyeir.com](https://www.heyeir.com)\n\n## Getting Started\n\n**Before setup, ask the user which mode to use:**\n\n| Mode | What it does | Requirements |\n|------|--------------|---------------|\n| **Standalone** | Search → curate → generate summaries locally | Search API key (Brave, Tavily, etc.) |\n| **Eir** | Full pipeline with delivery to Eir app | [Eir account](https://www.heyeir.com) + pairing code |\n\n> **Important:** The two modes use different content formats and topic slug conventions. Choose the correct mode at setup time — switching later requires reconfiguration. If the user has an Eir account, use Eir mode.\n\nThen follow the corresponding setup section below.\n\n## Standalone Mode\n\n### Flow\n\n```\n1. Configure          → Set up search API + interests (one-time)\n2. Search             → Search API queries for each interest topic\n3. Select + Crawl     → Agent picks best candidates, fetches full content\n4. Generate           → Agent writes structured summaries from task files\n5. Daily Brief        → Agent compiles brief from generated items\n```\n\n> Steps 1-3 are Python scripts you run directly. Steps 4-5 are **agent-driven** — you tell your OpenClaw agent to read the task files and generate content. The agent uses whatever LLM model is configured in your OpenClaw session (e.g. Claude, GPT-4, Gemini).\n\n### Quick Start\n\n**1. Initialize workspace** — creates `config/` directory and default settings:\n```bash\n# Option A: inline JSON\npython3 scripts/setup.py --init --settings '{\n  \"mode\": \"standalone\",\n  \"language\": \"en\",\n  \"search\": {\n    \"search_base_url\": \"https://api.search.brave.com/res/v1\",\n    \"search_api_key\": \"YOUR_BRAVE_API_KEY\"\n  }\n}'\n\n# Option B: settings file (recommended for PowerShell/Windows)\npython3 scripts/setup.py --init --settings-file path/to/settings.json\n```\n```\n\nSearch provider examples:\n| Provider | `search_base_url` | Get API key |\n|----------|-------------------|-------------|\n| Brave Search | `https://api.search.brave.com/res/v1` | [brave.com/search/api](https://brave.com/search/api/) |\n| Tavily | `https://api.tavily.com` | [tavily.com](https://tavily.com/) |\n\n> **Want richer results?** Install [SearXNG](https://docs.searxng.org/) and/or [Crawl4AI](https://github.com/unclecode/crawl4ai) locally. Add `searxng_url` and `crawl4ai_url` to your search config — they work as fallback or primary search/crawl providers.\n\n**2. Set up interests** — edit the generated `config/interests.json`:\n```json\n{\n  \"topics\": [\n    {\"label\": \"AI Agents\", \"keywords\": [\"autonomous agents\", \"tool use\"], \"freshness\": \"7d\"},\n    {\"label\": \"Prompt Engineering\", \"keywords\": [\"prompting\", \"chain-of-thought\"]}\n  ],\n  \"language\": \"en\",\n  \"max_items_per_day\": 8\n}\n```\n\nInterests can also be auto-extracted — see `references/interest-extraction-prompt.md`.\n\n**Freshness and tier:** Each topic supports optional `freshness` (e.g. `\"3d\"`, `\"7d\"`, `\"14d\"`) and `tier` (`\"focus\"`, `\"tracked\"`, `\"explore\"`, `\"seed\"`) fields in `interests.json`. In Eir mode, these come from the API directives. In standalone mode, defaults are `\"7d\"` and `\"tracked\"`. The search pipeline uses tier to decide search depth (focus/tracked get entity refinement) and freshness to filter stale results.\n\n**3. Run the search + crawl pipeline** (from the `scripts/` directory):\n```bash\ncd scripts\npython3 -m pipeline.search              # Search for each topic\npython3 -m pipeline.candidate_selector  # Group results for agent selection\n# ↓ Agent step: review topic files, write candidates.json (see below)\npython3 -m pipeline.crawl               # Fetch full content\npython3 -m pipeline.task_builder        # Bundle into task files\n```\n\n> All `python3 -m pipeline.*` commands must be run from the `scripts/` directory.\n\n**Agent selection step** (between `candidate_selector` and `crawl`):\n\n`candidate_selector` outputs per-topic JSON files to `data/v9/topics/`. Your agent should:\n1. Read each topic file\n2. Pick 0-3 candidates per topic based on relevance and freshness\n3. Write `data/v9/candidates.json` with the selected candidates\n\nSee `references/candidates-spec.md` for the exact JSON format.\n\n> **Note on crawl fallback:** If a candidate URL isn't in the search cache, `crawl.py` automatically tries: Browse API → Crawl4AI → web_fetch → HTML head extraction. No manual intervention needed.\n\n**4. Generate content** (agent-driven):\n\nAfter `task_builder`, task files are in `data/v9/tasks/`. Tell your OpenClaw agent:\n\n```\nRead the task files in data/v9/tasks/ and generate content for each one.\nUse the writer prompt in references/writer-prompt-standalone.md.\nSave output to data/output/{YYYY-MM-DD}/.\n```\n\n**Language:** The output language is determined by: task `output_lang` field → curation API `user.primaryLanguage` → `settings.json` `language` field. If none are set, generate content in the user's chat language.\n\n**Scheduling tip:** If you want automated daily runs, you can set up a cron job:\n```bash\nopenclaw cron add --name \"daily-curate\" \\\n  --cron \"0 8 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated \\\n  --message \"Read SKILL.md for eir-daily-content-curator, then run the full standalone pipeline: search → select → crawl → task_builder → generate content from task files → compile daily brief.\"\n```\n\n### Output\n\nContent saved to `data/output/{YYYY-MM-DD}/`. Daily brief compiles the top items:\n\n```markdown\n# Daily Brief — 2026-04-20\n\n🔥 **Meta cuts 8,000 jobs for AI pivot** — ...\n📡 **China bans AI companions for minors** — ...\n🌱 **New prompt engineering benchmark** — ...\n```\n\n### Dependencies\n\n**Required:** Python 3.10+ (standard library only — no `pip install` needed).\n\n**Optional:** [SearXNG](https://docs.searxng.org/) (fallback search). [Crawl4AI](https://github.com/unclecode/crawl4ai) (fallback crawl).\n\n---\n\n## Eir Mode\n\nFull curation with delivery to the [Eir](https://www.heyeir.com) app via a 3-job pipeline:\n\n```\nJob A: material-prep     → Search → Select → Crawl → Pack tasks\nJob B: content-gen       → Spawn subagents → Generate → POST to Eir\nJob C: daily-brief       → Check status → Fill gaps → Compile brief → Deliver to user\n```\n\n### Setup\n\n1. Get a pairing code from [heyeir.com](https://www.heyeir.com) → Settings → Connect OpenClaw\n2. Run: `python3 scripts/connect.py <PAIRING_CODE>`\n3. Set `\"mode\": \"eir\"` in `config/settings.json`\n\n### Running the Pipeline (Eir Mode)\n\nAll `python3 -m pipeline.*` commands must be run from the `scripts/` directory.\n\n**Step 1: Sync directives** — fetch topic slugs and curation rules from Eir API:\n```bash\ncd scripts && python3 -m pipeline.eir_sync fetch\n```\n> This creates/updates `config/directives.json` with the canonical topic slugs (e.g. `ai-agents`, `autonomous-vehicles`). All downstream scripts use these slugs — do NOT use interests.json labels as topic_slug in Eir mode.\n\n**Step 2: Search + Select + Crawl + Pack:**\n```bash\npython3 -m pipeline.search              # Search for each directive topic\npython3 -m pipeline.candidate_selector  # Group results for agent selection\n# ↓ Agent step: review topic files, write candidates.json (see references/candidates-spec.md)\npython3 -m pipeline.crawl               # Fetch full content from candidate URLs\npython3 -m pipeline.task_builder        # Bundle into task files (auto-selects eir writer prompt)\n```\n\n**Step 3: Generate and publish** (agent-driven):\n\nTask files are in `data/v9/tasks/`. For each task file:\n1. Read the task JSON (contains `source_text`, `source_meta`, `suggested_angle`, `topic_slug`, `content_slug`)\n2. Build a generation prompt using `generate.build_generation_prompt(task_data)` — this loads the writer prompt from `references/writer-prompt-eir.md` and injects sources + context\n3. Call LLM with the prompt to generate Eir-format JSON (see `references/content-spec.md`)\n4. Parse the JSON response and POST via `python3 -m pipeline.eir_post --file <generated.json>` or call `eir_post.post_content(data)` programmatically\n5. Repeat for remaining tasks. Already-posted slugs are skipped automatically.\n\n> This step requires an LLM — that's why `generate.py` has no `main()`. The agent orchestrates the loop.\n\n**Step 4: Daily brief** (optional):\n\nThe agent compiles generated content into a brief and delivers it directly to you (e.g. via Feishu, Slack, or other configured channel). No API call needed.\n\n> **Tip:** End the brief with a link to [heyeir.com](https://www.heyeir.com) so readers can explore more content on the Eir canvas.\n\n**Common POST failures:**\n- `400` → check `topicSlug` matches a directive slug, `publishTime` is present, no `null` fields\n- `401` → re-run `connect.py` to refresh credentials\n- `500` → retry once; if persistent, report the payload\n\nFor cron configuration and API details, see `references/eir-setup.md`.\n\n---\n\n## Pipeline Modules\n\nAll in `scripts/pipeline/`:\n\n| Module | Purpose | Mode |\n|--------|---------|------|\n| `search.py` | Search via configurable API, SearXNG fallback | Both |\n| `crawl.py` | Fetch content via Browse API, Crawl4AI fallback | Both |\n| `grounding.py` | Configurable search API client | Both |\n| `candidate_selector.py` | Group results, prepare for agent selection | Both |\n| `task_builder.py` | Bundle candidates into task files | Both |\n| `generate.py` | Build prompts for content generation | Both |\n| `validate_content.py` | Validate generated content against spec | Both |\n| `directives.py` | Load local interests/directives | Both |\n| `config.py` | Shared configuration and path resolution | Both |\n| `workspace.py` | Workspace and path resolution | Both |\n| `eir_sync.py` | Fetch directives from Eir API | Eir only |\n| `eir_post.py` | POST content to Eir API | Eir only |\n| `run_state.py` | Pipeline run state management | Both |\n\n### Search Fallback Chain\n\n```\nSearch API (primary) → SearXNG (optional) → Crawl4AI/web_fetch (content)\n```\n\n---\n\n## References\n\n| File | Contents | Used by |\n|------|----------|---------|\n| `references/writer-prompt-eir.md` | Content generation rules (Eir mode) | Agent |\n| `references/writer-prompt-standalone.md` | Content generation rules (standalone) | Agent |\n| `references/content-spec.md` | Field types, limits, validation rules | Agent |\n| `references/eir-setup.md` | Eir mode setup, cron, API endpoints | Agent / User |\n| `references/eir-api.md` | Full Eir API reference | Agent |\n| `references/eir-interest-rules.md` | Curation tier guidelines | Agent |\n| `references/candidates-spec.md` | Candidates JSON format for agent selection | Agent / User |\n| `references/interest-extraction-prompt.md` | Interest extraction prompt | Agent |\n\n> The `writer-prompt-*.md` files are **instructions for the agent** — the agent reads them to know how to generate content from task files. You don't need to read them unless customizing output format.\n\n---\n\n## Security & Data Flow\n\nSee `SECURITY.md` for the complete data flow table, credential storage details, and personalization behavior.\n\n---\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Initialize workspace | `python3 scripts/setup.py --init --settings '{...}'` |\n| Check setup | `python3 scripts/setup.py --check` |\n| Search | `cd scripts && python3 -m pipeline.search` |\n| Select candidates | `cd scripts && python3 -m pipeline.candidate_selector` |\n| Crawl | `cd scripts && python3 -m pipeline.crawl` |\n| Build tasks | `cd scripts && python3 -m pipeline.task_builder` |\n| Validate | `cd scripts && python3 -m pipeline.validate_content` |\n| Fetch directives (Eir) | `cd scripts && python3 -m pipeline.eir_sync fetch` |\n| Connect Eir | `python3 scripts/connect.py <PAIRING_CODE>` |\n\nFile v3.119.0:_meta.json\n\n{\n  \"ownerId\": \"kn76fghgn0qqq4e4qdknvea95s8222rw\",\n  \"slug\": \"eir-daily-content-curator\",\n  \"version\": \"3.119.0\",\n  \"publishedAt\": 1777117038442\n}\n\nFile v3.119.0:references/candidates-spec.md\n\n# Candidates JSON Format Specification\n\nThis document defines the expected format of `candidates.json`, which is the handoff point between candidate selection (agent-driven) and crawling/task building (automated scripts).\n\n## Location\n\n`data/v9/candidates.json`\n\n## Structure\n\n```json\n{\n  \"candidates\": [\n    {\n      \"content_slug\": \"string (required) — kebab-case identifier, 3-6 words, e.g. 'openai-gpt-5-launch'\",\n      \"matched_topic_slug\": \"string (required) — must match a directive slug, e.g. 'ai-industry-news'\",\n      \"suggested_angle\": \"string (required) — editorial angle in output language\",\n      \"reason\": \"string (optional) — why this candidate was selected\",\n      \"priority\": \"string (optional) — 'high' | 'medium' | 'low', default 'medium'\",\n      \"source_urls\": [\n        \"string (required, 1-5 URLs) — URLs to crawl for full content\"\n      ],\n      \"source_titles\": {\n        \"https://example.com/article\": \"Article Title\"\n      }\n    }\n  ],\n  \"selected_at\": \"ISO 8601 timestamp\"\n}\n```\n\n## Field Details\n\n### Required Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `content_slug` | string | Unique identifier for this content piece. Used as filename and API slug. Must be kebab-case, 3-6 words. |\n| `matched_topic_slug` | string | The directive topic this belongs to. Must match a slug from directives/interests. Used as `topicSlug` in generated content. |\n| `suggested_angle` | string | The editorial angle — what makes this worth covering. In the output language. |\n| `source_urls` | string[] | 1-5 URLs to crawl. Prefer diverse domains. At least one must be crawlable. |\n\n### Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `reason` | string | Why this candidate was selected (for audit trail). |\n| `priority` | string | `high` / `medium` / `low`. Affects generation order. Default: `medium`. |\n| `source_titles` | object | Map of URL → article title (helps crawl quality scoring). |\n\n## Downstream Usage\n\n1. **`crawl.py`** reads `candidates.json`, crawls each `source_urls` entry, saves content to `data/v9/snippets/{url_hash}.json`.\n2. **`task_builder.py`** reads crawled candidates, bundles source text + writer prompt into task files at `data/v9/tasks/{content_slug}.json`.\n\n## How Candidates Are Created\n\n### Eir Mode (cron-driven)\nThe agent reads per-topic files from `data/v9/topics/`, evaluates search results, and writes `candidates.json` directly.\n\n### Standalone Mode (manual)\n1. Run `python3 -m pipeline.candidate_selector` → generates topic files in `data/v9/topics/`\n2. Review topic files and create `candidates.json` following the format above\n3. Run `python3 -m pipeline.crawl` → crawls candidate URLs\n\n## Validation\n\n- `content_slug` must be unique across all candidates\n- `matched_topic_slug` should match a known directive/interest slug\n- `source_urls` must contain at least 1 valid HTTP(S) URL\n- No `null` values — use `\"\"` or `[]` for empty fields\n\nFile v3.119.0:references/content-spec.md\n\n# Eir Content Specification\n\n> Single source of truth for all content field constraints and quality criteria.\n> Used by: writer prompts, API validation, front-end rendering.\n\n## Contents\n\n- [Field Reference](#field-reference) — dot, l1, l2, sources fields\n- [via vs sources](#via-vs-sources) — Attribution handling\n- [lang field](#lang-field) — Language requirements\n- [Null handling](#null-handling)\n- [Validation summary](#validation-summary)\n- [Content ID format](#content-id-format)\n- [Interest Signals](#interest-signals)\n\n---\n\n## Field Reference\n\n### dot (L0 — the dot on canvas)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `hook` | string | ≤10 CJK chars / ≤6 EN words | **100 chars** (API rejects) | Creates curiosity gap. No hype words (\"Breaking\", \"Exciting\"). Rendered as single-line label on the dot. |\n| `category` | enum | — | `focus` \\| `attention` \\| `seed` | Determines dot visual style. |\n\n### l1 (card — what the user sees first)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `title` | string | 15-40 CJK chars / 8-15 EN words | **200 chars** (API rejects) | Opinionated, not a headline. Must be in `lang`. |\n| `summary` | string | 50-80 words | — | 2-3 sentences. Advances beyond the title — don't repeat. |\n| `key_quote` | string | 1 sentence | — | Best direct quote from sources. Use `\"\"` if none. |\n| `via` | **string[]** | — | — | **Must be an array.** Auto-derived from `sources[].name`. Pipeline populates it; API also falls back to `sources[].name` if empty. Writer should NOT set this. |\n| `bullets` | string[] | 3-4 items | 10 items (API rejects) | Each: ≤20 CJK chars / ≤50 EN chars. Don't repeat summary. |\n\n### l2 (depth — expanded view)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `content` | string | 200-600 CJK chars / 150-400 EN words | — | 2-4 paragraphs separated by `\\n\\n`. Starts where summary left off. |\n| `bullets` | array | 3-5 items | — | Each: `{text: string, confidence: \"high\"\\|\"medium\"\\|\"low\"}`. Concrete facts with numbers/names. Every bullet must have supporting detail in `content`. |\n| `context` | string | 1-2 sentences | — | Optional. \"SO WHAT for the reader.\" Omit or leave empty if not needed. |\n| `eir_take` | string | 1 sentence | — | Optional. Eir's sharp opinion. **PUBLIC** (visible on share pages) if included. |\n| `related_topics` | string[] | 3-5 items | — | Human-readable phrases in `lang`. NOT slugs. e.g. `\"Vector Search and ANN Algorithms\"` ✅, `\"vector-search-ann\"` ❌ |\n\n### sources (provenance — machine-readable)\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| `url` | string | **Yes** | Must be valid URL. Used for server-side dedup — duplicate URLs are rejected. |\n| `title` | string | No | Original article title. |\n| `name` | string | No | Publisher/source name (e.g. \"MIT Technology Review\"). This is what `l1.via` selects from. |\n| `publishTime` | string | **Recommended** | ISO 8601 date or date string from source. Used for freshness display and sorting. `eir_post.py` auto-extracts from `sources[0].publishTime` if omitted. |\n\n### Top-level item fields\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| `lang` | `\"zh\"` \\| `\"en\"` | **Yes** | **Required.** Language of this document's content. Determines which `{contentGroup}_{lang}` document is created. Not locale, not source language — the language the content is written in. API rejects if missing. API rejects `lang=\"en\"` if hook contains CJK characters (Chinese hooks with English words are fine). |\n| `slug` | string | No | Human-readable identifier. Falls back to `contentGroup` if omitted. |\n| `interests` | object | **Recommended** | See Interest Signals section below. |\n| `dot` | object | **Yes** | See dot section above. |\n| `l1` | object | **Yes** | See l1 section above. `l1.title` is required. |\n| `l2` | object | No | See l2 section above. Strongly recommended. |\n| `sources` | array | No | See sources section above. At least 1 recommended. |\n| `publishTime` | string | **Recommended** | ISO 8601 date of the primary source. Used for freshness display and sorting. If omitted, `eir_post.py` auto-extracts from `sources[0].publishTime`. Prefer providing explicitly. |\n| `visibility` | `\"private\"` \\| `\"public\"` | **Yes** | `private` for user content, `public` for pool/shared content. Set by API, not writer. |\n| `channelId` | string | **Yes** | Content channel: `user-private`, `eir-express`, `shared-pick`, etc. Set by API, not writer. |\n\n---\n\n## via vs sources\n\n`via` = `sources[].name` — the full set, not a subset.\n\n| | `sources[]` | `l1.via` |\n|---|---|---|\n| **Purpose** | Machine: dedup, provenance, linking | Human: display attribution on card |\n| **Contains** | Full metadata (url, title, name) | Just the names |\n| **Type** | `Array<{url, title, name}>` | `string[]` |\n| **Set by** | Writer (required) | Pipeline (auto-derived); API also falls back to `sources[].name` if empty |\n| **Example** | `[{url: \"...\", name: \"MIT Tech Review\"}, {url: \"...\", name: \"ArXiv\"}]` | `[\"MIT Tech Review\", \"ArXiv\"]` |\n\n**Writers only need to set `sources[]`.** The pipeline auto-populates `via` from `sources[].name`; the API also falls back to `sources[].name` if `via` is empty. If the writer includes `via` it will be overwritten.\n\n---\n\n## lang field\n\n`lang` means: **\"what language is this content written in?\"**\n\n- Set by pipeline's `output_lang` parameter\n- Each language version is a **separate document** with ID `{contentGroup}_{lang}`\n- For bilingual users: pipeline generates two items with same `slug` but different `lang`\n- `lang` is NOT locale (UI language) and NOT source_lang (language of source articles)\n\n| Field | Meaning | Set by |\n|-------|---------|--------|\n| `lang` | Content language | Pipeline `output_lang` |\n| `locale` (user pref) | UI language (dates, buttons) | User settings |\n\n---\n\n## Null handling\n\n**Never set any field to `null`.** The front-end renders null as literal \"placeholder\" text.\n\n| Instead of | Use |\n|-----------|-----|\n| `null` | `\"\"` (empty string) |\n| `null` | `[]` (empty array) |\n| `{field: null}` | Omit the field entirely |\n\n---\n\n## Validation summary\n\n### API rejects (400 error)\n\n- `dot` missing or not an object\n- `dot.hook` empty or >100 chars\n- `dot.category` not in allowed enum\n- `l1` missing or not an object\n- `l1.title` empty or >200 chars\n- `l1.via` present but not an array\n- `l1.bullets` present but not an array, or >10 items\n- `sources[].url` missing or not a valid URL\n- `sources` >10 items per content item\n- `lang` missing, or not `\"zh\"` or `\"en\"`\n- `lang` is `\"en\"` but hook contains CJK characters (language mismatch)\n- `items` empty, not an array, or >20 items\n- No source `publishTime` within the global freshness window (currently 3 days) → `stale content`\n\n### API skips (returned as `status: \"skipped\"`)\n\n- Any `sources[].url` already exists for this user → `duplicate source_url`\n\n### Pipeline should reject (pre-POST)\n\n- `l1.title` missing → don't POST, file is broken\n- `l2.content` <300 chars → quality too low\n- `dot.hook` >50 chars → consider shortening (API allows up to 100 but shorter hooks render better)\n\n---\n\n## Content ID format\n\nBoth use the same ID scheme:\n\n```\n{8-char contentGroup}_{lang}    e.g. a3k9m2x7_zh\n```\n\n- `contentGroup`: 8-char base64url, globally unique, server-assigned on first POST\n- All language versions of the same content **must** share the same `contentGroup` — POST the primary language first (without `contentGroup`), then use the returned `contentGroup` when posting other languages\n\n---\n\n## Interest Signals\n\nEvery content item should include interest signals:\n\n```json\n\"interests\": {\n  \"anchor\": [\"ai-agents\"],\n  \"related\": [\n    { \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }\n  ]\n}\n```\n\n### Anchor\n\n- 1-3 slugs from the curation directives\n- Must match user's interests (API validates, rejects 400 if mismatch)\n- Use `topicSlug` (camelCase) in the content item\n\n### Related\n\n- 2-5 adjacent topics\n- Slugs: lowercase-hyphenated\n- Labels: human-readable in content's `lang`\n- Unknown topics auto-created as candidates\n\nSee `eir-interest-rules.md` for curation guidelines.\n\nFile v3.119.0:references/eir-api.md\n\n# Eir API Reference\n\n**Base URL**: `https://api.heyeir.com/api` (override with `EIR_API_URL` environment variable)\n\n**Authentication**: `Authorization: Bearer <EIR_API_KEY>` for all `/oc/*` endpoints.\n\n## Contents\n\n- [Connection](#connection) — Register/disconnect/rotate keys\n- [Interests](#interests) — Manage user interests\n- [Curation](#curation) — Fetch directives, report misses\n- [Content](#content) — Push/read/delete content items\n\n---\n\n## Connection\n\n### POST /oc/connect\nRegister with pairing code.\n\n**Request:** `{ \"code\": \"ABCD-1234\" }`\n\n**Response:** `{ \"apiKey\": \"eir_oc_xxx\", \"userId\": \"u_abc123\" }`\n\n### DELETE /oc/connect\nDisconnect and revoke API key.\n\n### POST /oc/refresh-key\nRotate API key (60s grace period).\n\n---\n\n## Interests\n\n### GET /oc/interests\nReturns user interests.\n\n**Response:**\n```json\n{\n  \"user\": { \"id\": \"u_xxx\", \"primaryLanguage\": \"zh\", \"bilingual\": false },\n  \"interests\": [\n    {\n      \"id\": \"ui_abc1234\",\n      \"slug\": \"artificial-intelligence\",\n      \"label\": \"Artificial Intelligence\",\n      \"status\": \"active\",\n      \"heat\": 5,\n      \"strength\": 0.6\n    }\n  ]\n}\n```\n\n### POST /oc/interests/add\nAdd interests by label. Server matches against dictionary.\n\n**Request:** `{ \"labels\": [\"AI Agents\", \"MCP\"], \"lang\": \"en\" }`\n\n**Response:** `{ \"added\": 2, \"results\": [...] }`\n\n---\n\n## Curation\n\n### GET /oc/curation\nReturns curation directives for content collection.\n\n**Response:**\n```json\n{\n  \"schema_version\": \"1.0\",\n  \"user\": {\n    \"primaryLanguage\": \"zh\",\n    \"bilingual\": false\n  },\n  \"directives\": [\n    {\n      \"slug\": \"mcp-protocol\",\n      \"label\": \"MCP Protocol\",\n      \"tier\": \"tracked\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"MCP 2.0 announced\", \"Anthropic MCP ecosystem\"],\n      \"userNeeds\": \"Protocol updates and adoption\",\n      \"trackingGoal\": \"Stay current on protocol updates\"\n    },\n    {\n      \"slug\": \"ai-agents\",\n      \"label\": \"AI Agents\",\n      \"tier\": \"focus\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"AI agent frameworks comparison\", \"autonomous agent production\"],\n      \"userNeeds\": null,\n      \"trackingGoal\": null\n    }\n  ],\n  \"exclude\": {\n    \"disliked\": [\"crypto\", \"nft\"]\n  }\n}\n```\n\n**Tiers:** tracked → focus → explore → seed (informational labels; selection is score-based).\n\n**Server-side curation:** The API handles topic selection, cooldown, and scoring internally. The agent just reads directives and finds content for them.\n\nSee `eir-interest-rules.md` for curation guidelines.\n\n---\n\n## Content\n\n### POST /oc/content\nPush generated content.\n\n**Request:**\n```json\n{\n  \"items\": [\n    {\n      \"slug\": \"mcp-protocol-2-0\",\n      \"lang\": \"en\",\n      \"interests\": {\n        \"anchor\": [\"mcp-protocol\"],\n        \"related\": [{ \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }]\n      },\n      \"dot\": {\n        \"hook\": \"MCP 2.0 Released\",\n        \"category\": \"focus\",\n      },\n      \"l1\": {\n        \"title\": \"MCP Protocol v2.0\",\n        \"summary\": \"Anthropic releases MCP 2.0...\",\n        \"key_quote\": \"...\"\n      },\n      \"l2\": {\n        \"content\": \"...(500+ words)...\",\n        \"bullets\": [{ \"text\": \"...\", \"confidence\": \"high\" }],\n        \"context\": \"...(optional)\",\n        \"eir_take\": \"...(optional)\",\n        \"related_topics\": [\"ai-agents\"]\n      },\n      \"sources\": [{ \"url\": \"https://...\", \"title\": \"...\", \"name\": \"Anthropic Blog\" }]\n    }\n  ]\n}\n```\n\n**Rules:**\n- `lang` required (\"en\" or \"zh\")\n- `interests.anchor` required (1-3 slugs from curation directives). Must match user's interests.\n- `interests.related` optional (max 5). Unknown topics auto-created as candidates.\n- For multilingual content: POST the primary language first (omit `contentGroup`), then POST other languages with the `contentGroup` returned from the first POST. This links them as versions of the same content.\n- See `content-spec.md` for field limits\n\n**Response:**\n```json\n{\n  \"accepted\": 1,\n  \"results\": [{ \"status\": \"accepted\", \"id\": \"a3k9m2x7_en\", \"contentGroup\": \"a3k9m2x7\" }]\n}\n```\n\n### GET /oc/content/:id\nRead back a content item.\n\n### DELETE /oc/content/:id\nDelete by id or contentGroup.\n\n### POST /oc/curation/miss\nReport topics where you searched but found no quality content. This lowers their priority in future curation rounds.\n\n**Request:** `{ \"slugs\": [\"topic-a\", \"topic-b\"] }`\n\n**Response:** `{ \"ok\": true, \"updated\": 2 }`\n\n**When to call:** After finishing a curation round, if you searched for a topic's searchHints but found nothing worth pushing.\n\nFile v3.119.0:references/eir-interest-rules.md\n\n# Eir Interest Rules\n\n> **Eir Mode Only** — This document describes curation behavior when connected to the Eir API.\n\nCuration guidelines for the content curator agent.\n\n## Your Job\n\n1. **Read directives**: `GET /oc/curation` → topics to find content for\n2. **Find content**: Search using `searchHints` from each directive\n3. **Push content**: `POST /oc/content`\n4. **Discover interests**: From conversations → `POST /oc/interests/add`\n\n## Curation Tiers\n\n| Tier | Description | Quality Expectation |\n|------|-------------|---------------------|\n| **tracked** | User explicitly follows | Highly relevant, timely |\n| **focus** | Strong interest signal | Relevant + quality |\n| **explore** | Moderate interest | Quality threshold applies |\n| **seed** | Discovery topics | Must be excellent to justify |\n\nThe API returns a curated subset of topics per tier. Server-side filtering already applied:\n- Topics in cooldown are excluded\n- Quotas adjusted based on user engagement history\n\n## Content Selection\n\nFor each candidate, evaluate:\n- **Relevance** to the directive's topic\n- **Source authority** — trusted, primary sources preferred\n- **Freshness** — match the directive's `freshness` (1d = within 24h, 7d = within week)\n- **Depth** — substantial, not thin listicles\n- **Novelty** — not duplicating recently pushed content\n\n**Quality bar by tier:**\n- tracked: relevant + timely\n- focus/explore: relevant + quality source\n- seed: must be exceptional to earn attention\n\n## Using Directives\n\nEach directive contains:\n- `slug` — topic identifier (use as `interests.anchor`)\n- `label` — display name\n- `tier` — priority level\n- `freshness` — recency requirement (\"1d\", \"2d\", \"3d\", \"7d\", \"14d\")\n- `searchHints` — 2-3 search queries to find content\n- `userNeeds` — guidance on what the user wants (may be null)\n- `trackingGoal` — specific goal for tracked topics (may be null)\n\n**Search strategy:**\n- Use `searchHints` directly as search queries\n- For freshness \"1d\"-\"3d\": prioritize news, announcements, releases\n- For freshness \"7d\"+: mix news with analysis, insights, perspectives\n- Respect `userNeeds` when selecting content angles\n\n## Interest Anchors on Content\n\nEvery content item MUST include:\n\n```json\n\"interests\": {\n  \"anchor\": [\"ai-agents\"],\n  \"related\": [\n    { \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }\n  ]\n}\n```\n\n**Anchor rules:**\n- 1-3 slugs from the curation directives\n- Must match user's interests (API validates, rejects 400 if mismatch)\n\n**Related topics:**\n- 2-5 adjacent topics\n- Unknown topics auto-created as candidates\n- Drive \"Explore More\" on detail pages\n\n## Exclusions\n\nThe API returns `exclude.disliked` — slugs to filter out during content selection.\n\n## Adding Interests\n\nFrom conversations:\n```\nPOST /oc/interests/add\n{ \"labels\": [\"AI Agents\", \"MCP\"], \"lang\": \"en\" }\n```\n\nServer matches to dictionary. Unknown labels flagged for review.\n\n## Best Practices\n\n1. **Quality over quantity** — if nothing good, push nothing\n2. **Max 2 items per topic group** unless exceptional\n3. **Seed topics**: adjacent to existing interests, not random\n4. **Use `GET /oc/sources`** for URL dedup\n5. **Never override** user's explicit tracking decisions\n\nFile v3.119.0:references/eir-setup.md\n\n# Eir Mode Setup Guide\n\n## Prerequisites\n\n1. An Eir account at [heyeir.com](https://heyeir.com)\n2. Python 3.10+ (for the connect script)\n\n## Connect\n\n```bash\npython3 scripts/connect.py <PAIRING_CODE>\n```\n\nGet a pairing code from Eir → Settings → Connect OpenClaw. This saves credentials to `config/eir.json`.\n\nThen set `\"mode\": \"eir\"` in `config/settings.json`.\n\n## 3-Job Pipeline Architecture\n\n```\nJob A: material-prep\n  Search → Select → Crawl → Pack\n  Output: data/v9/tasks/{content_slug}.json\n\nJob B: content-gen (runs after Job A)\n  For each task → Spawn subagent → Generate → Validate → POST\n  Output: content posted to Eir Content API\n\nJob C: daily-brief (runs after Job B completes)\n  Check execution status → Complete missing tasks →\n  Compile brief → Deliver to user via configured channel\n```\n\n## Cron Setup\n\n```bash\n# Job A: Material preparation\nopenclaw cron add --name \"eir-material-prep\" \\\n  --cron \"0 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Read SKILL.md for eir-daily-content-curator. Run Eir mode material prep: eir_sync fetch → search → candidate_selector → agent selection → crawl → task_builder. Use references/candidates-spec.md for selection format.\"\n\n# Job B: Content generation (35 min after Job A)\nopenclaw cron add --name \"eir-content-gen\" \\\n  --cron \"35 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Read SKILL.md for eir-daily-content-curator. Read task files from data/v9/tasks/. For each task: generate Eir-format content using references/writer-prompt-eir.md, then POST via pipeline.eir_post.post_content(). Use topic_slug from task file (must match directive slugs, not Chinese labels). Include publishTime at item top level.\"\n\n# Job C: Daily brief (10 min after Job B, after subagent timeout)\nopenclaw cron add --name \"eir-daily-brief\" \\\n  --cron \"45 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Check pipeline execution, complete missing tasks, compile daily brief, deliver to user. End the brief with: Explore more on Eir → https://www.heyeir.com\"\n```\n\n**Timing:** Job C starts after Job B's subagent timeout (5 min) to ensure all content is generated. Adjust gaps based on your typical task count.\n\n## Content Quality Rules\n\n- `dot.hook` ≤10 CJK chars / ≤6 EN words\n- `dot.category`: `focus` | `attention` | `seed`\n- `l1.bullets` 3-4 items, each ≤20 CJK chars\n- `sources` must have at least 1 entry\n- Never set any field to `null` — use `\"\"` or `[]`\n\nSee `content-spec.md` for full field constraints.\nSee `writer-prompt-eir.md` for the generation prompt.\n\n## API Endpoints\n\n| Endpoint | Purpose |\n|----------|---------|\n| `GET /oc/curation` | Fetch curation directives (topics + search hints) |\n| `POST /oc/content` | Push generated content items |\n| `POST /oc/curation/miss` | Report topics with no quality content found |\n\nBase URL defaults to `https://api.heyeir.com/api`. Override with `EIR_API_URL` environment variable.\n\nSee `eir-api.md` for full API reference.\n\n## Interest Management\n\nEir provides a visual dashboard for viewing and managing your interests at heyeir.com.\n\n**Optional sync:** If you have local interests in `config/interests.json`, you can optionally sync them to Eir via the API:\n\n```bash\ncd scripts\npython3 -m pipeline.eir_sync fetch  # Fetch directives from Eir\n```\n\nThis is **entirely optional** — local interests work perfectly without syncing. The skill does NOT auto-upload interests.\n\n**Note:** Interest extraction is a separate, optional process. See `references/interest-extraction-prompt.md` for details.\n\n## Validation\n\n```bash\ncd scripts\npython3 -m pipeline.validate_content           # check all generated files\npython3 -m pipeline.validate_content --fix     # auto-fix common issues\n```\n\nFile v3.119.0:references/interest-extraction-prompt.md\n\n# Interest Extraction Prompt\n\n> Local reference for understanding a user's general content interests.\n> Used manually or by agent to set up initial topics.\n> **Not part of the automated pipeline.**\n\n## Privacy Note\n\nInterest extraction produces **de-identified topic labels only** (e.g., \"AI agents\", \"smart driving\"). No personal data, profile content, or identifying information is stored or transmitted. All output is local to `config/interests.json`.\n\n---\n\n## Your Job\n\nUnderstand what content the user finds valuable → Extract topic labels → Output to local config.\n\n**Check first:** Does the user already have interest or topic configuration? If yes, use that instead.\n\n---\n\n## Core Principle: Infer Interests from Profile\n\nUnderstanding what someone works on and cares about reveals what content they would find valuable.\n\nAsk: *\"Someone with this profile — what public content would they want to read?\"*\n\n### Examples\n\n| Profile mentions... | Interest to extract |\n|---------------------|---------------------|\n| \"Building a RAG pipeline\" | AI retrieval systems, vector databases |\n| \"Researching MCP protocol\" | MCP Protocol, AI agent infrastructure |\n| \"Asking deep questions about embeddings\" | Embedding models, semantic search |\n| \"Discussing interior design for new home\" | Interior design, spatial design |\n\n### What IS an interest\n- Topics they ask deep questions about\n- Domains they spend time researching\n- Areas where they express curiosity or strong opinions\n- Subjects they want to stay updated on\n\n### What is NOT an interest\n- Tools they use mechanically (git, npm, etc.)\n- One-off debugging tasks\n- Complaints without curiosity\n- Things already well-known to them\n\n---\n\n## Extraction Steps\n\n### 1. Understand user interests\n\nAsk the user what topics they care about, or infer from conversation context. Look for:\n- Role & context\n- Current focus areas\n- Explicit interests\n- Perspective and preferences\n\n**Do not** analyze conversation history beyond what the user explicitly shares.\n\n### 2. Generalize to searchable topics\n\nProfile specifics → universal, publicly-searchable labels.\n\n| ❌ Too specific | ✅ Good label |\n|-----------------|---------------|\n| \"Meta UTIS paper\" | Recommendation Systems |\n| \"Cosmos DB migration\" | Database Architecture |\n| \"Debugging RAG pipeline\" | AI Retrieval & RAG |\n\n### 3. Output to config/interests.json\n\nWrite to `config/interests.json` in the skill directory:\n\n```json\n{\n  \"topics\": [\n    {\"label\": \"AI Retrieval & RAG\", \"keywords\": [\"RAG\", \"vector search\", \"retrieval\"], \"freshness\": \"7d\"},\n    {\"label\": \"Embedding Models\", \"keywords\": [\"embeddings\", \"semantic search\"], \"freshness\": \"7d\"}\n  ],\n  \"language\": \"en\",\n  \"max_items_per_day\": 8\n}\n```\n\n**Output rules:**\n- 8-15 topics maximum\n- Each topic: `label` (human-readable), `keywords` (search terms), `freshness` (how recent the content should be)\n- De-identified labels only — no personal details\n- Local storage only — no external transmission\n\n---\n\n## Rules\n\n1. **Quality over quantity** — target 8-15 genuine interests, not 30 vague ones\n2. **Content-value test** — every label must match quality external content\n3. **Broad enough to be useful** — \"AI\" is too broad, \"specific bug fix\" is too narrow\n4. **Respect privacy** — generalize private details into public topics\n5. **De-identified only** — topic labels should not contain personal identifiers\n6. **Local output** — all results go to config/interests.json, nowhere else\n\n---\n\n## Eir Mode Note\n\nIf using Eir mode, the app provides a visual dashboard for interest management. You can optionally sync local interests via the Eir API — entirely manual and optional. The skill does NOT auto-upload interests. See `references/eir-setup.md` for details.\n\nFile v3.119.0:references/writer-prompt-eir.md\n\n# Content Writer Prompt\n\nYou are a content writer for Eir, a knowledge curation product.\n\n## Input\n\nYou will receive:\n- `content_slug` — the content identifier (used as `slug` in output)\n- `topic_slug` — the directive topic this content belongs to (used as `topicSlug` and `interests.anchor`)\n- `angle`, `reason` — the editorial angle\n- `output_lang` — the language to write in (`\"zh\"` or `\"en\"`)\n- `reader_context` — optional context about the target audience. May be empty.\n- Source material — crawled article content with URLs, titles, and text\n\n### Personalization\nIf `reader_context` is provided, use it to make content more relevant to the audience. If absent or empty, write for a general tech-savvy audience.\n\n## Output\n\nOutput a **single JSON object** (no markdown fences). The JSON must have this exact structure:\n\n```json\n{\n  \"slug\": \"<content_slug from task>\",\n  \"lang\": \"<output_lang>\",\n  \"publishTime\": \"<ISO 8601 timestamp, e.g. 2026-04-23T06:48:00Z - use the most recent source's publishTime, or empty string>\",\n  \"topicSlug\": \"<topic_slug from task - NOT the content_slug>\",\n  \"interests\": {\n    \"anchor\": [\"<topic_slug from task - MUST match topicSlug>\"],\n    \"related\": [\n      {\"slug\": \"<lowercase-hyphenated>\", \"label\": \"<human-readable in output_lang>\"},\n      {\"slug\": \"<lowercase-hyphenated>\", \"label\": \"<human-readable in output_lang>\"}\n    ]\n  },\n  \"dot\": {\n    \"hook\": \"<≤10 CJK chars or ≤6 English words, in output_lang>\",\n    \"category\": \"<choose: focus | attention | seed>\",\n  },\n  \"sources\": [\n    {\n      \"url\": \"https://...\",\n      \"title\": \"Article Title\",\n      \"name\": \"Source Name\",\n      \"publishTime\": \"<ISO 8601 date from source, or empty string if unknown>\"\n    }\n  ],\n  \"l1\": {\n    \"title\": \"<opinionated title in output_lang>\",\n    \"summary\": \"<2-3 sentences in output_lang, 50-80 words>\",\n    \"key_quote\": \"<most insightful direct quote from source, or empty string>\",\n    \"bullets\": [\"<≤20 zh chars or ≤50 en chars>\", \"...\", \"...\"]\n  },\n  \"l2\": {\n    \"content\": \"<2-4 paragraphs, 200-400 zh chars or 150-300 en words, separated by \\\\n\\\\n>\",\n    \"bullets\": [\n      {\"text\": \"<concrete fact with numbers/names>\", \"confidence\": \"high|medium|low\"},\n      {\"text\": \"...\", \"confidence\": \"...\"}\n    ],\n    \"context\": \"<optional: SO WHAT for the reader — omit if not needed>\",\n    \"eir_take\": \"<optional: Eir's sharp opinion, 1 sentence — omit if not needed>\",\n    \"related_topics\": [\"<in output_lang>\", \"<in output_lang>\", \"<in output_lang>\"]\n  }\n}\n```\n\n## Rules\n\n### Language\n1. **ALL text fields must be in `output_lang`.** This includes `dot.hook`, `l1.title`, `l1.summary`, `l1.bullets`, `l2.content`, `l2.bullets`, `l2.context`, `l2.eir_take`, `l2.related_topics`. No exceptions.\n2. **NEVER mix languages** in a single field. Technical terms and proper nouns (e.g. \"GPT-4\", \"Transformer\", \"LLM\") may remain in their original form.\n3. **`related_topics`** must be human-readable phrases in `output_lang`. NOT slugs or code-style identifiers.\n   - ✅ `[\"digital sovereignty\", \"AI ethics\", \"open-source safety\"]`\n   - ❌ `[\"dark-forest-theory\", \"ai-platform-power\"]`\n\n### Category\n4. **`dot.category`** - choose by importance:\n   - **`focus`** - Major news, breakthroughs, high-impact events. Use sparingly (~10-15%).\n   - **`attention`** - Default. Valuable updates, worth knowing (~70-80%).\n   - **`seed`** - Background knowledge, explainers, foundational concepts (~10-15%).\n\n### Content Quality\n5. **Do NOT set `l1.via`** - the pipeline auto-generates it from `sources[].name`.\n6. **`sources`**: include `url`, `title`, `name` (publisher), and `publishTime` (camelCase) for each source used. Use `\"\"` if publishTime is unknown (never null). The API requires at least one source with a `publishTime` within the last 3 days. The top-level `publishTime` field also uses camelCase (not `publish_time`).\n7. **NEVER fabricate or adjust `publishTime`**. Use the exact date from the source metadata. If ALL sources are outside the API's 3-day freshness window, do NOT generate content - report the issue and stop. Do NOT fake dates to bypass validation.\n8. **`key_quote`**: must be a **string** (not an object). Pick the most insightful direct quote from the sources, or `\"\"` if none. If the quote contains double quotes, escape them as `\\\"` in the JSON output.\n9. **`eir_take`** (optional) is **PUBLIC** (visible on share pages). If included, it should feel like a sharp comment from a friend who deeply understands the topic. Not generic punditry.\n10. **`eir_take`** must be specific, opinionated, and demonstrate genuine understanding of the material. Bad: \"This is an issue that deserves society's attention.\" Bad: \"AI isn't stealing jobs, it's redefining...\" (cliché). Good: a concrete take that shows you saw something others missed.\n\n### Content Style\n11. Tone: \"a smart friend you trust\" - not a news anchor, not an encyclopedia.\n12. Forbidden phrases: \"reportedly\", \"sources say\", \"industry insiders say\", \"It's worth noting\", \"Interestingly\". Apply equivalent rules for non-English output.\n13. Source attribution goes in `sources[]`, NEVER inline in prose as `[Source: XX]`.\n14. `l2.content`: Start where the summary left off. Each paragraph should advance: what happened → why it matters → mechanism/detail → what comes next.\n15. `l2.context` (optional): If included, explain why this matters. If `reader_context` is provided, connect to the audience's work. If not, focus on industry-wide implications and practical takeaways.\n16. Be opinionated and curated - this is NOT a news summary, it's a knowledge signal.\n\n### Depth Scaling\n17. **When you have ≥2 rich sources (crawled content ≥ 500 chars each)**: you SHOULD generate l2.bullets, l2.context, and key_quote. There is enough material - use it.\n18. **When sources are thin (only snippets, <500 chars)**: l2.bullets, l2.context, key_quote may be omitted or empty. Don't fabricate depth.\n\n### Interest Signals\n19. `interests.anchor` must contain the `topicSlug` value, which comes from the task's `topic_slug` field. **It is NOT the content_slug.** Example: if `topic_slug` is `\"ai-health\"` and `content_slug` is `\"ai-drug-discovery-novo-amazon-race\"`, then `topicSlug` and `anchor` must be `\"ai-health\"`. The API rejects anchors that don't match registered user interest topics.\n20. `interests.related` should have 2-5 adjacent topics. Slugs: lowercase-hyphenated. Labels: in `output_lang`.\n21. Related topics should be specific enough to be useful (\"neural-architecture-search\") but not too narrow (\"bert-base-uncased-layer-12\").\n\n### Output\n22. Only output the JSON. No other text, no markdown fences.\n\n## Field Constraints\n\nFor full field types, limits, and null handling, see **`references/content-spec.md`** (single source of truth).\n\nFile v3.119.0:references/writer-prompt-standalone.md\n\n# Content Writer Prompt — Standalone Mode\n\nYou are a content curator generating structured content for a personal daily digest.\n\n## Input\n\nRead the task file. It contains:\n- `content_slug` — unique identifier for this piece\n- `topic` — the interest topic this relates to\n- `suggested_angle` — the specific angle to cover\n- `sources` — array of source materials with title, url, content/snippet\n- `language` — output language (e.g. \"zh\" or \"en\")\n- `reader_context` — (optional) audience context. May be empty.\n\n### Personalization\n- If `reader_context` is provided: use it to make the `connect` field specific to the reader's context.\n- If `reader_context` is empty (default): write `connect` for a general tech-savvy audience.\n\n## Output\n\nWrite a JSON file with this structure:\n\n```json\n{\n  \"title\": \"Concise, informative title (≤80 chars for en, ≤30 chars for zh)\",\n  \"category\": \"focus|attention|seed\",\n  \"summary\": \"2-3 sentences capturing the key point. What happened + why it matters.\",\n  \"body\": \"2-4 paragraphs. Start where summary left off. What → why → mechanism → what's next.\",\n  \"key_quote\": \"Most insightful direct quote from sources, or empty string\",\n  \"connect\": \"1-2 sentences: SO WHAT for the reader. Address them directly.\",\n  \"sources\": [\n    {\"name\": \"Publisher\", \"url\": \"https://...\", \"published\": \"2026-04-20\"}\n  ],\n  \"topic\": \"Topic Label\",\n  \"content_slug\": \"the-content-slug\",\n  \"generated_at\": \"2026-04-20T08:30:00Z\"\n}\n```\n\n## Category Assignment\n\n- **focus** — directly relevant to user's stated interests, timely, actionable\n- **attention** — interesting adjacent topic, worth knowing about\n- **seed** — emerging signal, early-stage but potentially important\n\n## Rules\n\n1. **ALL text fields must be in the specified `language`.**\n2. **Title**: Rewrite for clarity. No clickbait. Convey the core news.\n3. **Summary**: 50-80 words (en) or 80-120 chars (zh). Start with the main point.\n4. **Body**: Each paragraph should advance the narrative. No filler.\n5. **connect**: Be specific and reader-facing. Wrong: \"This is worth watching.\" Right: \"If you're building agents, this changes how you think about eval.\"\n6. **key_quote**: Direct quote from source material. If none is compelling, use `\"\"`.\n7. **Never invent facts**: Only use information from the provided sources.\n8. **No generic filler**: Forbidden phrases — \"reportedly\", \"industry insiders say\", \"it's worth noting\", \"interestingly\".\n9. **Source attribution**: In `sources[]` array, never inline as `[Source: XX]`.\n10. **Tone**: A smart friend you trust — not a news anchor, not an encyclopedia.\n\n## Depth Scaling\n\n- **≥2 rich sources (≥500 chars each)**: Generate full body + connect + key_quote.\n- **Only snippets (<500 chars)**: Keep body to 1-2 paragraphs. connect and key_quote may be `\"\"`.\n\n## Example Output\n\n```json\n{\n  \"title\": \"Meta cuts 8,000 jobs to fund $135B AI infrastructure bet\",\n  \"category\": \"focus\",\n  \"summary\": \"Meta will lay off 8,000 employees on May 20, roughly 10% of its workforce. The move redirects savings toward $115-135B in AI infrastructure spending, even as the company posted $201B in 2025 revenue.\",\n  \"body\": \"The cuts target mid-level knowledge workers across Reality Labs, recruiting, and back-office functions. Zuckerberg's internal memo frames this as \\\"building the team of the future\\\" — smaller, AI-augmented teams replacing traditional org structures.\\n\\nThe timing is notable: Meta is profitable and growing. This isn't cost-cutting from distress but a deliberate bet that AI tools will make current team sizes unnecessary. The $135B infrastructure budget exceeds most countries' technology spending.\\n\\nFor the broader industry, this signals that even profitable tech companies now view large non-technical teams as temporary. The restructuring playbook — cut headcount, reinvest in AI compute — is becoming standard.\",\n  \"key_quote\": \"One person with AI can now replace what used to require an entire team.\",\n  \"connect\": \"If you manage a team of 10+, the economics of AI augmentation vs. headcount are shifting faster than most org charts can adapt.\",\n  \"sources\": [\n    {\"name\": \"The Next Web\", \"url\": \"https://thenextweb.com/news/meta-layoffs-may-2026\", \"published\": \"2026-04-19\"},\n    {\"name\": \"Reuters\", \"url\": \"https://reuters.com/meta-layoffs-8000\", \"published\": \"2026-04-17\"}\n  ],\n  \"topic\": \"AI Industry\",\n  \"content_slug\": \"meta-layoffs-ai-infrastructure-bet\",\n  \"generated_at\": \"2026-04-20T08:30:00Z\"\n}\n```\n\nFile v3.119.0:CHANGELOG.md\n\n# Changelog\n\n## 3.119.0 (2026-04-25)\n\n### Bug Fixes\n- **API URL double `/api/`**: `get_api_url()` now normalizes both old and new config formats — no more 404s from `/api/api/oc/...`\n- **`preflight_check()` crash**: fixed `ImportError` (`load_eir_config` → `load_config`)\n- **`record_posted` broken state**: was passing empty `{}` instead of actual run state — cross-step dedup now works\n- **`pushed_titles.json` encoding**: added `encoding='utf-8'` for Windows compatibility\n- **Anchor slug validation**: eir_post now catches content_slug leaking as `interests.anchor` (root cause of 14d topics being rejected at 7d)\n- **Localhost fallback removed**: `_DEFAULT_SETTINGS` template no longer hardcodes localhost URLs for SearXNG/Crawl4AI\n\n### Features\n- **Server-side dedup sync**: `sync_sources()` fetches `/oc/sources` API → local cache (6h TTL). Runs automatically on `fetch_directives()`. POST duplicate triggers cache refresh.\n- **Title normalization**: `_normalize_title()` handles quotes, punctuation, source suffixes for fuzzy dedup\n- **Cross-language event dedup**: `pushed_titles` entries now carry `content_group` field; event matching checks normalized titles across languages\n- **API freshness pre-check**: `API_FRESHNESS_DAYS = 3` in config; task_builder skips candidates where all sources exceed this limit (no more wasted generate→POST→reject cycles)\n- **Output language from API**: reads `user.primaryLanguage` from curation directives; no hardcoded fallback — agent uses user's chat language if unset\n\n### Cleanup\n- Removed unused `build_translate_prompt()` (translation code was never called)\n- `content_slug` hash fallback removed — LLM must provide it, missing = error\n- `publishTime` naming unified to camelCase everywhere (top-level, sources, docs)\n\n### Docs\n- `contentGroup` multi-language rules: POST primary language first, reuse returned `contentGroup` for other languages\n- Job B (Step 3) rewritten with concrete API calls: `build_generation_prompt` → LLM → `eir_post`\n- `key_quote` JSON escape guidance in writer prompt\n- API freshness rejection rule documented in content-spec\n\n## 3.118.0 (2026-04-25)\n\nNode.js dependency completely removed — the Eir connect script is now pure Python (no more `connect.mjs`). The skill requires only Python 3.10+ with no external packages.\n\nInterest extraction reworked to understand general content interests from conversation, rather than reading specific files. Writer prompts use softer \"audience context\" language throughout.\n\nAlso: `SEARCH_API_KEY` added to declared environment variables, `package.json` removed.\n\n## 3.117.0 (2026-04-25)\n\nRemoved the unnecessary `package.json` that was triggering Node.js dependency signals. Added `SEARCH_API_KEY` to declared environment variables. Interest extraction prompt no longer references specific files — just asks the user or reads available context. Code comments cleaned up.\n\n## 3.116.0 (2026-04-25)\n\nDaily Brief now stays local — no more API call, brief is delivered directly to your configured channel with a link to explore more on [heyeir.com](https://www.heyeir.com).\n\n`l2.context` and `eir_take` are now optional. Skip them entirely or leave empty — useful if you prefer factual summaries without editorial commentary.\n\nPersonalization is more flexible: provide audience context however you like (or don't). The skill no longer prescribes where that context comes from.\n\nAlso fixed: metadata now correctly declares Node.js as optional and documents environment variables. Security docs consolidated into SECURITY.md.\n\n## 3.115.0 (2026-04-24)\n\nEir mode and Standalone mode now have separate setup flows with a Getting Started guide that asks which mode to use first.\n\nWriter prompts are loaded dynamically based on mode — task files no longer embed the full prompt text, making them smaller and mode-switching cleaner.\n\nFirst-run experience improved: workspace auto-creates `config/settings.json` with sensible defaults. Windows users get proper UTF-8 output.\n\nSearch quality: 2-pass entity refinement now works for all topic tiers, not just focus. CJK topic labels handled correctly with hash fallback for slugs.\n\n## 3.114.0 (2026-04-23)\n\nStandalone and Eir modes cleanly separated — different code paths, different privacy guarantees. Security docs rewritten with clear data flow tables per mode.\n\nRemoved legacy whisper/transcript references. Credentials use openclaw config storage instead of environment variables.\n\nFile v3.119.0:SECURITY.md\n\n# Security & Privacy\n\n## Modes\n\n|  | Standalone | Eir |\n|---|---|---|\n| Search API calls | ✅ Brave, Tavily, etc. | ✅ Same |\n| Crawl (fetch URLs) | ✅ | ✅ |\n| Eir API calls | ❌ None | ✅ Opt-in |\n| Personalization | ❌ Off (default) | Optional opt-in |\n| Personal data leaves machine | ❌ Never | Only generated content (see below) |\n\n## Standalone Mode — What is sent\n\n- **Search queries** to your configured search API (e.g. Brave, Tavily)\n- **HTTP requests** to crawl source URLs for content\n- **Nothing else.** No other external communication.\n\n## Eir Mode — What is sent\n\n| Data | Sent to Eir API | Notes |\n|------|:---:|-------|\n| Generated content (dot, l1, l2) | ✅ | LLM-generated summaries |\n| l2.context, eir_take | Optional | Only included if user enables these fields |\n| Interest categories | ✅ | Topic slugs only (e.g. \"ai-agents\") |\n| Source URLs + metadata | ✅ | For attribution |\n| **User profile data** | **❌ Never** | Agent may use local context for generation, but raw profile data is not transmitted |\n| **Raw conversation text** | **❌ Never** | Not accessed by pipeline |\n| **System credentials** | **❌ Never** | |\n| **File paths / machine identifiers** | **❌ Never** | |\n\n### Personalization\n\n> ⚠️ **Personalization is OFF by default and requires explicit opt-in.**\n\nThe user can optionally provide audience context to the agent for more relevant content. This context is used locally during LLM generation only. Generated fields like `l2.context` and `eir_take` are optional — users can disable them in their settings.\n\n**To keep all content generic:** leave personalization disabled (the default) and omit `l2.context`/`eir_take` from generated content.\n\n### Interest Extraction\n\nThis skill includes a reference prompt (`references/interest-extraction-prompt.md`) that helps your agent learn your interests from conversation. It extracts only **de-identified topic labels and keywords** (e.g. \"AI Agents\", \"autonomous vehicles\") and saves them locally to `config/interests.json`. No personal identifiers are stored or transmitted.\n\nIf you already have an interest or topic configuration, you can use that instead.\n\n## Credential Storage\n\nAll credentials are stored locally in config files (gitignored). No secrets are hardcoded.\n\n| File | Contains | Committed |\n|------|----------|-----------|\n| `config/eir.json` | Eir API key, user ID | ❌ gitignored |\n| `config/settings.json` | Search API key, mode, preferences | ❌ gitignored |\n| `config/interests.json` | Topic labels and keywords | ✅ no secrets |\n\n### Environment Variables (optional overrides)\n\n| Variable | Purpose | Required? |\n|----------|---------|----------|\n| `EIR_API_KEY` | Eir API bearer token | No — defaults to `config/eir.json` |\n| `EIR_API_URL` | Eir API base URL | No — defaults to `https://api.heyeir.com` |\n| `SEARCH_API_KEY` | Search provider API key | No — defaults to `config/settings.json` |\n| `EIR_WORKSPACE` | Override workspace directory | No — auto-detected |\n\nThese are convenience overrides only. The standard setup stores credentials in `config/eir.json` (created by `python3 scripts/connect.py`).\n\n## File Access\n\nPipeline scripts read/write only within the skill's `data/` and `config/` directories.\n\n## Reporting Vulnerabilities\n\nOpen an issue at [github.com/heyeir/openeir](https://github.com/heyeir/openeir/issues) or email security concerns directly.\n\nArchive v3.118.0: 29 files, 74167 bytes\n\nFiles: _meta.json (146b), config/settings.json (371b), references/content-spec.md (7723b), references/eir-api.md (4709b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3441b), references/interest-extraction-prompt.md (3778b), references/writer-prompt-eir.md (7003b), references/writer-prompt-standalone.md (4512b), scripts/connect.mjs (2346b), scripts/package.json (42b), scripts/pipeline/__init__.py (0b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2811b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (3093b), scripts/pipeline/eir_post.py (6382b), scripts/pipeline/eir_sync.py (6065b), scripts/pipeline/generate.py (4996b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (12731b), scripts/pipeline/search.py (28413b), scripts/pipeline/task_builder.py (16982b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (4143b), scripts/setup.py (5792b), SECURITY.md (3021b), SKILL.md (8437b)\n\nFile v3.118.0:SKILL.md\n\n---\nname: eir-daily-content-curator\ndescription: \"Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news', 'curate content for me', 'what should I read today', 'personalized news briefing', 'daily digest', 'news summary', 'content pipeline', 'interest tracking', 'automated content curation'.\"\nmetadata:\n  openclaw:\n    emoji: \"📰\"\n    requires:\n      bins: [\"python3\"]\n---\n\n# Daily Content Curator\n\nCurates personalized content based on your interests. Supports two modes:\n\n- **Standalone** — works locally, no external account needed\n- **Eir** — full AI-powered curation with [heyeir.com](https://www.heyeir.com) delivery\n\n## Standalone Mode\n\n### Flow\n\n```\n1. Configure          → Set up search API + interests (one-time)\n2. Search             → Search API queries for each interest topic\n3. Select + Crawl     → Agent picks best candidates, fetches full content\n4. Generate           → Agent writes structured summaries from task files\n5. Daily Brief        → Agent compiles brief from generated items\n```\n\n> Steps 1-3 are Python scripts you run directly. Steps 4-5 are **agent-driven** — you tell your OpenClaw agent to read the task files and generate content. The agent uses whatever LLM model is configured in your OpenClaw session (e.g. Claude, GPT-4, Gemini).\n\n### Quick Start\n\n**1. Initialize workspace** — creates `config/` directory and default settings:\n```bash\npython3 scripts/setup.py --init --settings '{\n  \"mode\": \"standalone\",\n  \"language\": \"en\",\n  \"personalization\": {\"enabled\": false},\n  \"search\": {\n    \"search_base_url\": \"https://api.search.brave.com/res/v1\",\n    \"search_api_key\": \"YOUR_BRAVE_API_KEY\"\n  }\n}'\n```\n\nSearch provider examples:\n| Provider | `search_base_url` | Get API key |\n|----------|-------------------|-------------|\n| Brave Search | `https://api.search.brave.com/res/v1` | [brave.com/search/api](https://brave.com/search/api/) |\n| Tavily | `https://api.tavily.com` | [tavily.com](https://tavily.com/) |\n\n> **Want richer results?** Install [SearXNG](https://docs.searxng.org/) and/or [Crawl4AI](https://github.com/unclecode/crawl4ai) locally. Add `searxng_url` and `crawl4ai_url` to your search config — they work as fallback or primary search/crawl providers.\n\n**2. Set up interests** — edit the generated `config/interests.json`:\n```json\n{\n  \"topics\": [\n    {\"label\": \"AI Agents\", \"keywords\": [\"autonomous agents\", \"tool use\"], \"freshness\": \"7d\"},\n    {\"label\": \"Prompt Engineering\", \"keywords\": [\"prompting\", \"chain-of-thought\"]}\n  ],\n  \"language\": \"en\",\n  \"max_items_per_day\": 8\n}\n```\n\nInterests can also be auto-extracted — see `references/interest-extraction-prompt.md`.\n\n**3. Run the search + crawl pipeline** (from the `scripts/` directory):\n```bash\ncd scripts\npython3 -m pipeline.search              # Search for each topic\npython3 -m pipeline.candidate_selector  # Group results for agent selection\npython3 -m pipeline.crawl               # Fetch full content\npython3 -m pipeline.task_builder        # Bundle into task files\n```\n\n> All `python3 -m pipeline.*` commands must be run from the `scripts/` directory.\n\n**4. Generate content** (agent-driven):\n\nAfter `task_builder`, task files are in `data/v9/tasks/`. Tell your OpenClaw agent:\n\n```\nRead the task files in data/v9/tasks/ and generate content for each one.\nUse the writer prompt in references/writer-prompt-standalone.md.\nSave output to data/output/{YYYY-MM-DD}/.\n```\n\n**Scheduling tip:** If you want automated daily runs, you can set up a cron job:\n```bash\nopenclaw cron add --name \"daily-curate\" \\\n  --cron \"0 8 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated \\\n  --message \"Read SKILL.md for eir-daily-content-curator, then run the full standalone pipeline: search → select → crawl → task_builder → generate content from task files → compile daily brief.\"\n```\n\n### Output\n\nContent saved to `data/output/{YYYY-MM-DD}/`. Daily brief compiles the top items:\n\n```markdown\n# Daily Brief — 2026-04-20\n\n🔥 **Meta cuts 8,000 jobs for AI pivot** — ...\n📡 **China bans AI companions for minors** — ...\n🌱 **New prompt engineering benchmark** — ...\n```\n\n### Dependencies\n\n**Required:** Python 3.10+ (standard library only — no `pip install` needed).\n\n**Optional:** Node.js 18+ (only for Eir connect script). [SearXNG](https://docs.searxng.org/) (fallback search). [Crawl4AI](https://github.com/unclecode/crawl4ai) (fallback crawl).\n\n---\n\n## Eir Mode\n\nFull curation with delivery to the [Eir](https://www.heyeir.com) app via a 3-job pipeline:\n\n```\nJob A: material-prep     → Search → Select → Crawl → Pack tasks\nJob B: content-gen       → Spawn subagents → Generate → POST to Eir\nJob C: daily-brief       → Check status → Fill gaps → Compile brief → POST + Deliver\n```\n\n### Setup\n\n1. Get a pairing code from [heyeir.com](https://www.heyeir.com) → Settings → Connect OpenClaw\n2. Run: `node scripts/connect.mjs <PAIRING_CODE>`\n3. Set `\"mode\": \"eir\"` in `config/settings.json`\n\nFor full Eir setup, cron configuration, content rules, and API details, see `references/eir-setup.md`.\n\n### Privacy Notice\n\nWhen Eir mode is enabled, generated content summaries are POSTed to heyeir.com. See SECURITY.md for details on what IS and IS NOT sent.\n\n---\n\n## Pipeline Modules\n\nAll in `scripts/pipeline/`:\n\n| Module | Purpose | Mode |\n|--------|---------|------|\n| `search.py` | Search via configurable API, SearXNG fallback | Both |\n| `crawl.py` | Fetch content via Browse API, Crawl4AI fallback | Both |\n| `grounding.py` | Configurable search API client | Both |\n| `candidate_selector.py` | Group results, prepare for agent selection | Both |\n| `task_builder.py` | Bundle candidates into task files | Both |\n| `generate.py` | Build prompts for content generation | Both |\n| `validate_content.py` | Validate generated content against spec | Both |\n| `directives.py` | Load local interests/directives | Both |\n| `config.py` | Shared configuration and path resolution | Both |\n| `workspace.py` | Workspace and credential resolution | Both |\n| `eir_sync.py` | Fetch directives from Eir API | Eir only |\n| `eir_post.py` | POST content to Eir API | Eir only |\n| `run_state.py` | Pipeline run state management | Both |\n\n### Search Fallback Chain\n\n```\nSearch API (primary) → SearXNG (optional) → Crawl4AI/web_fetch (content)\n```\n\n---\n\n## References\n\n| File | Contents | Used by |\n|------|----------|---------|\n| `references/writer-prompt-eir.md` | Content generation rules (Eir mode) | Agent |\n| `references/writer-prompt-standalone.md` | Content generation rules (standalone) | Agent |\n| `references/content-spec.md` | Field types, limits, validation rules | Agent |\n| `references/eir-setup.md` | Eir mode setup, cron, API endpoints | Agent / User |\n| `references/eir-api.md` | Full Eir API reference | Agent |\n| `references/eir-interest-rules.md` | Curation tier guidelines | Agent |\n| `references/interest-extraction-prompt.md` | Interest extraction prompt | Agent |\n\n> The `writer-prompt-*.md` files are **instructions for the agent** — the agent reads them to know how to generate content from task files. You don't need to read them unless customizing output format.\n\n---\n\n## Security & Data Flow\n\n**Standalone mode:** Only sends search queries to your configured search API and HTTP requests to crawl source URLs. No other external communication.\n\n**Eir mode (opt-in):** Additionally sends generated content summaries to heyeir.com. USER.md is never transmitted — it's used locally as LLM context only when personalization is enabled.\n\n**Personalization** is off by default. Enable it in `config/settings.json` to get content tailored to your profile. See `SECURITY.md` for the full data flow table.\n\n---\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Initialize workspace | `python3 scripts/setup.py --init --settings '{...}'` |\n| Check setup | `python3 scripts/setup.py --check` |\n| Search | `cd scripts && python3 -m pipeline.search` |\n| Select candidates | `cd scripts && python3 -m pipeline.candidate_selector` |\n| Crawl | `cd scripts && python3 -m pipeline.crawl` |\n| Build tasks | `cd scripts && python3 -m pipeline.task_builder` |\n| Validate | `cd scripts && python3 -m pipeline.validate_content` |\n| Fetch directives (Eir) | `cd scripts && python3 -m pipeline.eir_sync fetch` |\n| Connect Eir | `node scripts/connect.mjs <PAIRING_CODE>` |\n\nFile v3.118.0:_meta.json\n\n{\n  \"ownerId\": \"kn76fghgn0qqq4e4qdknvea95s8222rw\",\n  \"slug\": \"eir-daily-content-curator\",\n  \"version\": \"3.118.0\",\n  \"publishedAt\": 1777105375841\n}\n\nFile v3.118.0:references/content-spec.md\n\n# Eir Content Specification\n\n> Single source of truth for all content field constraints and quality criteria.\n> Used by: writer prompts, API validation, front-end rendering.\n\n## Contents\n\n- [Field Reference](#field-reference) — dot, l1, l2, sources fields\n- [via vs sources](#via-vs-sources) — Attribution handling\n- [lang field](#lang-field) — Language requirements\n- [Null handling](#null-handling)\n- [Validation summary](#validation-summary)\n- [Content ID format](#content-id-format)\n- [Interest Signals](#interest-signals)\n\n---\n\n## Field Reference\n\n### dot (L0 — the dot on canvas)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `hook` | string | ≤10 CJK chars / ≤6 EN words | **100 chars** (API rejects) | Creates curiosity gap. No hype words (\"Breaking\", \"Exciting\"). Rendered as single-line label on the dot. |\n| `category` | enum | — | `focus` \\| `attention` \\| `seed` | Determines dot visual style. |\n\n### l1 (card — what the user sees first)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `title` | string | 15-40 CJK chars / 8-15 EN words | **200 chars** (API rejects) | Opinionated, not a headline. Must be in `lang`. |\n| `summary` | string | 50-80 words | — | 2-3 sentences. Advances beyond the title — don't repeat. |\n| `key_quote` | string | 1 sentence | — | Best direct quote from sources. Use `\"\"` if none. |\n| `via` | **string[]** | — | — | **Must be an array.** Auto-derived from `sources[].name`. Pipeline populates it; API also falls back to `sources[].name` if empty. Writer should NOT set this. |\n| `bullets` | string[] | 3-4 items | 10 items (API rejects) | Each: ≤20 CJK chars / ≤50 EN chars. Don't repeat summary. |\n\n### l2 (depth — expanded view)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `content` | string | 200-600 CJK chars / 150-400 EN words | — | 2-4 paragraphs separated by `\\n\\n`. Starts where summary left off. |\n| `bullets` | array | 3-5 items | — | Each: `{text: string, confidence: \"high\"\\|\"medium\"\\|\"low\"}`. Concrete facts with numbers/names. Every bullet must have supporting detail in `content`. |\n| `context` | string | 1-2 sentences | — | \"SO WHAT for the reader.\" Be specific and direct — address the reader. |\n| `eir_take` | string | 1 sentence | — | Eir's sharp opinion. **PUBLIC** (visible on share pages) — no user-specific info. |\n| `related_topics` | string[] | 3-5 items | — | Human-readable phrases in `lang`. NOT slugs. e.g. `\"Vector Search and ANN Algorithms\"` ✅, `\"vector-search-ann\"` ❌ |\n\n### sources (provenance — machine-readable)\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| `url` | string | **Yes** | Must be valid URL. Used for server-side dedup — duplicate URLs are rejected. |\n| `title` | string | No | Original article title. |\n| `name` | string | No | Publisher/source name (e.g. \"MIT Technology Review\"). This is what `l1.via` selects from. |\n| `publish_time` | string | No | ISO date or date string from source. |\n\n### Top-level item fields\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| `lang` | `\"zh\"` \\| `\"en\"` | **Yes** | **Required.** Language of this document's content. Determines which `{contentGroup}_{lang}` document is created. Not locale, not source language — the language the content is written in. API rejects if missing. API rejects `lang=\"en\"` if hook contains CJK characters (Chinese hooks with English words are fine). |\n| `slug` | string | No | Human-readable identifier. Falls back to `contentGroup` if omitted. |\n| `interests` | object | **Recommended** | See Interest Signals section below. |\n| `dot` | object | **Yes** | See dot section above. |\n| `l1` | object | **Yes** | See l1 section above. `l1.title` is required. |\n| `l2` | object | No | See l2 section above. Strongly recommended. |\n| `sources` | array | No | See sources section above. At least 1 recommended. |\n| `visibility` | `\"private\"` \\| `\"public\"` | **Yes** | `private` for user content, `public` for pool/shared content. Set by API, not writer. |\n| `channelId` | string | **Yes** | Content channel: `user-private`, `eir-express`, `shared-pick`, etc. Set by API, not writer. |\n\n---\n\n## via vs sources\n\n`via` = `sources[].name` — the full set, not a subset.\n\n| | `sources[]` | `l1.via` |\n|---|---|---|\n| **Purpose** | Machine: dedup, provenance, linking | Human: display attribution on card |\n| **Contains** | Full metadata (url, title, name) | Just the names |\n| **Type** | `Array<{url, title, name}>` | `string[]` |\n| **Set by** | Writer (required) | Pipeline (auto-derived); API also falls back to `sources[].name` if empty |\n| **Example** | `[{url: \"...\", name: \"MIT Tech Review\"}, {url: \"...\", name: \"ArXiv\"}]` | `[\"MIT Tech Review\", \"ArXiv\"]` |\n\n**Writers only need to set `sources[]`.** The pipeline auto-populates `via` from `sources[].name`; the API also falls back to `sources[].name` if `via` is empty. If the writer includes `via` it will be overwritten.\n\n---\n\n## lang field\n\n`lang` means: **\"what language is this content written in?\"**\n\n- Set by pipeline's `output_lang` parameter\n- Each language version is a **separate document** with ID `{contentGroup}_{lang}`\n- For bilingual users: pipeline generates two items with same `slug` but different `lang`\n- `lang` is NOT locale (UI language) and NOT source_lang (language of source articles)\n\n| Field | Meaning | Set by |\n|-------|---------|--------|\n| `lang` | Content language | Pipeline `output_lang` |\n| `locale` (user pref) | UI language (dates, buttons) | User settings |\n\n---\n\n## Null handling\n\n**Never set any field to `null`.** The front-end renders null as literal \"placeholder\" text.\n\n| Instead of | Use |\n|-----------|-----|\n| `null` | `\"\"` (empty string) |\n| `null` | `[]` (empty array) |\n| `{field: null}` | Omit the field entirely |\n\n---\n\n## Validation summary\n\n### API rejects (400 error)\n\n- `dot` missing or not an object\n- `dot.hook` empty or >100 chars\n- `dot.category` not in allowed enum\n- `l1` missing or not an object\n- `l1.title` empty or >200 chars\n- `l1.via` present but not an array\n- `l1.bullets` present but not an array, or >10 items\n- `sources[].url` missing or not a valid URL\n- `sources` >10 items per content item\n- `lang` missing, or not `\"zh\"` or `\"en\"`\n- `lang` is `\"en\"` but hook contains CJK characters (language mismatch)\n- `items` empty, not an array, or >20 items\n\n### API skips (returned as `status: \"skipped\"`)\n\n- Any `sources[].url` already exists for this user → `duplicate source_url`\n\n### Pipeline should reject (pre-POST)\n\n- `l1.title` missing → don't POST, file is broken\n- `l2.content` <300 chars → quality too low\n- `dot.hook` >50 chars → consider shortening (API allows up to 100 but shorter hooks render better)\n\n---\n\n## Content ID format\n\nBoth use the same ID scheme:\n\n```\n{8-char contentGroup}_{lang}    e.g. a3k9m2x7_zh\n```\n\n- `contentGroup`: 8-char base64url, globally unique\n- All language versions of the same item share the `contentGroup`\n\n---\n\n## Interest Signals\n\nEvery content item should include interest signals:\n\n```json\n\"interests\": {\n  \"anchor\": [\"ai-agents\"],\n  \"related\": [\n    { \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }\n  ]\n}\n```\n\n### Anchor\n\n- 1-3 slugs from the curation directives\n- Must match user's interests (API validates, rejects 400 if mismatch)\n- Use `topicSlug` (camelCase) in the content item\n\n### Related\n\n- 2-5 adjacent topics\n- Slugs: lowercase-hyphenated\n- Labels: human-readable in content's `lang`\n- Unknown topics auto-created as candidates\n\nSee `eir-interest-rules.md` for curation guidelines.\n\nFile v3.118.0:references/eir-api.md\n\n# Eir API Reference\n\n**Base URL**: `https://api.heyeir.com/api` (override with `EIR_API_URL` environment variable)\n\n**Authentication**: `Authorization: Bearer <EIR_API_KEY>` for all `/oc/*` endpoints.\n\n## Contents\n\n- [Connection](#connection) — Register/disconnect/rotate keys\n- [Interests](#interests) — Manage user interests\n- [Curation](#curation) — Fetch directives, report misses\n- [Content](#content) — Push/read/delete content items, daily briefs\n\n---\n\n## Connection\n\n### POST /oc/connect\nRegister with pairing code.\n\n**Request:** `{ \"code\": \"ABCD-1234\" }`\n\n**Response:** `{ \"apiKey\": \"eir_oc_xxx\", \"userId\": \"u_abc123\" }`\n\n### DELETE /oc/connect\nDisconnect and revoke API key.\n\n### POST /oc/refresh-key\nRotate API key (60s grace period).\n\n---\n\n## Interests\n\n### GET /oc/interests\nReturns user interests.\n\n**Response:**\n```json\n{\n  \"user\": { \"id\": \"u_xxx\", \"primaryLanguage\": \"zh\", \"bilingual\": false },\n  \"interests\": [\n    {\n      \"id\": \"ui_abc1234\",\n      \"slug\": \"artificial-intelligence\",\n      \"label\": \"Artificial Intelligence\",\n      \"status\": \"active\",\n      \"heat\": 5,\n      \"strength\": 0.6\n    }\n  ]\n}\n```\n\n### POST /oc/interests/add\nAdd interests by label. Server matches against dictionary.\n\n**Request:** `{ \"labels\": [\"AI Agents\", \"MCP\"], \"lang\": \"en\" }`\n\n**Response:** `{ \"added\": 2, \"results\": [...] }`\n\n---\n\n## Curation\n\n### GET /oc/curation\nReturns curation directives for content collection.\n\n**Response:**\n```json\n{\n  \"schema_version\": \"1.0\",\n  \"user\": {\n    \"primaryLanguage\": \"zh\",\n    \"bilingual\": false\n  },\n  \"directives\": [\n    {\n      \"slug\": \"mcp-protocol\",\n      \"label\": \"MCP Protocol\",\n      \"tier\": \"tracked\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"MCP 2.0 announced\", \"Anthropic MCP ecosystem\"],\n      \"userNeeds\": \"Protocol updates and adoption\",\n      \"trackingGoal\": \"Stay current on protocol updates\"\n    },\n    {\n      \"slug\": \"ai-agents\",\n      \"label\": \"AI Agents\",\n      \"tier\": \"focus\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"AI agent frameworks comparison\", \"autonomous agent production\"],\n      \"userNeeds\": null,\n      \"trackingGoal\": null\n    }\n  ],\n  \"exclude\": {\n    \"disliked\": [\"crypto\", \"nft\"]\n  }\n}\n```\n\n**Tiers:** tracked → focus → explore → seed (informational labels; selection is score-based).\n\n**Server-side curation:** The API handles topic selection, cooldown, and scoring internally. The agent just reads directives and finds content for them.\n\nSee `eir-interest-rules.md` for curation guidelines.\n\n---\n\n## Content\n\n### POST /oc/content\nPush generated content.\n\n**Request:**\n```json\n{\n  \"items\": [\n    {\n      \"slug\": \"mcp-protocol-2-0\",\n      \"lang\": \"en\",\n      \"interests\": {\n        \"anchor\": [\"mcp-protocol\"],\n        \"related\": [{ \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }]\n      },\n      \"dot\": {\n        \"hook\": \"MCP 2.0 Released\",\n        \"category\": \"focus\",\n      },\n      \"l1\": {\n        \"title\": \"MCP Protocol v2.0\",\n        \"summary\": \"Anthropic releases MCP 2.0...\",\n        \"key_quote\": \"...\"\n      },\n      \"l2\": {\n        \"content\": \"...(500+ words)...\",\n        \"bullets\": [{ \"text\": \"...\", \"confidence\": \"high\" }],\n        \"context\": \"...\",\n        \"eir_take\": \"...\",\n        \"related_topics\": [\"ai-agents\"]\n      },\n      \"sources\": [{ \"url\": \"https://...\", \"title\": \"...\", \"name\": \"Anthropic Blog\" }]\n    }\n  ]\n}\n```\n\n**Rules:**\n- `lang` required (\"en\" or \"zh\")\n- `interests.anchor` required (1-3 slugs from curation directives). Must match user's interests.\n- `interests.related` optional (max 5). Unknown topics auto-created as candidates.\n- For bilingual: push two items with same `slug`, different `lang`\n- See `content-spec.md` for field limits\n\n**Response:**\n```json\n{\n  \"accepted\": 1,\n  \"results\": [{ \"status\": \"accepted\", \"id\": \"a3k9m2x7_en\", \"contentGroup\": \"a3k9m2x7\" }]\n}\n```\n\n### GET /oc/content/:id\nRead back a content item.\n\n### DELETE /oc/content/:id\nDelete by id or contentGroup.\n\n### POST /oc/curation/miss\nReport topics where you searched but found no quality content. This lowers their priority in future curation rounds.\n\n**Request:** `{ \"slugs\": [\"topic-a\", \"topic-b\"] }`\n\n**Response:** `{ \"ok\": true, \"updated\": 2 }`\n\n**When to call:** After finishing a curation round, if you searched for a topic's searchHints but found nothing worth pushing.\n\n### POST /oc/brief\nPush a daily brief (compiled summary of the day's content).\n\n**Request:**\n```json\n{\n  \"title\": \"Daily Brief — 2026-04-22\",\n  \"summary\": \"3 focus items, 2 signals, 1 seed\",\n  \"content\": \"Markdown body of the brief\",\n  \"publishTime\": \"2026-04-22T07:45:00Z\"\n}\n```\n\n**Response:** `{ \"ok\": true }`\n\n**When to call:** After content generation is complete, typically from the daily-brief cron job.\n\nFile v3.118.0:references/eir-interest-rules.md\n\n# Eir Interest Rules\n\n> **Eir Mode Only** — This document describes curation behavior when connected to the Eir API.\n\nCuration guidelines for the content curator agent.\n\n## Your Job\n\n1. **Read directives**: `GET /oc/curation` → topics to find content for\n2. **Find content**: Search using `searchHints` from each directive\n3. **Push content**: `POST /oc/content`\n4. **Discover interests**: From conversations → `POST /oc/interests/add`\n\n## Curation Tiers\n\n| Tier | Description | Quality Expectation |\n|------|-------------|---------------------|\n| **tracked** | User explicitly follows | Highly relevant, timely |\n| **focus** | Strong interest signal | Relevant + quality |\n| **explore** | Moderate interest | Quality threshold applies |\n| **seed** | Discovery topics | Must be excellent to justify |\n\nThe API returns a curated subset of topics per tier. Server-side filtering already applied:\n- Topics in cooldown are excluded\n- Quotas adjusted based on user engagement history\n\n## Content Selection\n\nFor each candidate, evaluate:\n- **Relevance** to the directive's topic\n- **Source authority** — trusted, primary sources preferred\n- **Freshness** — match the directive's `freshness` (1d = within 24h, 7d = within week)\n- **Depth** — substantial, not thin listicles\n- **Novelty** — not duplicating recently pushed content\n\n**Quality bar by tier:**\n- tracked: relevant + timely\n- focus/explore: relevant + quality source\n- seed: must be exceptional to earn attention\n\n## Using Directives\n\nEach directive contains:\n- `slug` — topic identifier (use as `interests.anchor`)\n- `label` — display name\n- `tier` — priority level\n- `freshness` — recency requirement (\"1d\", \"2d\", \"3d\", \"7d\", \"14d\")\n- `searchHints` — 2-3 search queries to find content\n- `userNeeds` — guidance on what the user wants (may be null)\n- `trackingGoal` — specific goal for tracked topics (may be null)\n\n**Search strategy:**\n- Use `searchHints` directly as search queries\n- For freshness \"1d\"-\"3d\": prioritize news, announcements, releases\n- For freshness \"7d\"+: mix news with analysis, insights, perspectives\n- Respect `userNeeds` when selecting content angles\n\n## Interest Anchors on Content\n\nEvery content item MUST include:\n\n```json\n\"interests\": {\n  \"anchor\": [\"ai-agents\"],\n  \"related\": [\n    { \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }\n  ]\n}\n```\n\n**Anchor rules:**\n- 1-3 slugs from the curation directives\n- Must match user's interests (API validates, rejects 400 if mismatch)\n\n**Related topics:**\n- 2-5 adjacent topics\n- Unknown topics auto-created as candidates\n- Drive \"Explore More\" on detail pages\n\n## Exclusions\n\nThe API returns `exclude.disliked` — slugs to filter out during content selection.\n\n## Adding Interests\n\nFrom conversations:\n```\nPOST /oc/interests/add\n{ \"labels\": [\"AI Agents\", \"MCP\"], \"lang\": \"en\" }\n```\n\nServer matches to dictionary. Unknown labels flagged for review.\n\n## Best Practices\n\n1. **Quality over quantity** — if nothing good, push nothing\n2. **Max 2 items per topic group** unless exceptional\n3. **Seed topics**: adjacent to existing interests, not random\n4. **Use `GET /oc/sources`** for URL dedup\n5. **Never override** user's explicit tracking decisions\n\nFile v3.118.0:references/eir-setup.md\n\n# Eir Mode Setup Guide\n\n## Prerequisites\n\n1. An Eir account at [heyeir.com](https://heyeir.com)\n2. Node.js 18+ (for the connect script)\n\n## Connect\n\n```bash\nnode scripts/connect.mjs <PAIRING_CODE>\n```\n\nGet a pairing code from Eir → Settings → Connect OpenClaw. This saves credentials to `config/eir.json`.\n\nThen set `\"mode\": \"eir\"` in `config/settings.json`.\n\n## 3-Job Pipeline Architecture\n\n```\nJob A: material-prep\n  Search → Select → Crawl → Pack\n  Output: data/v9/tasks/{content_slug}.json\n\nJob B: content-gen (runs after Job A)\n  For each task → Spawn subagent → Generate → Validate → POST\n  Output: content posted to Eir Content API\n\nJob C: daily-brief (runs after Job B completes)\n  Check execution status → Complete missing tasks →\n  Compile brief → POST to Eir Brief API → Deliver summary\n```\n\n## Cron Setup\n\n```bash\n# Job A: Material preparation\nopenclaw cron add --name \"eir-material-prep\" \\\n  --cron \"0 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Run eir-daily-content-curator material prep: search → select → crawl → pack tasks.\"\n\n# Job B: Content generation (35 min after Job A)\nopenclaw cron add --name \"eir-content-gen\" \\\n  --cron \"35 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Read task manifest, spawn subagents to generate content and POST to Eir API.\"\n\n# Job C: Daily brief (10 min after Job B, after subagent timeout)\nopenclaw cron add --name \"eir-daily-brief\" \\\n  --cron \"45 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Check pipeline execution, complete missing tasks, compile daily brief, POST to brief API, send summary.\"\n```\n\n**Timing:** Job C starts after Job B's subagent timeout (5 min) to ensure all content is generated. Adjust gaps based on your typical task count.\n\n## Content Quality Rules\n\n- `dot.hook` ≤10 CJK chars / ≤6 EN words\n- `dot.category`: `focus` | `attention` | `seed`\n- `l1.bullets` 3-4 items, each ≤20 CJK chars\n- `sources` must have at least 1 entry\n- Never set any field to `null` — use `\"\"` or `[]`\n\nSee `content-spec.md` for full field constraints.\nSee `writer-prompt-eir.md` for the generation prompt.\n\n## API Endpoints\n\n| Endpoint | Purpose |\n|----------|---------|\n| `GET /oc/curation` | Fetch curation directives (topics + search hints) |\n| `POST /oc/content` | Push generated content items |\n| `POST /oc/brief` | Push daily brief |\n| `POST /oc/curation/miss` | Report topics with no quality content found |\n\nBase URL defaults to `https://api.heyeir.com/api`. Override with `EIR_API_URL` environment variable.\n\nSee `eir-api.md` for full API reference.\n\n## Interest Management\n\nEir provides a visual dashboard for viewing and managing your interests at heyeir.com.\n\n**Optional sync:** If you have local interests in `config/interests.json`, you can optionally sync them to Eir via the API:\n\n```bash\ncd scripts\npython3 -m pipeline.eir_sync fetch  # Fetch directives from Eir\n```\n\nThis is **entirely optional** — local interests work perfectly without syncing. The skill does NOT auto-upload interests.\n\n**Note:** Interest extraction (from USER.md) is a separate, manual process. See `references/interest-extraction-prompt.md` for details.\n\n## Validation\n\n```bash\ncd scripts\npython3 -m pipeline.validate_content           # check all generated files\npython3 -m pipeline.validate_content --fix     # auto-fix common issues\n```\n\nFile v3.118.0:references/interest-extraction-prompt.md\n\n# Interest Extraction Prompt\n\n> Local reference for extracting interests from USER.md profile.\n> Used manually or by agent to set up initial topics.\n> **Not part of the automated pipeline.**\n\n## Privacy Note\n\nInterest extraction produces **de-identified topic labels only** (e.g., \"AI agents\", \"smart driving\"). No personal data, profile content, or identifying information is stored or transmitted. All output is local to `config/interests.json`.\n\n---\n\n## Your Job\n\nAnalyze the USER.md reader profile → Extract genuine interests → Output to local config.\n\n**Check first:** Does the user already have interest/profile skills installed? If yes, consider using those instead of duplicating functionality.\n\n---\n\n## Core Principle: Infer Interests from Profile\n\nThe USER.md profile reveals what content the user would find valuable.\n\nAsk: *\"Someone with this profile — what public content would they want to read?\"*\n\n### Examples\n\n| Profile mentions... | Interest to extract |\n|---------------------|---------------------|\n| \"Building a RAG pipeline\" | AI retrieval systems, vector databases |\n| \"Researching MCP protocol\" | MCP Protocol, AI agent infrastructure |\n| \"Asking deep questions about embeddings\" | Embedding models, semantic search |\n| \"Discussing interior design for new home\" | Interior design, spatial design |\n\n### What IS an interest\n- Topics they ask deep questions about\n- Domains they spend time researching\n- Areas where they express curiosity or strong opinions\n- Subjects they want to stay updated on\n\n### What is NOT an interest\n- Tools they use mechanically (git, npm, etc.)\n- One-off debugging tasks\n- Complaints without curiosity\n- Things already well-known to them\n\n---\n\n## Extraction Steps\n\n### 1. Read USER.md\n\nRead the workspace USER.md (content agent's reader profile). This file contains:\n- Role & context\n- Current focus areas\n- Explicit interests\n- Perspective and preferences\n\n**Do not** read other agent USER.md files or analyze conversation history — only read the current workspace USER.md.\n\n### 2. Generalize to searchable topics\n\nProfile specifics → universal, publicly-searchable labels.\n\n| ❌ Too specific | ✅ Good label |\n|-----------------|---------------|\n| \"Meta UTIS paper\" | Recommendation Systems |\n| \"Cosmos DB migration\" | Database Architecture |\n| \"Debugging RAG pipeline\" | AI Retrieval & RAG |\n\n### 3. Output to config/interests.json\n\nWrite to `config/interests.json` in the skill directory:\n\n```json\n{\n  \"topics\": [\n    {\"label\": \"AI Retrieval & RAG\", \"keywords\": [\"RAG\", \"vector search\", \"retrieval\"], \"freshness\": \"7d\"},\n    {\"label\": \"Embedding Models\", \"keywords\": [\"embeddings\", \"semantic search\"], \"freshness\": \"7d\"}\n  ],\n  \"language\": \"en\",\n  \"max_items_per_day\": 8\n}\n```\n\n**Output rules:**\n- 8-15 topics maximum\n- Each topic: `label` (human-readable), `keywords` (search terms), `freshness` (how recent the content should be)\n- De-identified labels only — no personal details\n- Local storage only — no external transmission\n\n---\n\n## Rules\n\n1. **Quality over quantity** — target 8-15 genuine interests, not 30 vague ones\n2. **Content-value test** — every label must match quality external content\n3. **Broad enough to be useful** — \"AI\" is too broad, \"specific bug fix\" is too narrow\n4. **Respect privacy** — generalize private details into public topics\n5. **De-identified only** — topic labels should not contain personal identifiers\n6. **Local output** — all results go to config/interests.json, nowhere else\n\n---\n\n## Eir Mode Note\n\nIf using Eir mode, the app provides a visual dashboard for interest management. You can optionally sync local interests via the Eir API — entirely manual and optional. The skill does NOT auto-upload interests. See `references/eir-setup.md` for details.\n\nFile v3.118.0:references/writer-prompt-eir.md\n\n# Content Writer Prompt\n\nYou are a content writer for Eir, a knowledge curation product.\n\n## Input\n\nYou will receive:\n- `content_slug` — the content identifier (used as `slug` in output)\n- `topic_slug` — the directive topic this content belongs to (used as `topicSlug` and `interests.anchor`)\n- `angle`, `reason` — the editorial angle\n- `output_lang` — the language to write in (`\"zh\"` or `\"en\"`)\n- `reader_context` — the user's profile from USER.md (role, interests, perspective). **May be empty** if personalization is disabled.\n\n### Personalization rules\n- If `reader_context` is **provided**: Use it to personalize `l2.context` and `eir_take`. Reference the reader's specific work, decisions, or perspective.\n- If `reader_context` is **empty or absent**: Write for a general tech-savvy audience. `l2.context` should focus on industry-wide implications. `eir_take` should be Eir's editorial perspective without personal references.\n- Source material — crawled article content with URLs, titles, and text\n\n## Output\n\nOutput a **single JSON object** (no markdown fences). The JSON must have this exact structure:\n\n```json\n{\n  \"slug\": \"<content_slug from task>\",\n  \"lang\": \"<output_lang>\",\n  \"topicSlug\": \"<topic_slug from task - NOT the content_slug>\",\n  \"interests\": {\n    \"anchor\": [\"<topic_slug from task - MUST match topicSlug>\"],\n    \"related\": [\n      {\"slug\": \"<lowercase-hyphenated>\", \"label\": \"<human-readable in output_lang>\"},\n      {\"slug\": \"<lowercase-hyphenated>\", \"label\": \"<human-readable in output_lang>\"}\n    ]\n  },\n  \"dot\": {\n    \"hook\": \"<≤10 CJK chars or ≤6 English words, in output_lang>\",\n    \"category\": \"<choose: focus | attention | seed>\",\n  },\n  \"sources\": [\n    {\n      \"url\": \"https://...\",\n      \"title\": \"Article Title\",\n      \"name\": \"Source Name\",\n      \"publishTime\": \"<ISO 8601 date from source, or empty string if unknown>\"\n    }\n  ],\n  \"l1\": {\n    \"title\": \"<opinionated title in output_lang>\",\n    \"summary\": \"<2-3 sentences in output_lang, 50-80 words>\",\n    \"key_quote\": \"<most insightful direct quote from source, or empty string>\",\n    \"bullets\": [\"<≤20 zh chars or ≤50 en chars>\", \"...\", \"...\"]\n  },\n  \"l2\": {\n    \"content\": \"<2-4 paragraphs, 200-400 zh chars or 150-300 en words, separated by \\\\n\\\\n>\",\n    \"bullets\": [\n      {\"text\": \"<concrete fact with numbers/names>\", \"confidence\": \"high|medium|low\"},\n      {\"text\": \"...\", \"confidence\": \"...\"}\n    ],\n    \"context\": \"<SO WHAT for the reader, address them directly>\",\n    \"eir_take\": \"<Eir's sharp opinion, 1 sentence>\",\n    \"related_topics\": [\"<in output_lang>\", \"<in output_lang>\", \"<in output_lang>\"]\n  }\n}\n```\n\n## Rules\n\n### Language\n1. **ALL text fields must be in `output_lang`.** This includes `dot.hook`, `l1.title`, `l1.summary`, `l1.bullets`, `l2.content`, `l2.bullets`, `l2.context`, `l2.eir_take`, `l2.related_topics`. No exceptions.\n2. **NEVER mix languages** in a single field. Technical terms and proper nouns (e.g. \"GPT-4\", \"Transformer\", \"LLM\") may remain in their original form.\n3. **`related_topics`** must be human-readable phrases in `output_lang`. NOT slugs or code-style identifiers.\n   - ✅ `[\"digital sovereignty\", \"AI ethics\", \"open-source safety\"]`\n   - ❌ `[\"dark-forest-theory\", \"ai-platform-power\"]`\n\n### Category\n4. **`dot.category`** - choose by importance:\n   - **`focus`** - Major news, breakthroughs, high-impact events. Use sparingly (~10-15%).\n   - **`attention`** - Default. Valuable updates, worth knowing (~70-80%).\n   - **`seed`** - Background knowledge, explainers, foundational concepts (~10-15%).\n\n### Content Quality\n5. **Do NOT set `l1.via`** - the pipeline auto-generates it from `sources[].name`.\n6. **`sources`**: include `url`, `title`, `name` (publisher), and `publishTime` (camelCase) for each source used. Use `\"\"` if publishTime is unknown (never null). The API requires at least one source with a `publishTime` within the last 3 days.\n7. **NEVER fabricate or adjust `publishTime`**. Use the exact date from the source metadata. If ALL sources are outside the API's 3-day freshness window, do NOT generate content - report the issue and stop. Do NOT fake dates to bypass validation.\n8. **`key_quote`**: must be a **string** (not an object). Pick the most insightful direct quote from the sources, or `\"\"` if none.\n9. **`eir_take`** is **PUBLIC** (visible on share pages). It should feel like a sharp comment from a friend who deeply understands the reader's work and perspective. Not generic punditry.\n10. **`eir_take`** must be specific, opinionated, and demonstrate genuine understanding of the material. Bad: \"This is an issue that deserves society's attention.\" Bad: \"AI isn't stealing jobs, it's redefining...\" (cliché). Good: a concrete take that shows you saw something others missed.\n\n### Content Style\n11. Tone: \"a smart friend you trust\" - not a news anchor, not an encyclopedia.\n12. Forbidden phrases: \"reportedly\", \"sources say\", \"industry insiders say\", \"It's worth noting\", \"Interestingly\". Apply equivalent rules for non-English output.\n13. Source attribution goes in `sources[]`, NEVER inline in prose as `[Source: XX]`.\n14. `l2.content`: Start where the summary left off. Each paragraph should advance: what happened → why it matters → mechanism/detail → what comes next.\n15. `l2.context`: This is the **personal relevance** section. If `reader_context` is provided, it must feel like advice from someone who knows the reader's actual work — connect this news to something concrete in the reader's daily work, a decision they're facing, or a belief they hold. If `reader_context` is empty, write for a general tech-savvy audience — focus on industry-wide implications, practical takeaways, and what this means for practitioners.\n16. Be opinionated and curated - this is NOT a news summary, it's a knowledge signal.\n\n### Depth Scaling\n17. **When you have ≥2 rich sources (crawled content ≥ 500 chars each)**: you SHOULD generate l2.bullets, l2.context, and key_quote. There is enough material - use it.\n18. **When sources are thin (only snippets, <500 chars)**: l2.bullets, l2.context, key_quote may be omitted or empty. Don't fabricate depth.\n\n### Interest Signals\n19. `interests.anchor` must contain the `topicSlug` value, which comes from the task's `topic_slug` field. **It is NOT the content_slug.** Example: if `topic_slug` is `\"ai-health\"` and `content_slug` is `\"ai-drug-discovery-novo-amazon-race\"`, then `topicSlug` and `anchor` must be `\"ai-health\"`. The API rejects anchors that don't match registered user interest topics.\n20. `interests.related` should have 2-5 adjacent topics. Slugs: lowercase-hyphenated. Labels: in `output_lang`.\n21. Related topics should be specific enough to be useful (\"neural-architecture-search\") but not too narrow (\"bert-base-uncased-layer-12\").\n\n### Output\n22. Only output the JSON. No other text, no markdown fences.\n\n## Field Constraints\n\nFor full field types, limits, and null handling, see **`references/content-spec.md`** (single source of truth).\n\nFile v3.118.0:references/writer-prompt-standalone.md\n\n# Content Writer Prompt — Standalone Mode\n\nYou are a content curator generating structured content for a personal daily digest.\n\n## Input\n\nRead the task file. It contains:\n- `content_slug` — unique identifier for this piece\n- `topic` — the interest topic this relates to\n- `suggested_angle` — the specific angle to cover\n- `sources` — array of source materials with title, url, content/snippet\n- `language` — output language (e.g. \"zh\" or \"en\")\n- `reader_context` — (optional) user profile for personalization. May be empty.\n\n### Personalization\n- If `reader_context` is provided: use it to make the `connect` field specific to the reader's context.\n- If `reader_context` is empty (default): write `connect` for a general tech-savvy audience.\n\n## Output\n\nWrite a JSON file with this structure:\n\n```json\n{\n  \"title\": \"Concise, informative title (≤80 chars for en, ≤30 chars for zh)\",\n  \"category\": \"focus|attention|seed\",\n  \"summary\": \"2-3 sentences capturing the key point. What happened + why it matters.\",\n  \"body\": \"2-4 paragraphs. Start where summary left off. What → why → mechanism → what's next.\",\n  \"key_quote\": \"Most insightful direct quote from sources, or empty string\",\n  \"connect\": \"1-2 sentences: SO WHAT for the reader. Address them directly.\",\n  \"sources\": [\n    {\"name\": \"Publisher\", \"url\": \"https://...\", \"published\": \"2026-04-20\"}\n  ],\n  \"topic\": \"Topic Label\",\n  \"content_slug\": \"the-content-slug\",\n  \"generated_at\": \"2026-04-20T08:30:00Z\"\n}\n```\n\n## Category Assignment\n\n- **focus** — directly relevant to user's stated interests, timely, actionable\n- **attention** — interesting adjacent topic, worth knowing about\n- **seed** — emerging signal, early-stage but potentially important\n\n## Rules\n\n1. **ALL text fields must be in the specified `language`.**\n2. **Title**: Rewrite for clarity. No clickbait. Convey the core news.\n3. **Summary**: 50-80 words (en) or 80-120 chars (zh). Start with the main point.\n4. **Body**: Each paragraph should advance the narrative. No filler.\n5. **connect**: Be specific and reader-facing. Wrong: \"This is worth watching.\" Right: \"If you're building agents, this changes how you think about eval.\"\n6. **key_quote**: Direct quote from source material. If none is compelling, use `\"\"`.\n7. **Never invent facts**: Only use information from the provided sources.\n8. **No generic filler**: Forbidden phrases — \"reportedly\", \"industry insiders say\", \"it's worth noting\", \"interestingly\".\n9. **Source attribution**: In `sources[]` array, never inline as `[Source: XX]`.\n10. **Tone**: A smart friend you trust — not a news anchor, not an encyclopedia.\n\n## Depth Scaling\n\n- **≥2 rich sources (≥500 chars each)**: Generate full body + connect + key_quote.\n- **Only snippets (<500 chars)**: Keep body to 1-2 paragraphs. connect and key_quote may be `\"\"`.\n\n## Example Output\n\n```json\n{\n  \"title\": \"Meta cuts 8,000 jobs to fund $135B AI infrastructure bet\",\n  \"category\": \"focus\",\n  \"summary\": \"Meta will lay off 8,000 employees on May 20, roughly 10% of its workforce. The move redirects savings toward $115-135B in AI infrastructure spending, even as the company posted $201B in 2025 revenue.\",\n  \"body\": \"The cuts target mid-level knowledge workers across Reality Labs, recruiting, and back-office functions. Zuckerberg's internal memo frames this as \\\"building the team of the future\\\" — smaller, AI-augmented teams replacing traditional org structures.\\n\\nThe timing is notable: Meta is profitable and growing. This isn't cost-cutting from distress but a deliberate bet that AI tools will make current team sizes unnecessary. The $135B infrastructure budget exceeds most countries' technology spending.\\n\\nFor the broader industry, this signals that even profitable tech companies now view large non-technical teams as temporary. The restructuring playbook — cut headcount, reinvest in AI compute — is becoming standard.\",\n  \"key_quote\": \"One person with AI can now replace what used to require an entire team.\",\n  \"connect\": \"If you manage a team of 10+, the economics of AI augmentation vs. headcount are shifting faster than most org charts can adapt.\",\n  \"sources\": [\n    {\"name\": \"The Next Web\", \"url\": \"https://thenextweb.com/news/meta-layoffs-may-2026\", \"published\": \"2026-04-19\"},\n    {\"name\": \"Reuters\", \"url\": \"https://reuters.com/meta-layoffs-8000\", \"published\": \"2026-04-17\"}\n  ],\n  \"topic\": \"AI Industry\",\n  \"content_slug\": \"meta-layoffs-ai-infrastructure-bet\",\n  \"generated_at\": \"2026-04-20T08:30:00Z\"\n}\n```\n\nFile v3.118.0:scripts/package.json\n\n{\n  \"private\": true,\n  \"type\": \"module\"\n}\n\nFile v3.118.0:SECURITY.md\n\n# Security & Privacy\n\n## Modes\n\n|  | Standalone | Eir |\n|---|---|---|\n| Search API calls | ✅ Brave, Tavily, etc. | ✅ Same |\n| Crawl (fetch URLs) | ✅ | ✅ |\n| Eir API calls | ❌ None | ✅ Opt-in |\n| Reads USER.md | ❌ Never (default) | Only if personalization enabled |\n| Personal data leaves machine | ❌ Never | Only generated content (see below) |\n\n## Standalone Mode — What is sent\n\n- **Search queries** to your configured search API (e.g. Brave, Tavily)\n- **HTTP requests** to crawl source URLs for content\n- **Nothing else.** No other external communication.\n\n## Eir Mode — What is sent\n\n| Data | Sent to Eir API | Notes |\n|------|:---:|-------|\n| Generated content (dot, l1, l2) | ✅ | LLM-generated summaries |\n| l2.context, eir_take | ✅ | See personalization note below |\n| Interest categories | ✅ | Topic slugs only (e.g. \"ai-agents\") |\n| Source URLs + metadata | ✅ | For attribution |\n| **USER.md content** | **❌ Never** | Used as local LLM prompt context only |\n| **Raw conversation text** | **❌ Never** | Not accessed by pipeline |\n| **System credentials** | **❌ Never** | |\n| **File paths / machine identifiers** | **❌ Never** | |\n\n### Personalization\n\nPersonalization is **off by default**. When enabled (`\"personalization\": {\"enabled\": true}` in `config/settings.json`), the pipeline reads your USER.md to provide context to the LLM during content generation. USER.md itself is never transmitted, but the LLM-generated `l2.context` and `eir_take` fields may reflect your professional context (e.g. \"as an AI product builder...\").\n\nTo disable: set `\"personalization\": {\"enabled\": false}` in `config/settings.json`, or simply don't enable it — it's off by default. When disabled, content is written for a general audience with no personal references.\n\n### Interest Extraction\n\nThis skill includes a reference prompt (`references/interest-extraction-prompt.md`) that helps your agent learn your interests from conversations. It extracts only **de-identified topic labels and keywords** (e.g. \"AI Agents\", \"autonomous vehicles\") and saves them locally to `config/interests.json`. No raw conversation text or personal identifiers are stored.\n\nIf you already have a profile or interest skill installed, you can use that instead.\n\n## Credential Storage\n\nAll credentials are stored locally in config files (gitignored). No secrets are hardcoded.\n\n| File | Contains | Committed |\n|------|----------|-----------|\n| `config/eir.json` | Eir API key, user ID | ❌ gitignored |\n| `config/settings.json` | Search API key, mode, preferences | ❌ gitignored |\n| `config/interests.json` | Topic labels and keywords | ✅ no secrets |\n\n## File Access\n\nPipeline scripts read/write only within the skill's `data/` and `config/` directories. The only external file access is optionally reading `USER.md` (when personalization is enabled).\n\n## Reporting Vulnerabilities\n\nOpen an issue at [github.com/heyeir/openeir](https://github.com/heyeir/openeir/issues) or email security concerns directly.\n\nFile v3.118.0:config/settings.json\n\n{\n  \"mode\": \"standalone\",\n  \"skill_version\": \"1.0.0\",\n  \"supported_schema_versions\": [\n    \"2\"\n  ],\n  \"max_items_per_day\": 10,\n  \"local_storage\": \"data/\",\n  \"search\": {\n    \"providers\": [\n      \"brave\"\n    ],\n    \"searxng_url\": \"http://localhost:8888\",\n    \"crawl4ai_url\": \"http://localhost:11235\"\n  },\n  \"cron\": {\n    \"schedule\": \"0 8 * * *\",\n    \"timezone\": \"UTC\"\n  }\n}\n\nArchive v3.117.0: 29 files, 78129 bytes\n\nFiles: references/candidates-spec.md (2972b), references/content-spec.md (7948b), references/eir-api.md (4709b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3832b), references/interest-extraction-prompt.md (3778b), references/writer-prompt-eir.md (7132b), references/writer-prompt-standalone.md (4512b), scripts/connect.mjs (2346b), scripts/package.json (42b), scripts/pipeline/__init__.py (352b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2811b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (3997b), scripts/pipeline/eir_post.py (7402b), scripts/pipeline/eir_sync.py (6065b), scripts/pipeline/generate.py (5380b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (12731b), scripts/pipeline/search.py (28413b), scripts/pipeline/task_builder.py (17033b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (4143b), scripts/setup.py (6250b), SECURITY.md (3021b), SKILL.md (12347b), _meta.json (146b)\n\nFile v3.117.0:SKILL.md\n\n---\nname: eir-daily-content-curator\ndescription: \"Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news', 'curate content for me', 'what should I read today', 'personalized news briefing', 'daily digest', 'news summary', 'content pipeline', 'interest tracking', 'automated content curation'.\"\nmetadata:\n  openclaw:\n    emoji: \"📰\"\n    requires:\n      bins: [\"python3\"]\n---\n\n# Daily Content Curator\n\nCurates personalized content based on your interests. Supports two modes:\n\n- **Standalone** — works locally, no external account needed\n- **Eir** — full curation + delivery via [heyeir.com](https://www.heyeir.com)\n\n## Getting Started\n\n**Before setup, ask the user which mode to use:**\n\n| Mode | What it does | Requirements |\n|------|--------------|---------------|\n| **Standalone** | Search → curate → generate summaries locally | Search API key (Brave, Tavily, etc.) |\n| **Eir** | Full pipeline with delivery to Eir app | [Eir account](https://www.heyeir.com) + pairing code |\n\n> **Important:** The two modes use different content formats and topic slug conventions. Choose the correct mode at setup time — switching later requires reconfiguration. If the user has an Eir account, use Eir mode.\n\nThen follow the corresponding setup section below.\n\n## Standalone Mode\n\n### Flow\n\n```\n1. Configure          → Set up search API + interests (one-time)\n2. Search             → Search API queries for each interest topic\n3. Select + Crawl     → Agent picks best candidates, fetches full content\n4. Generate           → Agent writes structured summaries from task files\n5. Daily Brief        → Agent compiles brief from generated items\n```\n\n> Steps 1-3 are Python scripts you run directly. Steps 4-5 are **agent-driven** — you tell your OpenClaw agent to read the task files and generate content. The agent uses whatever LLM model is configured in your OpenClaw session (e.g. Claude, GPT-4, Gemini).\n\n### Quick Start\n\n**1. Initialize workspace** — creates `config/` directory and default settings:\n```bash\n# Option A: inline JSON\npython3 scripts/setup.py --init --settings '{\n  \"mode\": \"standalone\",\n  \"language\": \"en\",\n  \"personalization\": {\"enabled\": false},\n  \"search\": {\n    \"search_base_url\": \"https://api.search.brave.com/res/v1\",\n    \"search_api_key\": \"YOUR_BRAVE_API_KEY\"\n  }\n}'\n\n# Option B: settings file (recommended for PowerShell/Windows)\npython3 scripts/setup.py --init --settings-file path/to/settings.json\n```\n```\n\nSearch provider examples:\n| Provider | `search_base_url` | Get API key |\n|----------|-------------------|-------------|\n| Brave Search | `https://api.search.brave.com/res/v1` | [brave.com/search/api](https://brave.com/search/api/) |\n| Tavily | `https://api.tavily.com` | [tavily.com](https://tavily.com/) |\n\n> **Want richer results?** Install [SearXNG](https://docs.searxng.org/) and/or [Crawl4AI](https://github.com/unclecode/crawl4ai) locally. Add `searxng_url` and `crawl4ai_url` to your search config — they work as fallback or primary search/crawl providers.\n\n**2. Set up interests** — edit the generated `config/interests.json`:\n```json\n{\n  \"topics\": [\n    {\"label\": \"AI Agents\", \"keywords\": [\"autonomous agents\", \"tool use\"], \"freshness\": \"7d\"},\n    {\"label\": \"Prompt Engineering\", \"keywords\": [\"prompting\", \"chain-of-thought\"]}\n  ],\n  \"language\": \"en\",\n  \"max_items_per_day\": 8\n}\n```\n\nInterests can also be auto-extracted — see `references/interest-extraction-prompt.md`.\n\n**Freshness and tier:** Each topic supports optional `freshness` (e.g. `\"3d\"`, `\"7d\"`, `\"14d\"`) and `tier` (`\"focus\"`, `\"tracked\"`, `\"explore\"`, `\"seed\"`) fields in `interests.json`. In Eir mode, these come from the API directives. In standalone mode, defaults are `\"7d\"` and `\"tracked\"`. The search pipeline uses tier to decide search depth (focus/tracked get entity refinement) and freshness to filter stale results.\n\n**3. Run the search + crawl pipeline** (from the `scripts/` directory):\n```bash\ncd scripts\npython3 -m pipeline.search              # Search for each topic\npython3 -m pipeline.candidate_selector  # Group results for agent selection\n# ↓ Agent step: review topic files, write candidates.json (see below)\npython3 -m pipeline.crawl               # Fetch full content\npython3 -m pipeline.task_builder        # Bundle into task files\n```\n\n> All `python3 -m pipeline.*` commands must be run from the `scripts/` directory.\n\n**Agent selection step** (between `candidate_selector` and `crawl`):\n\n`candidate_selector` outputs per-topic JSON files to `data/v9/topics/`. Your agent should:\n1. Read each topic file\n2. Pick 0-3 candidates per topic based on relevance and freshness\n3. Write `data/v9/candidates.json` with the selected candidates\n\nSee `references/candidates-spec.md` for the exact JSON format.\n\n> **Note on crawl fallback:** If a candidate URL isn't in the search cache, `crawl.py` automatically tries: Browse API → Crawl4AI → web_fetch → HTML head extraction. No manual intervention needed.\n\n**4. Generate content** (agent-driven):\n\nAfter `task_builder`, task files are in `data/v9/tasks/`. Tell your OpenClaw agent:\n\n```\nRead the task files in data/v9/tasks/ and generate content for each one.\nUse the writer prompt in references/writer-prompt-standalone.md.\nSave output to data/output/{YYYY-MM-DD}/.\n```\n\n**Scheduling tip:** If you want automated daily runs, you can set up a cron job:\n```bash\nopenclaw cron add --name \"daily-curate\" \\\n  --cron \"0 8 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated \\\n  --message \"Read SKILL.md for eir-daily-content-curator, then run the full standalone pipeline: search → select → crawl → task_builder → generate content from task files → compile daily brief.\"\n```\n\n### Output\n\nContent saved to `data/output/{YYYY-MM-DD}/`. Daily brief compiles the top items:\n\n```markdown\n# Daily Brief — 2026-04-20\n\n🔥 **Meta cuts 8,000 jobs for AI pivot** — ...\n📡 **China bans AI companions for minors** — ...\n🌱 **New prompt engineering benchmark** — ...\n```\n\n### Dependencies\n\n**Required:** Python 3.10+ (standard library only — no `pip install` needed).\n\n**Optional:** Node.js 18+ (only for Eir connect script). [SearXNG](https://docs.searxng.org/) (fallback search). [Crawl4AI](https://github.com/unclecode/crawl4ai) (fallback crawl).\n\n---\n\n## Eir Mode\n\nFull curation with delivery to the [Eir](https://www.heyeir.com) app via a 3-job pipeline:\n\n```\nJob A: material-prep     → Search → Select → Crawl → Pack tasks\nJob B: content-gen       → Spawn subagents → Generate → POST to Eir\nJob C: daily-brief       → Check status → Fill gaps → Compile brief → POST + Deliver\n```\n\n### Setup\n\n1. Get a pairing code from [heyeir.com](https://www.heyeir.com) → Settings → Connect OpenClaw\n2. Run: `node scripts/connect.mjs <PAIRING_CODE>`\n3. Set `\"mode\": \"eir\"` in `config/settings.json`\n\n### Running the Pipeline (Eir Mode)\n\nAll `python3 -m pipeline.*` commands must be run from the `scripts/` directory.\n\n**Step 1: Sync directives** — fetch topic slugs and curation rules from Eir API:\n```bash\ncd scripts && python3 -m pipeline.eir_sync fetch\n```\n> This creates/updates `config/directives.json` with the canonical topic slugs (e.g. `ai-agents`, `autonomous-vehicles`). All downstream scripts use these slugs — do NOT use interests.json labels as topic_slug in Eir mode.\n\n**Step 2: Search + Select + Crawl + Pack:**\n```bash\npython3 -m pipeline.search              # Search for each directive topic\npython3 -m pipeline.candidate_selector  # Group results for agent selection\n# ↓ Agent step: review topic files, write candidates.json (see references/candidates-spec.md)\npython3 -m pipeline.crawl               # Fetch full content from candidate URLs\npython3 -m pipeline.task_builder        # Bundle into task files (auto-selects eir writer prompt)\n```\n\n**Step 3: Generate content** (agent-driven):\n\nTask files are in `data/v9/tasks/`. The agent should:\n1. Read each task file\n2. Follow the writer prompt in `references/writer-prompt-eir.md`\n3. Generate Eir-format JSON (`slug`, `lang`, `dot`, `l1`, `l2`, `sources`, `interests`)\n4. POST via `from pipeline.eir_post import post_content; post_content(data, api_key)`\n\n> **Key difference from standalone:** Eir format uses `dot.hook`, `l1.bullets`, `l2.context`, `l2.eir_take` etc. Do NOT use the standalone format (`title/summary/body/connect`).\n\n**Step 4: Daily brief** (optional):\n```bash\n# Agent compiles generated content into a brief and POSTs to /api/oc/brief\n```\n\n**Common POST failures:**\n- `400 Bad Request` → check that `topicSlug` matches a directive slug (not a Chinese label), `publishTime` is present at the item level, and no fields are `null`\n- `401 Unauthorized` → check `config/eir.json` has valid credentials\n- `500 Internal Server Error` → retry once; if persistent, report the payload\n\nFor cron configuration and API details, see `references/eir-setup.md`.\n\n### Privacy Notice\n\nWhen Eir mode is enabled, generated content summaries are POSTed to heyeir.com. See SECURITY.md for details on what IS and IS NOT sent.\n\n---\n\n## Pipeline Modules\n\nAll in `scripts/pipeline/`:\n\n| Module | Purpose | Mode |\n|--------|---------|------|\n| `search.py` | Search via configurable API, SearXNG fallback | Both |\n| `crawl.py` | Fetch content via Browse API, Crawl4AI fallback | Both |\n| `grounding.py` | Configurable search API client | Both |\n| `candidate_selector.py` | Group results, prepare for agent selection | Both |\n| `task_builder.py` | Bundle candidates into task files | Both |\n| `generate.py` | Build prompts for content generation | Both |\n| `validate_content.py` | Validate generated content against spec | Both |\n| `directives.py` | Load local interests/directives | Both |\n| `config.py` | Shared configuration and path resolution | Both |\n| `workspace.py` | Workspace and credential resolution | Both |\n| `eir_sync.py` | Fetch directives from Eir API | Eir only |\n| `eir_post.py` | POST content to Eir API | Eir only |\n| `run_state.py` | Pipeline run state management | Both |\n\n### Search Fallback Chain\n\n```\nSearch API (primary) → SearXNG (optional) → Crawl4AI/web_fetch (content)\n```\n\n---\n\n## References\n\n| File | Contents | Used by |\n|------|----------|---------|\n| `references/writer-prompt-eir.md` | Content generation rules (Eir mode) | Agent |\n| `references/writer-prompt-standalone.md` | Content generation rules (standalone) | Agent |\n| `references/content-spec.md` | Field types, limits, validation rules | Agent |\n| `references/eir-setup.md` | Eir mode setup, cron, API endpoints | Agent / User |\n| `references/eir-api.md` | Full Eir API reference | Agent |\n| `references/eir-interest-rules.md` | Curation tier guidelines | Agent |\n| `references/candidates-spec.md` | Candidates JSON format for agent selection | Agent / User |\n| `references/interest-extraction-prompt.md` | Interest extraction prompt | Agent |\n\n> The `writer-prompt-*.md` files are **instructions for the agent** — the agent reads them to know how to generate content from task files. You don't need to read them unless customizing output format.\n\n---\n\n## Security & Data Flow\n\n**Standalone mode:** Only sends search queries to your configured search API and HTTP requests to crawl source URLs. No other external communication.\n\n**Eir mode (opt-in):** Additionally sends generated content summaries to heyeir.com. USER.md is never transmitted — it's used locally as LLM context only when personalization is enabled.\n\n**Personalization** is off by default. Enable it in `config/settings.json` to get content tailored to your profile. See `SECURITY.md` for the full data flow table.\n\n---\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Initialize workspace | `python3 scripts/setup.py --init --settings '{...}'` |\n| Check setup | `python3 scripts/setup.py --check` |\n| Search | `cd scripts && python3 -m pipeline.search` |\n| Select candidates | `cd scripts && python3 -m pipeline.candidate_selector` |\n| Crawl | `cd scripts && python3 -m pipeline.crawl` |\n| Build tasks | `cd scripts && python3 -m pipeline.task_builder` |\n| Validate | `cd scripts && python3 -m pipeline.validate_content` |\n| Fetch directives (Eir) | `cd scripts && python3 -m pipeline.eir_sync fetch` |\n| Connect Eir | `node scripts/connect.mjs <PAIRING_CODE>` |\n\nFile v3.117.0:_meta.json\n\n{\n  \"ownerId\": \"kn76fghgn0qqq4e4qdknvea95s8222rw\",\n  \"slug\": \"eir-daily-content-curator\",\n  \"version\": \"3.117.0\",\n  \"publishedAt\": 1777041083014\n}\n\nFile v3.117.0:references/candidates-spec.md\n\n# Candidates JSON Format Specification\n\nThis document defines the expected format of `candidates.json`, which is the handoff point between candidate selection (agent-driven) and crawling/task building (automated scripts).\n\n## Location\n\n`data/v9/candidates.json`\n\n## Structure\n\n```json\n{\n  \"candidates\": [\n    {\n      \"content_slug\": \"string (required) — kebab-case identifier, 3-6 words, e.g. 'openai-gpt-5-launch'\",\n      \"matched_topic_slug\": \"string (required) — must match a directive slug, e.g. 'ai-industry-news'\",\n      \"suggested_angle\": \"string (required) — editorial angle in output language\",\n      \"reason\": \"string (optional) — why this candidate was selected\",\n      \"priority\": \"string (optional) — 'high' | 'medium' | 'low', default 'medium'\",\n      \"source_urls\": [\n        \"string (required, 1-5 URLs) — URLs to crawl for full content\"\n      ],\n      \"source_titles\": {\n        \"https://example.com/article\": \"Article Title\"\n      }\n    }\n  ],\n  \"selected_at\": \"ISO 8601 timestamp\"\n}\n```\n\n## Field Details\n\n### Required Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `content_slug` | string | Unique identifier for this content piece. Used as filename and API slug. Must be kebab-case, 3-6 words. |\n| `matched_topic_slug` | string | The directive topic this belongs to. Must match a slug from directives/interests. Used as `topicSlug` in generated content. |\n| `suggested_angle` | string | The editorial angle — what makes this worth covering. In the output language. |\n| `source_urls` | string[] | 1-5 URLs to crawl. Prefer diverse domains. At least one must be crawlable. |\n\n### Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `reason` | string | Why this candidate was selected (for audit trail). |\n| `priority` | string | `high` / `medium` / `low`. Affects generation order. Default: `medium`. |\n| `source_titles` | object | Map of URL → article title (helps crawl quality scoring). |\n\n## Downstream Usage\n\n1. **`crawl.py`** reads `candidates.json`, crawls each `source_urls` entry, saves content to `data/v9/snippets/{url_hash}.json`.\n2. **`task_builder.py`** reads crawled candidates, bundles source text + writer prompt into task files at `data/v9/tasks/{content_slug}.json`.\n\n## How Candidates Are Created\n\n### Eir Mode (cron-driven)\nThe agent reads per-topic files from `data/v9/topics/`, evaluates search results, and writes `candidates.json` directly.\n\n### Standalone Mode (manual)\n1. Run `python3 -m pipeline.candidate_selector` → generates topic files in `data/v9/topics/`\n2. Review topic files and create `candidates.json` following the format above\n3. Run `python3 -m pipeline.crawl` → crawls candidate URLs\n\n## Validation\n\n- `content_slug` must be unique across all candidates\n- `matched_topic_slug` should match a known directive/interest slug\n- `source_urls` must contain at least 1 valid HTTP(S) URL\n- No `null` values — use `\"\"` or `[]` for empty fields\n\nFile v3.117.0:references/content-spec.md\n\n# Eir Content Specification\n\n> Single source of truth for all content field constraints and quality criteria.\n> Used by: writer prompts, API validation, front-end rendering.\n\n## Contents\n\n- [Field Reference](#field-reference) — dot, l1, l2, sources fields\n- [via vs sources](#via-vs-sources) — Attribution handling\n- [lang field](#lang-field) — Language requirements\n- [Null handling](#null-handling)\n- [Validation summary](#validation-summary)\n- [Content ID format](#content-id-format)\n- [Interest Signals](#interest-signals)\n\n---\n\n## Field Reference\n\n### dot (L0 — the dot on canvas)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `hook` | string | ≤10 CJK chars / ≤6 EN words | **100 chars** (API rejects) | Creates curiosity gap. No hype words (\"Breaking\", \"Exciting\"). Rendered as single-line label on the dot. |\n| `category` | enum | — | `focus` \\| `attention` \\| `seed` | Determines dot visual style. |\n\n### l1 (card — what the user sees first)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `title` | string | 15-40 CJK chars / 8-15 EN words | **200 chars** (API rejects) | Opinionated, not a headline. Must be in `lang`. |\n| `summary` | string | 50-80 words | — | 2-3 sentences. Advances beyond the title — don't repeat. |\n| `key_quote` | string | 1 sentence | — | Best direct quote from sources. Use `\"\"` if none. |\n| `via` | **string[]** | — | — | **Must be an array.** Auto-derived from `sources[].name`. Pipeline populates it; API also falls back to `sources[].name` if empty. Writer should NOT set this. |\n| `bullets` | string[] | 3-4 items | 10 items (API rejects) | Each: ≤20 CJK chars / ≤50 EN chars. Don't repeat summary. |\n\n### l2 (depth — expanded view)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `content` | string | 200-600 CJK chars / 150-400 EN words | — | 2-4 paragraphs separated by `\\n\\n`. Starts where summary left off. |\n| `bullets` | array | 3-5 items | — | Each: `{text: string, confidence: \"high\"\\|\"medium\"\\|\"low\"}`. Concrete facts with numbers/names. Every bullet must have supporting detail in `content`. |\n| `context` | string | 1-2 sentences | — | \"SO WHAT for the reader.\" Be specific and direct — address the reader. |\n| `eir_take` | string | 1 sentence | — | Eir's sharp opinion. **PUBLIC** (visible on share pages) — no user-specific info. |\n| `related_topics` | string[] | 3-5 items | — | Human-readable phrases in `lang`. NOT slugs. e.g. `\"Vector Search and ANN Algorithms\"` ✅, `\"vector-search-ann\"` ❌ |\n\n### sources (provenance — machine-readable)\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| `url` | string | **Yes** | Must be valid URL. Used for server-side dedup — duplicate URLs are rejected. |\n| `title` | string | No | Original article title. |\n| `name` | string | No | Publisher/source name (e.g. \"MIT Technology Review\"). This is what `l1.via` selects from. |\n| `publish_time` | string | No | ISO date or date string from source. |\n\n### Top-level item fields\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| `lang` | `\"zh\"` \\| `\"en\"` | **Yes** | **Required.** Language of this document's content. Determines which `{contentGroup}_{lang}` document is created. Not locale, not source language — the language the content is written in. API rejects if missing. API rejects `lang=\"en\"` if hook contains CJK characters (Chinese hooks with English words are fine). |\n| `slug` | string | No | Human-readable identifier. Falls back to `contentGroup` if omitted. |\n| `interests` | object | **Recommended** | See Interest Signals section below. |\n| `dot` | object | **Yes** | See dot section above. |\n| `l1` | object | **Yes** | See l1 section above. `l1.title` is required. |\n| `l2` | object | No | See l2 section above. Strongly recommended. |\n| `sources` | array | No | See sources section above. At least 1 recommended. |\n| `publishTime` | string | **Recommended** | ISO 8601 date of the primary source. Used for freshness display and sorting. If omitted, `eir_post.py` auto-extracts from `sources[0].publish_time`. Prefer providing explicitly. |\n| `visibility` | `\"private\"` \\| `\"public\"` | **Yes** | `private` for user content, `public` for pool/shared content. Set by API, not writer. |\n| `channelId` | string | **Yes** | Content channel: `user-private`, `eir-express`, `shared-pick`, etc. Set by API, not writer. |\n\n---\n\n## via vs sources\n\n`via` = `sources[].name` — the full set, not a subset.\n\n| | `sources[]` | `l1.via` |\n|---|---|---|\n| **Purpose** | Machine: dedup, provenance, linking | Human: display attribution on card |\n| **Contains** | Full metadata (url, title, name) | Just the names |\n| **Type** | `Array<{url, title, name}>` | `string[]` |\n| **Set by** | Writer (required) | Pipeline (auto-derived); API also falls back to `sources[].name` if empty |\n| **Example** | `[{url: \"...\", name: \"MIT Tech Review\"}, {url: \"...\", name: \"ArXiv\"}]` | `[\"MIT Tech Review\", \"ArXiv\"]` |\n\n**Writers only need to set `sources[]`.** The pipeline auto-populates `via` from `sources[].name`; the API also falls back to `sources[].name` if `via` is empty. If the writer includes `via` it will be overwritten.\n\n---\n\n## lang field\n\n`lang` means: **\"what language is this content written in?\"**\n\n- Set by pipeline's `output_lang` parameter\n- Each language version is a **separate document** with ID `{contentGroup}_{lang}`\n- For bilingual users: pipeline generates two items with same `slug` but different `lang`\n- `lang` is NOT locale (UI language) and NOT source_lang (language of source articles)\n\n| Field | Meaning | Set by |\n|-------|---------|--------|\n| `lang` | Content language | Pipeline `output_lang` |\n| `locale` (user pref) | UI language (dates, buttons) | User settings |\n\n---\n\n## Null handling\n\n**Never set any field to `null`.** The front-end renders null as literal \"placeholder\" text.\n\n| Instead of | Use |\n|-----------|-----|\n| `null` | `\"\"` (empty string) |\n| `null` | `[]` (empty array) |\n| `{field: null}` | Omit the field entirely |\n\n---\n\n## Validation summary\n\n### API rejects (400 error)\n\n- `dot` missing or not an object\n- `dot.hook` empty or >100 chars\n- `dot.category` not in allowed enum\n- `l1` missing or not an object\n- `l1.title` empty or >200 chars\n- `l1.via` present but not an array\n- `l1.bullets` present but not an array, or >10 items\n- `sources[].url` missing or not a valid URL\n- `sources` >10 items per content item\n- `lang` missing, or not `\"zh\"` or `\"en\"`\n- `lang` is `\"en\"` but hook contains CJK characters (language mismatch)\n- `items` empty, not an array, or >20 items\n\n### API skips (returned as `status: \"skipped\"`)\n\n- Any `sources[].url` already exists for this user → `duplicate source_url`\n\n### Pipeline should reject (pre-POST)\n\n- `l1.title` missing → don't POST, file is broken\n- `l2.content` <300 chars → quality too low\n- `dot.hook` >50 chars → consider shortening (API allows up to 100 but shorter hooks render better)\n\n---\n\n## Content ID format\n\nBoth use the same ID scheme:\n\n```\n{8-char contentGroup}_{lang}    e.g. a3k9m2x7_zh\n```\n\n- `contentGroup`: 8-char base64url, globally unique\n- All language versions of the same item share the `contentGroup`\n\n---\n\n## Interest Signals\n\nEvery content item should include interest signals:\n\n```json\n\"interests\": {\n  \"anchor\": [\"ai-agents\"],\n  \"related\": [\n    { \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }\n  ]\n}\n```\n\n### Anchor\n\n- 1-3 slugs from the curation directives\n- Must match user's interests (API validates, rejects 400 if mismatch)\n- Use `topicSlug` (camelCase) in the content item\n\n### Related\n\n- 2-5 adjacent topics\n- Slugs: lowercase-hyphenated\n- Labels: human-readable in content's `lang`\n- Unknown topics auto-created as candidates\n\nSee `eir-interest-rules.md` for curation guidelines.\n\nFile v3.117.0:references/eir-api.md\n\n# Eir API Reference\n\n**Base URL**: `https://api.heyeir.com/api` (override with `EIR_API_URL` environment variable)\n\n**Authentication**: `Authorization: Bearer <EIR_API_KEY>` for all `/oc/*` endpoints.\n\n## Contents\n\n- [Connection](#connection) — Register/disconnect/rotate keys\n- [Interests](#interests) — Manage user interests\n- [Curation](#curation) — Fetch directives, report misses\n- [Content](#content) — Push/read/delete content items, daily briefs\n\n---\n\n## Connection\n\n### POST /oc/connect\nRegister with pairing code.\n\n**Request:** `{ \"code\": \"ABCD-1234\" }`\n\n**Response:** `{ \"apiKey\": \"eir_oc_xxx\", \"userId\": \"u_abc123\" }`\n\n### DELETE /oc/connect\nDisconnect and revoke API key.\n\n### POST /oc/refresh-key\nRotate API key (60s grace period).\n\n---\n\n## Interests\n\n### GET /oc/interests\nReturns user interests.\n\n**Response:**\n```json\n{\n  \"user\": { \"id\": \"u_xxx\", \"primaryLanguage\": \"zh\", \"bilingual\": false },\n  \"interests\": [\n    {\n      \"id\": \"ui_abc1234\",\n      \"slug\": \"artificial-intelligence\",\n      \"label\": \"Artificial Intelligence\",\n      \"status\": \"active\",\n      \"heat\": 5,\n      \"strength\": 0.6\n    }\n  ]\n}\n```\n\n### POST /oc/interests/add\nAdd interests by label. Server matches against dictionary.\n\n**Request:** `{ \"labels\": [\"AI Agents\", \"MCP\"], \"lang\": \"en\" }`\n\n**Response:** `{ \"added\": 2, \"results\": [...] }`\n\n---\n\n## Curation\n\n### GET /oc/curation\nReturns curation directives for content collection.\n\n**Response:**\n```json\n{\n  \"schema_version\": \"1.0\",\n  \"user\": {\n    \"primaryLanguage\": \"zh\",\n    \"bilingual\": false\n  },\n  \"directives\": [\n    {\n      \"slug\": \"mcp-protocol\",\n      \"label\": \"MCP Protocol\",\n      \"tier\": \"tracked\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"MCP 2.0 announced\", \"Anthropic MCP ecosystem\"],\n      \"userNeeds\": \"Protocol updates and adoption\",\n      \"trackingGoal\": \"Stay current on protocol updates\"\n    },\n    {\n      \"slug\": \"ai-agents\",\n      \"label\": \"AI Agents\",\n      \"tier\": \"focus\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"AI agent frameworks comparison\", \"autonomous agent production\"],\n      \"userNeeds\": null,\n      \"trackingGoal\": null\n    }\n  ],\n  \"exclude\": {\n    \"disliked\": [\"crypto\", \"nft\"]\n  }\n}\n```\n\n**Tiers:** tracked → focus → explore → seed (informational labels; selection is score-based).\n\n**Server-side curation:** The API handles topic selection, cooldown, and scoring internally. The agent just reads directives and finds content for them.\n\nSee `eir-interest-rules.md` for curation guidelines.\n\n---\n\n## Content\n\n### POST /oc/content\nPush generated content.\n\n**Request:**\n```json\n{\n  \"items\": [\n    {\n      \"slug\": \"mcp-protocol-2-0\",\n      \"lang\": \"en\",\n      \"interests\": {\n        \"anchor\": [\"mcp-protocol\"],\n        \"related\": [{ \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }]\n      },\n      \"dot\": {\n        \"hook\": \"MCP 2.0 Released\",\n        \"category\": \"focus\",\n      },\n      \"l1\": {\n        \"title\": \"MCP Protocol v2.0\",\n        \"summary\": \"Anthropic releases MCP 2.0...\",\n        \"key_quote\": \"...\"\n      },\n      \"l2\": {\n        \"content\": \"...(500+ words)...\",\n        \"bullets\": [{ \"text\": \"...\", \"confidence\": \"high\" }],\n        \"context\": \"...\",\n        \"eir_take\": \"...\",\n        \"related_topics\": [\"ai-agents\"]\n      },\n      \"sources\": [{ \"url\": \"https://...\", \"title\": \"...\", \"name\": \"Anthropic Blog\" }]\n    }\n  ]\n}\n```\n\n**Rules:**\n- `lang` required (\"en\" or \"zh\")\n- `interests.anchor` required (1-3 slugs from curation directives). Must match user's interests.\n- `interests.related` optional (max 5). Unknown topics auto-created as candidates.\n- For bilingual: push two items with same `slug`, different `lang`\n- See `content-spec.md` for field limits\n\n**Response:**\n```json\n{\n  \"accepted\": 1,\n  \"results\": [{ \"status\": \"accepted\", \"id\": \"a3k9m2x7_en\", \"contentGroup\": \"a3k9m2x7\" }]\n}\n```\n\n### GET /oc/content/:id\nRead back a content item.\n\n### DELETE /oc/content/:id\nDelete by id or contentGroup.\n\n### POST /oc/curation/miss\nReport topics where you searched but found no quality content. This lowers their priority in future curation rounds.\n\n**Request:** `{ \"slugs\": [\"topic-a\", \"topic-b\"] }`\n\n**Response:** `{ \"ok\": true, \"updated\": 2 }`\n\n**When to call:** After finishing a curation round, if you searched for a topic's searchHints but found nothing worth pushing.\n\n### POST /oc/brief\nPush a daily brief (compiled summary of the day's content).\n\n**Request:**\n```json\n{\n  \"title\": \"Daily Brief — 2026-04-22\",\n  \"summary\": \"3 focus items, 2 signals, 1 seed\",\n  \"content\": \"Markdown body of the brief\",\n  \"publishTime\": \"2026-04-22T07:45:00Z\"\n}\n```\n\n**Response:** `{ \"ok\": true }`\n\n**When to call:** After content generation is complete, typically from the daily-brief cron job.\n\nFile v3.117.0:references/eir-interest-rules.md\n\n# Eir Interest Rules\n\n> **Eir Mode Only** — This document describes curation behavior when connected to the Eir API.\n\nCuration guidelines for the content curator agent.\n\n## Your Job\n\n1. **Read directives**: `GET /oc/curation` → topics to find content for\n2. **Find content**: Search using `searchHints` from each directive\n3. **Push content**: `POST /oc/content`\n4. **Discover interests**: From conversations → `POST /oc/interests/add`\n\n## Curation Tiers\n\n| Tier | Description | Quality Expectation |\n|------|-------------|---------------------|\n| **tracked** | User explicitly follows | Highly relevant, timely |\n| **focus** | Strong interest signal | Relevant + quality |\n| **explore** | Moderate interest | Quality threshold applies |\n| **seed** | Discovery topics | Must be excellent to justify |\n\nThe API returns a curated subset of topics per tier. Server-side filtering already applied:\n- Topics in cooldown are excluded\n- Quotas adjusted based on user engagement history\n\n## Content Selection\n\nFor each candidate, evaluate:\n- **Relevance** to the directive's topic\n- **Source authority** — trusted, primary sources preferred\n- **Freshness** — match the directive's `freshness` (1d = within 24h, 7d = within week)\n- **Depth** — substantial, not thin listicles\n- **Novelty** — not duplicating recently pushed content\n\n**Quality bar by tier:**\n- tracked: relevant + timely\n- focus/explore: relevant + quality source\n- seed: must be exceptional to earn attention\n\n## Using Directives\n\nEach directive contains:\n- `slug` — topic identifier (use as `interests.anchor`)\n- `label` — display name\n- `tier` — priority level\n- `freshness` — recency requirement (\"1d\", \"2d\", \"3d\", \"7d\", \"14d\")\n- `searchHints` — 2-3 search queries to find content\n- `userNeeds` — guidance on what the user wants (may be null)\n- `trackingGoal` — specific goal for tracked topics (may be null)\n\n**Search strategy:**\n- Use `searchHints` directly as search queries\n- For freshness \"1d\"-\"3d\": prioritize news, announcements, releases\n- For freshness \"7d\"+: mix news with analysis, insights, perspectives\n- Respect `userNeeds` when selecting content angles\n\n## Interest Anchors on Content\n\nEvery content item MUST include:\n\n```json\n\"interests\": {\n  \"anchor\": [\"ai-agents\"],\n  \"related\": [\n    { \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }\n  ]\n}\n```\n\n**Anchor rules:**\n- 1-3 slugs from the curation directives\n- Must match user's interests (API validates, rejects 400 if mismatch)\n\n**Related topics:**\n- 2-5 adjacent topics\n- Unknown topics auto-created as candidates\n- Drive \"Explore More\" on detail pages\n\n## Exclusions\n\nThe API returns `exclude.disliked` — slugs to filter out during content selection.\n\n## Adding Interests\n\nFrom conversations:\n```\nPOST /oc/interests/add\n{ \"labels\": [\"AI Agents\", \"MCP\"], \"lang\": \"en\" }\n```\n\nServer matches to dictionary. Unknown labels flagged for review.\n\n## Best Practices\n\n1. **Quality over quantity** — if nothing good, push nothing\n2. **Max 2 items per topic group** unless exceptional\n3. **Seed topics**: adjacent to existing interests, not random\n4. **Use `GET /oc/sources`** for URL dedup\n5. **Never override** user's explicit tracking decisions\n\nFile v3.117.0:references/eir-setup.md\n\n# Eir Mode Setup Guide\n\n## Prerequisites\n\n1. An Eir account at [heyeir.com](https://heyeir.com)\n2. Node.js 18+ (for the connect script)\n\n## Connect\n\n```bash\nnode scripts/connect.mjs <PAIRING_CODE>\n```\n\nGet a pairing code from Eir → Settings → Connect OpenClaw. This saves credentials to `config/eir.json`.\n\nThen set `\"mode\": \"eir\"` in `config/settings.json`.\n\n## 3-Job Pipeline Architecture\n\n```\nJob A: material-prep\n  Search → Select → Crawl → Pack\n  Output: data/v9/tasks/{content_slug}.json\n\nJob B: content-gen (runs after Job A)\n  For each task → Spawn subagent → Generate → Validate → POST\n  Output: content posted to Eir Content API\n\nJob C: daily-brief (runs after Job B completes)\n  Check execution status → Complete missing tasks →\n  Compile brief → POST to Eir Brief API → Deliver summary\n```\n\n## Cron Setup\n\n```bash\n# Job A: Material preparation\nopenclaw cron add --name \"eir-material-prep\" \\\n  --cron \"0 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Read SKILL.md for eir-daily-content-curator. Run Eir mode material prep: eir_sync fetch → search → candidate_selector → agent selection → crawl → task_builder. Use references/candidates-spec.md for selection format.\"\n\n# Job B: Content generation (35 min after Job A)\nopenclaw cron add --name \"eir-content-gen\" \\\n  --cron \"35 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Read SKILL.md for eir-daily-content-curator. Read task files from data/v9/tasks/. For each task: generate Eir-format content using references/writer-prompt-eir.md, then POST via pipeline.eir_post.post_content(). Use topic_slug from task file (must match directive slugs, not Chinese labels). Include publishTime at item top level.\"\n\n# Job C: Daily brief (10 min after Job B, after subagent timeout)\nopenclaw cron add --name \"eir-daily-brief\" \\\n  --cron \"45 7 * * *\" --tz \"Asia/Shanghai\" \\\n  --session isolated --agent content \\\n  --message \"Check pipeline execution, complete missing tasks, compile daily brief, POST to brief API, send summary.\"\n```\n\n**Timing:** Job C starts after Job B's subagent timeout (5 min) to ensure all content is generated. Adjust gaps based on your typical task count.\n\n## Content Quality Rules\n\n- `dot.hook` ≤10 CJK chars / ≤6 EN words\n- `dot.category`: `focus` | `attention` | `seed`\n- `l1.bullets` 3-4 items, each ≤20 CJK chars\n- `sources` must have at least 1 entry\n- Never set any field to `null` — use `\"\"` or `[]`\n\nSee `content-spec.md` for full field constraints.\nSee `writer-prompt-eir.md` for the generation prompt.\n\n## API Endpoints\n\n| Endpoint | Purpose |\n|----------|---------|\n| `GET /oc/curation` | Fetch curation directives (topics + search hints) |\n| `POST /oc/content` | Push generated content items |\n| `POST /oc/brief` | Push daily brief |\n| `POST /oc/curation/miss` | Report topics with no quality content found |\n\nBase URL defaults to `https://api.heyeir.com/api`. Override with `EIR_API_URL` environment variable.\n\nSee `eir-api.md` for full API reference.\n\n## Interest Management\n\nEir provides a visual dashboard for viewing and managing your interests at heyeir.com.\n\n**Optional sync:** If you have local interests in `config/interests.json`, you can optionally sync them to Eir via the API:\n\n```bash\ncd scripts\npython3 -m pipeline.eir_sync fetch  # Fetch directives from Eir\n```\n\nThis is **entirely optional** — local interests work perfectly without syncing. The skill does NOT auto-upload interests.\n\n**Note:** Interest extraction (from USER.md) is a separate, manual process. See `references/interest-extraction-prompt.md` for details.\n\n## Validation\n\n```bash\ncd scripts\npython3 -m pipeline.validate_content           # check all generated files\npython3 -m pipeline.validate_content --fix     # auto-fix common issues\n```\n\nFile v3.117.0:references/interest-extraction-prompt.md\n\n# Interest Extraction Prompt\n\n> Local reference for extracting interests from USER.md profile.\n> Used manually or by agent to set up initial topics.\n> **Not part of the automated pipeline.**\n\n## Privacy Note\n\nInterest extraction produces **de-identified topic labels only** (e.g., \"AI agents\", \"smart driving\"). No personal data, profile content, or identifying information is stored or transmitted. All output is local to `config/interests.json`.\n\n---\n\n## Your Job\n\nAnalyze the USER.md reader profile → Extract genuine interests → Output to local config.\n\n**Check first:** Does the user already have interest/profile skills installed? If yes, consider using those instead of duplicating functionality.\n\n---\n\n## Core Principle: Infer Interests from Profile\n\nThe USER.md profile reveals what content the user would find valuable.\n\nAsk: *\"Someone with this profile — what public content would they want to read?\"*\n\n### Examples\n\n| Profile mentions... | Interest to extract |\n|---------------------|---------------------|\n| \"Building a RAG pipeline\" | AI retrieval systems, vector databases |\n| \"Researching MCP protocol\" | MCP Protocol, AI agent infrastructure |\n| \"Asking deep questions about embeddings\" | Embedding models, semantic search |\n| \"Discussing interior design for new home\" | Interior\n\nArchive v3.116.0: 29 files, 78129 bytes\n\nFiles: references/candidates-spec.md (2972b), references/content-spec.md (7948b), references/eir-api.md (4709b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3832b), references/interest-extraction-prompt.md (3778b), references/writer-prompt-eir.md (7132b), references/writer-prompt-standalone.md (4512b), scripts/connect.mjs (2346b), scripts/package.json (42b), scripts/pipeline/__init__.py (352b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2811b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (3997b), scripts/pipeline/eir_post.py (7402b), scripts/pipeline/eir_sync.py (6065b), scripts/pipeline/generate.py (5380b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (12731b), scripts/pipeline/search.py (28413b), scripts/pipeline/task_builder.py (17033b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (4143b), scripts/setup.py (6250b), SECURITY.md (3021b), SKILL.md (12347b), _meta.json (146b)\n\nArchive v3.115.0: 28 files, 73397 bytes\n\nFiles: references/content-spec.md (7723b), references/eir-api.md (4709b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3441b), references/interest-extraction-prompt.md (3778b), references/writer-prompt-eir.md (7003b), references/writer-prompt-standalone.md (4512b), scripts/connect.mjs (2346b), scripts/package.json (42b), scripts/pipeline/__init__.py (0b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2811b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (3093b), scripts/pipeline/eir_post.py (6382b), scripts/pipeline/eir_sync.py (6065b), scripts/pipeline/generate.py (4996b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (12731b), scripts/pipeline/search.py (28413b), scripts/pipeline/task_builder.py (16982b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (3027b), scripts/setup.py (5792b), SECURITY.md (3021b), SKILL.md (8437b), _meta.json (146b)\n\nArchive v1.3.3: 28 files, 70007 bytes\n\nFiles: references/content-spec.md (7723b), references/eir-api.md (4709b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3441b), references/interest-extraction-prompt.md (3778b), references/writer-prompt-eir.md (7003b), references/writer-prompt-standalone.md (4512b), scripts/connect.mjs (2346b), scripts/package.json (42b), scripts/pipeline/__init__.py (0b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2811b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (1969b), scripts/pipeline/eir_post.py (6382b), scripts/pipeline/eir_sync.py (3525b), scripts/pipeline/generate.py (4996b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (10885b), scripts/pipeline/search.py (21692b), scripts/pipeline/task_builder.py (16948b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (3027b), scripts/setup.py (5792b), SECURITY.md (3021b), SKILL.md (8437b), _meta.json (144b)\n\nArchive v1.3.2: 28 files, 70181 bytes\n\nFiles: references/content-spec.md (7723b), references/eir-api.md (4709b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3441b), references/interest-extraction-prompt.md (3778b), references/writer-prompt-eir.md (7003b), references/writer-prompt-standalone.md (4512b), scripts/connect.mjs (2875b), scripts/package.json (42b), scripts/pipeline/__init__.py (0b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2811b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (1969b), scripts/pipeline/eir_post.py (6382b), scripts/pipeline/eir_sync.py (3525b), scripts/pipeline/generate.py (4996b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (10885b), scripts/pipeline/search.py (21692b), scripts/pipeline/task_builder.py (16948b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (3027b), scripts/setup.py (5792b), SECURITY.md (3021b), SKILL.md (8437b), _meta.json (144b)\n\nArchive v1.3.1: 28 files, 70181 bytes\n\nFiles: references/content-spec.md (7723b), references/eir-api.md (4709b), references/eir-interest-rules.md (3201b), references/eir-setup.md (3441b), references/interest-extraction-prompt.md (3778b), references/writer-prompt-eir.md (7003b), references/writer-prompt-standalone.md (4512b), scripts/connect.mjs (2875b), scripts/package.json (42b), scripts/pipeline/__init__.py (0b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2811b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/directives.py (1969b), scripts/pipeline/eir_post.py (6382b), scripts/pipeline/eir_sync.py (3525b), scripts/pipeline/generate.py (4996b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/run_state.py (10885b), scripts/pipeline/search.py (21692b), scripts/pipeline/task_builder.py (16948b), scripts/pipeline/validate_content.py (7863b), scripts/pipeline/workspace.py (3027b), scripts/setup.py (5792b), SECURITY.md (3021b), SKILL.md (8437b), _meta.json (144b)\n\nArchive v1.3.0: 2 files, 415 bytes\n\nFiles: SKILL.md (52b), _meta.json (144b)\n\nArchive v1.2.0: 25 files, 67430 bytes\n\nFiles: references/content-spec.md (7723b), references/eir-api.md (4709b), references/eir-interest-rules.md (3103b), references/eir-setup.md (2854b), references/interest-extraction-prompt.md (5111b), references/writer-prompt-eir.md (6619b), references/writer-prompt-standalone.md (4212b), scripts/connect.mjs (2024b), scripts/package.json (42b), scripts/pipeline/__init__.py (0b), scripts/pipeline/candidate_selector.py (6764b), scripts/pipeline/config.py (2851b), scripts/pipeline/crawl.py (29444b), scripts/pipeline/date_extractor.py (9936b), scripts/pipeline/eir_config.py (3027b), scripts/pipeline/generate_and_post.py (12094b), scripts/pipeline/grounding.py (4607b), scripts/pipeline/pack_tasks.py (17935b), scripts/pipeline/run_state.py (10885b), scripts/pipeline/search.py (24153b), scripts/pipeline/validate_content.py (7863b), scripts/setup.py (5792b), SECURITY.md (1669b), SKILL.md (7827b), _meta.json (144b)","readmeExcerpt":"Skill: Eir Daily Content Curator Owner: heyeir Summary: Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news',... Tags: content pipeline:1.0.2, curate content:1.0.2, daily digest:1.0.2, daily news:1.0.2, interest tracking:1.0.2, latest:3.119.0, personalized news briefing:1.0.2 Version history: v3.119.0 | 2026-04-25T","codeSnippets":[],"executableExamples":[{"language":"text","snippet":"1. Configure          → Set up search API + interests (one-time)\n2. Search             → Search API queries for each interest topic\n3. Select + Crawl     → Agent picks best candidates, fetches full content\n4. Generate           → Agent writes structured summaries from task files\n5. Daily Brief        → Agent compiles brief from generated items"},{"language":"bash","snippet":"# Option A: inline JSON\npython3 scripts/setup.py --init --settings '{\n  \"mode\": \"standalone\",\n  \"language\": \"en\",\n  \"search\": {\n    \"search_base_url\": \"https://api.search.brave.com/res/v1\",\n    \"search_api_key\": \"YOUR_BRAVE_API_KEY\"\n  }\n}'\n\n# Option B: settings file (recommended for PowerShell/Windows)\npython3 scripts/setup.py --init --settings-file path/to/settings.json"},{"language":"text","snippet":"Search provider examples:\n| Provider | `search_base_url` | Get API key |\n|----------|-------------------|-------------|\n| Brave Search | `https://api.search.brave.com/res/v1` | [brave.com/search/api](https://brave.com/search/api/) |\n| Tavily | `https://api.tavily.com` | [tavily.com](https://tavily.com/) |\n\n> **Want richer results?** Install [SearXNG](https://docs.searxng.org/) and/or [Crawl4AI](https://github.com/unclecode/crawl4ai) locally. Add `searxng_url` and `crawl4ai_url` to your search config — they work as fallback or primary search/crawl providers.\n\n**2. Set up interests** — edit the generated `config/interests.json`:"},{"language":"text","snippet":"Interests can also be auto-extracted — see `references/interest-extraction-prompt.md`.\n\n**Freshness and tier:** Each topic supports optional `freshness` (e.g. `\"3d\"`, `\"7d\"`, `\"14d\"`) and `tier` (`\"focus\"`, `\"tracked\"`, `\"explore\"`, `\"seed\"`) fields in `interests.json`. In Eir mode, these come from the API directives. In standalone mode, defaults are `\"7d\"` and `\"tracked\"`. The search pipeline uses tier to decide search depth (focus/tracked get entity refinement) and freshness to filter stale results.\n\n**3. Run the search + crawl pipeline** (from the `scripts/` directory):"},{"language":"text","snippet":"> All `python3 -m pipeline.*` commands must be run from the `scripts/` directory.\n\n**Agent selection step** (between `candidate_selector` and `crawl`):\n\n`candidate_selector` outputs per-topic JSON files to `data/v9/topics/`. Your agent should:\n1. Read each topic file\n2. Pick 0-3 candidates per topic based on relevance and freshness\n3. Write `data/v9/candidates.json` with the selected candidates\n\nSee `references/candidates-spec.md` for the exact JSON format.\n\n> **Note on crawl fallback:** If a candidate URL isn't in the search cache, `crawl.py` automatically tries: Browse API → Crawl4AI → web_fetch → HTML head extraction. No manual intervention needed.\n\n**4. Generate content** (agent-driven):\n\nAfter `task_builder`, task files are in `data/v9/tasks/`. Tell your OpenClaw agent:"},{"language":"text","snippet":"**Language:** The output language is determined by: task `output_lang` field → curation API `user.primaryLanguage` → `settings.json` `language` field. If none are set, generate content in the user's chat language.\n\n**Scheduling tip:** If you want automated daily runs, you can set up a cron job:"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: eir-daily-content-curator\ndescription: \"Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news', 'curate content for me', 'what should I read today', 'personalized news briefing', 'daily digest', 'news summary', 'content pipeline', 'interest tracking', 'automated content curation'.\"\nmetadata:\n  openclaw:\n    emoji: \"📰\"\n    requires:\n      bins: [\"python3\"]\n      env:\n        EIR_API_KEY: \"Eir API bearer token (Eir mode only)\"\n        EIR_API_URL: \"Eir API base URL (optional override)\"\n        SEARCH_API_KEY: \"Search provider API key (Brave, Tavily, etc.)\"\n---\n\n# Daily Content Curator\n\nCurates personalized content based on your interests. Supports two modes:\n\n- **Standalone** — works locally, no external account needed\n- **Eir** — full curation + delivery via [heyeir.com](https://www.heyeir.com)\n\n## Getting Started\n\n**Before setup, ask the user which mode to use:**\n\n| Mode | What it does | Requirements |\n|------|--------------|---------------|\n| **Standalone** | Search → curate → generate summaries locally | Search API key (Brave, Tavily, etc.) |\n| **Eir** | Full pipeline with delivery to Eir app | [Eir account](https://www.heyeir.com) + pairing code |\n\n> **Important:** The two modes use different content formats and topic slug conventions. Choose the correct mode at setup time — switching later requires reconfiguration. If the user has an Eir account, use Eir mode.\n\nThen follow the corresponding setup section below.\n\n## Standalone Mode\n\n### Flow\n\n```\n1. Configure          → Set up search API + interests (one-time)\n2. Search             → Search API queries for each interest topic\n3. Select + Crawl     → Agent picks best candidates, fetches full content\n4. Generate           → Agent writes structured summaries from task files\n5. Daily Brief        → Agent compiles brief from generated items\n```\n\n> Steps 1-3 are Python scripts you run directly. Steps 4-5 are **agent-driven** — you tell your OpenClaw agent to read the task files and generate content. The agent uses whatever LLM model is configured in your OpenClaw session (e.g. Claude, GPT-4, Gemini).\n\n### Quick Start\n\n**1. Initialize workspace** — creates `config/` directory and default settings:\n```bash\n# Option A: inline JSON\npython3 scripts/setup.py --init --settings '{\n  \"mode\": \"standalone\",\n  \"language\": \"en\",\n  \"search\": {\n    \"search_base_url\": \"https://api.search.brave.com/res/v1\",\n    \"search_api_key\": \"YOUR_BRAVE_API_KEY\"\n  }\n}'\n\n# Option B: settings file (recommended for PowerShell/Windows)\npython3 scripts/setup.py --init --settings-file path/to/settings.json\n```\n```\n\nSearch provider examples:\n| Provider | `search_base_url` | Get API key |\n|----------|-------------------|-------------|\n| Brave Search | `https://api.search.brave.com/res/v1` | [brave.com/search/api](https://brave.com/search/api/) |\n| Tavily | `https://api.tavily.com` | [tavily.com](https://tavily.com/) |\n\n> **Wa"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn76fghgn0qqq4e4qdknvea95s8222rw\",\n  \"slug\": \"eir-daily-content-curator\",\n  \"version\": \"3.119.0\",\n  \"publishedAt\": 1777117038442\n}"},{"path":"references/candidates-spec.md","content":"# Candidates JSON Format Specification\n\nThis document defines the expected format of `candidates.json`, which is the handoff point between candidate selection (agent-driven) and crawling/task building (automated scripts).\n\n## Location\n\n`data/v9/candidates.json`\n\n## Structure\n\n```json\n{\n  \"candidates\": [\n    {\n      \"content_slug\": \"string (required) — kebab-case identifier, 3-6 words, e.g. 'openai-gpt-5-launch'\",\n      \"matched_topic_slug\": \"string (required) — must match a directive slug, e.g. 'ai-industry-news'\",\n      \"suggested_angle\": \"string (required) — editorial angle in output language\",\n      \"reason\": \"string (optional) — why this candidate was selected\",\n      \"priority\": \"string (optional) — 'high' | 'medium' | 'low', default 'medium'\",\n      \"source_urls\": [\n        \"string (required, 1-5 URLs) — URLs to crawl for full content\"\n      ],\n      \"source_titles\": {\n        \"https://example.com/article\": \"Article Title\"\n      }\n    }\n  ],\n  \"selected_at\": \"ISO 8601 timestamp\"\n}\n```\n\n## Field Details\n\n### Required Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `content_slug` | string | Unique identifier for this content piece. Used as filename and API slug. Must be kebab-case, 3-6 words. |\n| `matched_topic_slug` | string | The directive topic this belongs to. Must match a slug from directives/interests. Used as `topicSlug` in generated content. |\n| `suggested_angle` | string | The editorial angle — what makes this worth covering. In the output language. |\n| `source_urls` | string[] | 1-5 URLs to crawl. Prefer diverse domains. At least one must be crawlable. |\n\n### Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `reason` | string | Why this candidate was selected (for audit trail). |\n| `priority` | string | `high` / `medium` / `low`. Affects generation order. Default: `medium`. |\n| `source_titles` | object | Map of URL → article title (helps crawl quality scoring). |\n\n## Downstream Usage\n\n1. **`crawl.py`** reads `candidates.json`, crawls each `source_urls` entry, saves content to `data/v9/snippets/{url_hash}.json`.\n2. **`task_builder.py`** reads crawled candidates, bundles source text + writer prompt into task files at `data/v9/tasks/{content_slug}.json`.\n\n## How Candidates Are Created\n\n### Eir Mode (cron-driven)\nThe agent reads per-topic files from `data/v9/topics/`, evaluates search results, and writes `candidates.json` directly.\n\n### Standalone Mode (manual)\n1. Run `python3 -m pipeline.candidate_selector` → generates topic files in `data/v9/topics/`\n2. Review topic files and create `candidates.json` following the format above\n3. Run `python3 -m pipeline.crawl` → crawls candidate URLs\n\n## Validation\n\n- `content_slug` must be unique across all candidates\n- `matched_topic_slug` should match a known directive/interest slug\n- `source_urls` must contain at least 1 valid HTTP(S) URL\n- No `null` values — use `\"\"` or `[]` for empty fields"},{"path":"references/content-spec.md","content":"# Eir Content Specification\n\n> Single source of truth for all content field constraints and quality criteria.\n> Used by: writer prompts, API validation, front-end rendering.\n\n## Contents\n\n- [Field Reference](#field-reference) — dot, l1, l2, sources fields\n- [via vs sources](#via-vs-sources) — Attribution handling\n- [lang field](#lang-field) — Language requirements\n- [Null handling](#null-handling)\n- [Validation summary](#validation-summary)\n- [Content ID format](#content-id-format)\n- [Interest Signals](#interest-signals)\n\n---\n\n## Field Reference\n\n### dot (L0 — the dot on canvas)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `hook` | string | ≤10 CJK chars / ≤6 EN words | **100 chars** (API rejects) | Creates curiosity gap. No hype words (\"Breaking\", \"Exciting\"). Rendered as single-line label on the dot. |\n| `category` | enum | — | `focus` \\| `attention` \\| `seed` | Determines dot visual style. |\n\n### l1 (card — what the user sees first)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `title` | string | 15-40 CJK chars / 8-15 EN words | **200 chars** (API rejects) | Opinionated, not a headline. Must be in `lang`. |\n| `summary` | string | 50-80 words | — | 2-3 sentences. Advances beyond the title — don't repeat. |\n| `key_quote` | string | 1 sentence | — | Best direct quote from sources. Use `\"\"` if none. |\n| `via` | **string[]** | — | — | **Must be an array.** Auto-derived from `sources[].name`. Pipeline populates it; API also falls back to `sources[].name` if empty. Writer should NOT set this. |\n| `bullets` | string[] | 3-4 items | 10 items (API rejects) | Each: ≤20 CJK chars / ≤50 EN chars. Don't repeat summary. |\n\n### l2 (depth — expanded view)\n\n| Field | Type | Recommended | Hard Limit | Notes |\n|-------|------|-------------|------------|-------|\n| `content` | string | 200-600 CJK chars / 150-400 EN words | — | 2-4 paragraphs separated by `\\n\\n`. Starts where summary left off. |\n| `bullets` | array | 3-5 items | — | Each: `{text: string, confidence: \"high\"\\|\"medium\"\\|\"low\"}`. Concrete facts with numbers/names. Every bullet must have supporting detail in `content`. |\n| `context` | string | 1-2 sentences | — | Optional. \"SO WHAT for the reader.\" Omit or leave empty if not needed. |\n| `eir_take` | string | 1 sentence | — | Optional. Eir's sharp opinion. **PUBLIC** (visible on share pages) if included. |\n| `related_topics` | string[] | 3-5 items | — | Human-readable phrases in `lang`. NOT slugs. e.g. `\"Vector Search and ANN Algorithms\"` ✅, `\"vector-search-ann\"` ❌ |\n\n### sources (provenance — machine-readable)\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n| `url` | string | **Yes** | Must be valid URL. Used for server-side dedup — duplicate URLs are rejected. |\n| `title` | string | No | Original article title. |\n| `name` | string | No | Publisher/source name (e.g. \"MIT Technology Review\"). This is what `l"},{"path":"references/eir-api.md","content":"# Eir API Reference\n\n**Base URL**: `https://api.heyeir.com/api` (override with `EIR_API_URL` environment variable)\n\n**Authentication**: `Authorization: Bearer <EIR_API_KEY>` for all `/oc/*` endpoints.\n\n## Contents\n\n- [Connection](#connection) — Register/disconnect/rotate keys\n- [Interests](#interests) — Manage user interests\n- [Curation](#curation) — Fetch directives, report misses\n- [Content](#content) — Push/read/delete content items\n\n---\n\n## Connection\n\n### POST /oc/connect\nRegister with pairing code.\n\n**Request:** `{ \"code\": \"ABCD-1234\" }`\n\n**Response:** `{ \"apiKey\": \"eir_oc_xxx\", \"userId\": \"u_abc123\" }`\n\n### DELETE /oc/connect\nDisconnect and revoke API key.\n\n### POST /oc/refresh-key\nRotate API key (60s grace period).\n\n---\n\n## Interests\n\n### GET /oc/interests\nReturns user interests.\n\n**Response:**\n```json\n{\n  \"user\": { \"id\": \"u_xxx\", \"primaryLanguage\": \"zh\", \"bilingual\": false },\n  \"interests\": [\n    {\n      \"id\": \"ui_abc1234\",\n      \"slug\": \"artificial-intelligence\",\n      \"label\": \"Artificial Intelligence\",\n      \"status\": \"active\",\n      \"heat\": 5,\n      \"strength\": 0.6\n    }\n  ]\n}\n```\n\n### POST /oc/interests/add\nAdd interests by label. Server matches against dictionary.\n\n**Request:** `{ \"labels\": [\"AI Agents\", \"MCP\"], \"lang\": \"en\" }`\n\n**Response:** `{ \"added\": 2, \"results\": [...] }`\n\n---\n\n## Curation\n\n### GET /oc/curation\nReturns curation directives for content collection.\n\n**Response:**\n```json\n{\n  \"schema_version\": \"1.0\",\n  \"user\": {\n    \"primaryLanguage\": \"zh\",\n    \"bilingual\": false\n  },\n  \"directives\": [\n    {\n      \"slug\": \"mcp-protocol\",\n      \"label\": \"MCP Protocol\",\n      \"tier\": \"tracked\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"MCP 2.0 announced\", \"Anthropic MCP ecosystem\"],\n      \"userNeeds\": \"Protocol updates and adoption\",\n      \"trackingGoal\": \"Stay current on protocol updates\"\n    },\n    {\n      \"slug\": \"ai-agents\",\n      \"label\": \"AI Agents\",\n      \"tier\": \"focus\",\n      \"freshness\": \"7d\",\n      \"searchHints\": [\"AI agent frameworks comparison\", \"autonomous agent production\"],\n      \"userNeeds\": null,\n      \"trackingGoal\": null\n    }\n  ],\n  \"exclude\": {\n    \"disliked\": [\"crypto\", \"nft\"]\n  }\n}\n```\n\n**Tiers:** tracked → focus → explore → seed (informational labels; selection is score-based).\n\n**Server-side curation:** The API handles topic selection, cooldown, and scoring internally. The agent just reads directives and finds content for them.\n\nSee `eir-interest-rules.md` for curation guidelines.\n\n---\n\n## Content\n\n### POST /oc/content\nPush generated content.\n\n**Request:**\n```json\n{\n  \"items\": [\n    {\n      \"slug\": \"mcp-protocol-2-0\",\n      \"lang\": \"en\",\n      \"interests\": {\n        \"anchor\": [\"mcp-protocol\"],\n        \"related\": [{ \"slug\": \"a2a-protocol\", \"label\": \"A2A Protocol\" }]\n      },\n      \"dot\": {\n        \"hook\": \"MCP 2.0 Released\",\n        \"category\": \"focus\",\n      },\n      \"l1\": {\n        \"title\": \"MCP Protocol v2.0\",\n        \"summary\": \"Anthropic releases MCP 2.0...\",\n        \"key_quote\": \"...\"\n      },\n   "}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news',... Skill: Eir Daily Content Curator Owner: heyeir Summary: Daily AI news curation — learns interests from your profile, searches the web, delivers structured summaries and daily briefs. Use when: 'set up daily news',... Tags: content pipeline:1.0.2, curate content:1.0.2, daily digest:1.0.2, daily news:1.0.2, interest tracking:1.0.2, latest:3.119.0, personalized news briefing:1.0.2 Version history: v3.119.0 | 2026-04-25T","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1208,"uniquenessScore":54,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T18:05:42.716Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T18:05:42.716Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T20:58:48.758Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}