ai-daily · diff

git:20260903.05e0c06 to git:20260910.98ba754

68 added, 36 removed. Audit A to A.

---
name: ai-daily
description: Daily AI news briefing skill. Fetches today's content from independent news sites, YouTube AI channels, X.com AI accounts, and Google DeepMind/Anthropic/OpenAI/Claude official blogs. Outputs a bilingual (Chinese + English) Markdown digest. HTML newspaper layout is optional — ask the user after the Markdown is shown. Use when the user says "daily brief", "AI news", "today's brief", "每日简报", "AI日报", or asks for today's AI news.
---
# AI Daily — Daily AI Brief
## Purpose
Fetch and present today's AI news in a bilingual digest with a professional HTML newspaper layout.
## Paths
All paths use `<BASE>` — the skill's base directory, which Claude Code provides in the system context as "Base directory for this skill: ...". Use it to construct absolute paths for all bash commands.
- Scripts: `<BASE>/scripts/`
- Templates: `<BASE>/assets/rationalist/` and `<BASE>/assets/modernism/`
- Seed images: `<BASE>/output/` — hero images bundled with the skill
- - **Generated output: `~/.claude/ai-daily/output/`** — fixed location outside the skill, persists across updates
+ - **Generated output locations:**
+ - **Workspace output (Primary):** `<WORKSPACE>/output/` (e.g. `d:/Users/Alex/AI-Daily/output/`) — for immediate viewing and access in the IDE file tree
+ - **Global backup:** `~/.claude/ai-daily/output/` (Windows: `%USERPROFILE%\.claude\ai-daily\output\`) — persistent storage across updates
+ *Always write or copy generated output files to both locations.*
## Setup Requirements
- **X.com credentials:** `~/.claude/private/x-creds.json` (see README for details)
- **twscrape:** `pip install --upgrade "twscrape[curl]>=0.20.0"`
## Trigger Phrases
- "daily brief" / "ai news" / "today's ai news"
- "每日简报" / "AI日报" / "今天的新闻"
- "fetch my daily brief"
---
## Workflow
### Step 1 — Calculate Dates
Beijing Time (UTC+8).
- **File name:** today's date (the run date). e.g. run on 2026-06-20 → `daily-brief-2026-06-20.md`
- **Content window:** yesterday (full 24 hours) + today up to the current run time. e.g. run at 10:00 AM on 6/20 → include all 6/19 content + any 6/20 content published before 10:00 AM Beijing time.
### Step 1.5 — Initialize Output Directory (first run only)
- Check if `~/.claude/ai-daily/output/` exists. If not, create it and copy the hero images there. Run silently.
+ Check if output directories exist. Create them and copy the hero images there. Run silently.
```bash
# macOS / Linux
- mkdir -p ~/.claude/ai-daily/output
- for img in spotlight.jpg youtubepicks.jpg spotlight.png youtubepicks.png; do
- [ ! -f ~/.claude/ai-daily/output/$img ] && cp "<BASE>/output/$img" ~/.claude/ai-daily/output/$img || true
+ for dir in ~/.claude/ai-daily/output ./output; do
+ mkdir -p "$dir"
+ for img in spotlight.jpg youtubepicks.jpg spotlight.png youtubepicks.png; do
+ [ ! -f "$dir/$img" ] && cp "<BASE>/output/$img" "$dir/$img" || true
+ done
done
```
```powershell
# Windows
- if (-not (Test-Path "$env:USERPROFILE\.claude\ai-daily\output")) { New-Item -ItemType Directory -Path "$env:USERPROFILE\.claude\ai-daily\output" -Force | Out-Null }
- foreach ($img in @("spotlight.jpg","youtubepicks.jpg","spotlight.png","youtubepicks.png")) {
- $dst = "$env:USERPROFILE\.claude\ai-daily\output\$img"
- if (-not (Test-Path $dst)) { Copy-Item "<BASE>\output\$img" $dst }
+ $dirs = @("$env:USERPROFILE\.claude\ai-daily\output", "output")
+ foreach ($d in $dirs) {
+ if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
+ foreach ($img in @("spotlight.jpg","youtubepicks.jpg","spotlight.png","youtubepicks.png")) {
+ $dst = Join-Path $d $img
+ if (-not (Test-Path $dst)) { Copy-Item "<BASE>\output\$img" $dst -Force }
+ }
}
```
+
### Step 2 — Run All Fetch Scripts (parallel)
Run all three in one turn as parallel Bash calls. Replace `<BASE>` with the skill's base directory from context:
```bash
python "<BASE>/scripts/fetch_youtube.py"
```
```bash
python "<BASE>/scripts/fetch_x.py"
```
```bash
python "<BASE>/scripts/fetch_news.py"
```
Each script auto-retries failed requests up to 3 times (2s / 4s backoff) before giving up.
The scripts also write machine-readable status markers to stderr. An empty JSON
array is not, by itself, proof that a source was reachable.
**Script outputs:**
- `fetch_youtube.py` → JSON array of long-form videos from subscribed AI channels; YouTube Shorts are excluded
- `fetch_x.py` → JSON array of posts sorted by engagement (takes 1-2 min)
- `fetch_news.py` → JSON array with `title, url, source, category, date, summary`; stderr shows `[OK]` / `[FAIL]` per source
**Date window (all scripts):** yesterday full day + today up to run time, Beijing time (UTC+8). 0 results is normal on weekends and slow news days.
### Step 3 — Verify Data and Self-Repair Before Proceeding
Do not proceed from counts alone. For each finished process, verify all of the
following before writing the digest:
1. The process exit code is 0.
2. stdout parses as a JSON array (a JSON object containing `error` is a
failure, not an empty result).
3. Every returned item has the required fields and a non-empty HTTP(S) URL.
4. stderr contains an explicit reachability result:
- `fetch_news.py`: every configured source is `[OK]`, or is explicitly
`[WARN]` with a working fallback; `[XML ERROR]` without a later fallback
`[OK]` is a failure.
- `fetch_youtube.py`: each channel is reported; 0 videos is valid only when
the channel feed is `[OK]`. Every returned URL must be a long-form video
URL; any `/shorts/` URL is a validation failure.
- `fetch_x.py`: there must be an `[OK] X.com` line showing at least one
account succeeded. 0 posts can be a valid slow-news result only with that
line. `[FAIL]`, credential errors, transaction-id/parser errors, or all
account lookups failing are real failures.
Record the verified counts:
- News articles: N
- YouTube videos: N
- X.com posts: N
#### Automatic recovery loop
If any process, source, channel, or account fails validation, preserve its
stderr and repair before composing Markdown. Perform at most two recovery
passes, then verify again after each pass:
1. Classify the failure from the status line and error text. Do not turn a
failed request into a fabricated 0-result section.
2. Re-run the affected endpoint/script after its built-in retry delay. For
X.com, confirm credentials are present, use the configured Windows/system
proxy explicitly, use the X bundle compatibility path in
`fetch_x.py`, and allow its verified public-profile HTML fallback when the
authenticated UserTweets queue is rate-limited; for migrated feeds, try the source's alternate RSS/page
fallback; for YouTube, retry only the failed channel feeds first.
3. Re-parse stdout and stderr and repeat the validation checklist. If the
retry succeeds, use only the verified result. If it remains partial, keep
the successful data and add a bilingual `抓取备注 / Fetch note` naming the
unavailable source(s).
If all three scripts fail, or if `fetch_news.py` has no reachable source after
the recovery passes, stop and tell the user which status/error caused the
block. If at least one section has verified data and the remaining failures
are explicitly recorded, continue without silently presenting them as empty.
### Step 4 — Save Markdown File & Ask About HTML
- Save the full digest as a Markdown file:
+ Save the full digest as a Markdown file to BOTH the workspace output directory (for direct IDE access) and the persistent global backup directory:
- - macOS/Linux: `~/.claude/ai-daily/output/daily-brief-YYYY-MM-DD.md`
- - Windows: `%USERPROFILE%\.claude\ai-daily\output\daily-brief-YYYY-MM-DD.md`
+ - **Workspace (Primary):** `<WORKSPACE>/output/daily-brief-YYYY-MM-DD.md`
+ - **Global backup:**
+ - Windows: `%USERPROFILE%\.claude\ai-daily\output\daily-brief-YYYY-MM-DD.md`
+ - macOS/Linux: `~/.claude/ai-daily/output/daily-brief-YYYY-MM-DD.md`
Then open it:
```powershell
# Windows
- $brief = "$env:USERPROFILE\.claude\ai-daily\output\daily-brief-YYYY-MM-DD.md"
- if (-not (Test-Path -LiteralPath $brief) -or (Get-Item -LiteralPath $brief).Length -eq 0) {
- throw "AI Daily Markdown was not written or is empty: $brief"
+ $globalBrief = "$env:USERPROFILE\.claude\ai-daily\output\daily-brief-YYYY-MM-DD.md"
+ $wsBrief = "output\daily-brief-YYYY-MM-DD.md"
+ if (-not (Test-Path -LiteralPath $globalBrief) -or (Get-Item -LiteralPath $globalBrief).Length -eq 0) {
+ throw "AI Daily Markdown was not written or is empty: $globalBrief"
}
- $body = Get-Content -LiteralPath $brief -Raw -Encoding utf8
+ $body = Get-Content -LiteralPath $globalBrief -Raw -Encoding utf8
if ($body -notmatch '^# AI Daily Brief — \d{4}-\d{2}-\d{2}' -or
$body -notmatch '# 中文版' -or $body -notmatch '# English Version') {
- throw "AI Daily Markdown failed structural verification: $brief"
+ throw "AI Daily Markdown failed structural verification: $globalBrief"
}
- # A fresh VS Code window prevents an old dirty/empty editor buffer for the
- # same path from hiding the newly written file.
- Start-Process code -ArgumentList '--new-window', $brief
- # macOS
- open ~/.claude/ai-daily/output/daily-brief-YYYY-MM-DD.md
+ # Sync to workspace output directory
+ if (-not (Test-Path "output")) { New-Item -ItemType Directory -Path "output" -Force | Out-Null }
+ Copy-Item -LiteralPath $globalBrief -Destination $wsBrief -Force
+
+ # Open in IDE or system default
+ try { Start-Process code -ArgumentList '--new-window', (Resolve-Path $wsBrief).Path } catch {}
+ Invoke-Item -LiteralPath $wsBrief
```
+ ```bash
+ # macOS / Linux
+ mkdir -p ./output
+ cp ~/.claude/ai-daily/output/daily-brief-YYYY-MM-DD.md ./output/daily-brief-YYYY-MM-DD.md
+ open ./output/daily-brief-YYYY-MM-DD.md || open ~/.claude/ai-daily/output/daily-brief-YYYY-MM-DD.md
+ ```
- In chat, output only a brief confirmation: filename saved, total item counts per section, and 1-line summary of the top insight.
+ **Chat Output Rules:**
+ In chat, output a brief confirmation with:
+ 1. **Clickable Links**: ALWAYS provide clickable markdown links using the `file:///` URI scheme with forward slashes (e.g. `[output/daily-brief-YYYY-MM-DD.md](file:///d:/Users/Alex/AI-Daily/output/daily-brief-YYYY-MM-DD.md)`).
+ 2. **Never output bare `~` on Windows**: Always output the resolved absolute path or workspace path, since Windows Explorer and run prompts cannot resolve `~`.
+ 3. Total item counts per section and 1-line summary of top insight.
After that, ask:
> "要生成 HTML 报纸版式吗?(输入 Y 生成并打开)"
Only generate HTML if the user confirms. If yes, ask which style, then follow the HTML Generation section below.
---
## HTML Generation — Style Selection
When the user confirms HTML output, ask:
> 选择排版风格:
> - **Rationalist** — Works in Progress 风格:学术编辑风,衬线+等宽字体,米白背景,sticky hero 图片,双语切换
> - **Modernism** — Monocle 风格:极简黑白,EB Garamond 衬线,大号 masthead 滚动收缩,4列 YouTube 卡片,双列 X.com
>
> 输入风格名称(Rationalist / Modernism):
---
## HTML Generation — Template-Based Workflow
**Do NOT generate HTML from scratch.** Always read the template for the chosen style, fill placeholders, expand REPEAT blocks, and save.
- Hero images are pre-placed in `~/.claude/ai-daily/output/` (seeded in Step 1.5) — no copying needed during HTML generation.
+ Hero images are pre-placed in both output directories (seeded in Step 1.5).
---
### Rationalist
**Template:** `<BASE>/assets/rationalist/template.html`
- **Output file:**
- - macOS/Linux: `~/.claude/ai-daily/output/r-brief-YYYY-MM-DD.html`
- - Windows: `%USERPROFILE%\.claude\ai-daily\output\r-brief-YYYY-MM-DD.html`
+ **Output files (save to both):**
+ - Workspace: `<WORKSPACE>/output/r-brief-YYYY-MM-DD.html`
+ - Global backup:
+ - macOS/Linux: `~/.claude/ai-daily/output/r-brief-YYYY-MM-DD.html`
+ - Windows: `%USERPROFILE%\.claude\ai-daily\output\r-brief-YYYY-MM-DD.html`
**Steps:**
1. Read `<BASE>/assets/rationalist/template.html`
2. Fill every `{{PLACEHOLDER}}` with today's content from the Markdown digest
3. For Spotlight and YouTube Picks: expand `<!-- REPEAT ... /REPEAT -->` blocks — one `.si` card per article/video beyond the hero
- 4. Save the filled HTML to the output path above
+ 4. Save the filled HTML to both the global backup directory and workspace `output/` directory
5. Open the file:
```bash
# Windows
- start "%USERPROFILE%\.claude\ai-daily\output\r-brief-YYYY-MM-DD.html"
+ Invoke-Item "output\r-brief-YYYY-MM-DD.html"
# macOS
- open ~/.claude/ai-daily/output/r-brief-YYYY-MM-DD.html
+ open output/r-brief-YYYY-MM-DD.html
```
+ 6. In chat, provide clickable `file:///` links to the generated HTML file.
+
**Layout notes:**
- Spotlight: sticky hero card (blue caption) left + 2-col card grid right
- YouTube Picks: sticky hero card (gold caption) left + 2-col card grid right
- X.com: 1-column vertical feed
**Placeholder Reference — Rationalist:**
| Placeholder | Value |
|---|---|
| `{{DATE_EN}}` | e.g. `June 20, 2026` |
| `{{OFFICIAL_COUNT}}` | number of Official Updates articles |
| `{{NEWS_COUNT}}` | number of Independent News articles |
| `{{SPOTLIGHT_HERO_URL}}` | URL of the top Spotlight article |
| `{{SPOTLIGHT_HERO_TITLE_EN}}` | English title of hero article |
| `{{SPOTLIGHT_HERO_TITLE_CN}}` | Chinese title of hero article |
| `{{SPOTLIGHT_HERO_SOURCE}}` | Source name (e.g. `Claude Blog`) |
| `{{SPOTLIGHT_HERO_SUMMARY_EN}}` | 1-2 sentence English summary |
| `{{SPOTLIGHT_HERO_SUMMARY_CN}}` | 1-2 sentence Chinese summary |
| `{{SPOTLIGHT_HERO_DATE}}` | Display date shared by both panels (e.g. `Jun 18, 2026`) |
| `{{SPOTLIGHT_1_URL}}` `{{SPOTLIGHT_1_TITLE_EN}}` `{{SPOTLIGHT_1_TITLE_CN}}` `{{SPOTLIGHT_1_SOURCE}}` `{{SPOTLIGHT_1_SUMMARY_EN}}` `{{SPOTLIGHT_1_SUMMARY_CN}}` `{{SPOTLIGHT_1_DATE}}` | Same fields for each additional Spotlight article (expand REPEAT block) |
| `{{YT_COUNT}}` | number of YouTube videos |
| `{{YT_HERO_URL}}` `{{YT_HERO_TITLE_EN}}` `{{YT_HERO_TITLE_CN}}` `{{YT_HERO_SOURCE}}` `{{YT_HERO_SUMMARY_EN}}` `{{YT_HERO_SUMMARY_CN}}` `{{YT_HERO_DATE}}` | Top YouTube video (the hero) |
| `{{YT_1_URL}}` `{{YT_1_TITLE_EN}}` `{{YT_1_TITLE_CN}}` `{{YT_1_SOURCE}}` `{{YT_1_SUMMARY_EN}}` `{{YT_1_SUMMARY_CN}}` `{{YT_1_DATE}}` | Each additional YouTube video (expand REPEAT block) |
| `{{X_COUNT}}` | number of X.com posts |
| `{{X_1_HANDLE}}` | e.g. `@swyx` |
| `{{X_1_LIKES}}` | e.g. `12,209` |
| `{{X_1_DATE}}` | e.g. `Jun 19` (shared by both panels) |
| `{{X_1_BODY_EN}}` | English post text |
| `{{X_1_BODY_CN}}` | Chinese translation of post |
| `{{X_1_URL}}` | Post URL |
---
### Modernism
**Template:** `<BASE>/assets/modernism/template.html`
- **Output file:**
- - macOS/Linux: `~/.claude/ai-daily/output/m-brief-YYYY-MM-DD.html`
- - Windows: `%USERPROFILE%\.claude\ai-daily\output\m-brief-YYYY-MM-DD.html`
+ **Output files (save to both):**
+ - Workspace: `<WORKSPACE>/output/m-brief-YYYY-MM-DD.html`
+ - Global backup:
+ - macOS/Linux: `~/.claude/ai-daily/output/m-brief-YYYY-MM-DD.html`
+ - Windows: `%USERPROFILE%\.claude\ai-daily\output\m-brief-YYYY-MM-DD.html`
**Steps:**
1. Read `<BASE>/assets/modernism/template.html`
2. Fill every `{{PLACEHOLDER}}` with today's content from the Markdown digest
3. For Spotlight stack: expand `<!-- REPEAT .stack-item /REPEAT -->` — one `.stack-item` per article beyond the hero (stack items have no summary, only tag + title + byline + CTA)
4. For YouTube Picks: expand `<!-- REPEAT .card /REPEAT -->` — one `.card` per video (all videos are cards; there is no separate hero video)
5. For X.com: expand `<!-- REPEAT .x-row /REPEAT -->` — one `.x-row` per post
- 6. Save the filled HTML to the output path above
+ 6. Save the filled HTML to both the global backup directory and workspace `output/` directory
7. Open the file:
```bash
# Windows
- start "%USERPROFILE%\.claude\ai-daily\output\m-brief-YYYY-MM-DD.html"
+ Invoke-Item "output\m-brief-YYYY-MM-DD.html"
# macOS
- open ~/.claude/ai-daily/output/m-brief-YYYY-MM-DD.html
+ open output/m-brief-YYYY-MM-DD.html
```
+ 8. In chat, provide clickable `file:///` links to the generated HTML file.
+
**Layout notes:**
- Masthead: large EB Garamond "AI Daily" that shrinks on scroll (JS-driven)
- Spotlight: sticky left article (hero-grid 3fr 2fr) + right stack of articles with no scroll limit
- YouTube Picks: banner image centered above, then 4-column card grid (all videos are equal cards — no separate hero)
- X.com: 2-column grid
**Empty Spotlight state (0 articles):** Keep the `hero-grid` structure — show `spotlight.png` with a "No articles today" message in place of article content. Do NOT remove the image. Pattern:
```html
<div class="hero-grid">
<article>
<img class="hero-img" src="spotlight.png" alt="Spotlight">
<div class="hero-cap">
<span class="hero-tag">Weekend</span>
<h2 class="hero-title">No articles today</h2>
<p class="hero-sum">Official updates and independent news published nothing in today's content window. All sources reachable — weekend publishing cycle.</p>
<div class="hero-by">— · [DATE_EN]</div>
</div>
</article>
<div class="hero-stack"></div>
</div>
```
Chinese panel: tag = `周末`, title = `今日无文章`, summary in Chinese, date in `YYYY年M月D日` format.
**Placeholder Reference — Modernism:**
| Placeholder | Value |
|---|---|
| `{{DATE_EN}}` | e.g. `June 20, 2026` |
| `{{SPOTLIGHT_HERO_TAG_EN}}` | English category tag for hero (e.g. `Official Update · Claude Blog`) |
| `{{SPOTLIGHT_HERO_TAG_CN}}` | Chinese category tag for hero |
| `{{SPOTLIGHT_HERO_URL}}` | URL of the top Spotlight article |
| `{{SPOTLIGHT_HERO_TITLE_EN}}` | English title |
| `{{SPOTLIGHT_HERO_TITLE_CN}}` | Chinese title |
| `{{SPOTLIGHT_HERO_SUMMARY_EN}}` | 1-2 sentence English summary (italic in layout) |
| `{{SPOTLIGHT_HERO_SUMMARY_CN}}` | 1-2 sentence Chinese summary |
| `{{SPOTLIGHT_HERO_SOURCE}}` | Source name |
| `{{SPOTLIGHT_HERO_DATE_EN}}` | English date (e.g. `June 18, 2026`) |
| `{{SPOTLIGHT_HERO_DATE_CN}}` | Chinese date (e.g. `2026年6月18日`) |
| `{{SPOTLIGHT_1_TAG_EN}}` `{{SPOTLIGHT_1_TAG_CN}}` `{{SPOTLIGHT_1_URL}}` `{{SPOTLIGHT_1_TITLE_EN}}` `{{SPOTLIGHT_1_TITLE_CN}}` `{{SPOTLIGHT_1_SOURCE}}` `{{SPOTLIGHT_1_DATE_EN}}` `{{SPOTLIGHT_1_DATE_CN}}` | Each additional Spotlight article (expand REPEAT block; no summary field) |
| `{{YT_COUNT}}` | number of YouTube videos |
| `{{YT_1_TAG_EN}}` `{{YT_1_TAG_CN}}` `{{YT_1_URL}}` `{{YT_1_TITLE_EN}}` `{{YT_1_TITLE_CN}}` `{{YT_1_SUMMARY_EN}}` `{{YT_1_SUMMARY_CN}}` `{{YT_1_SOURCE}}` `{{YT_1_DATE_EN}}` `{{YT_1_DATE_CN}}` | Each YouTube video card (expand REPEAT block; all videos including the first are cards) |
| `{{X_1_HANDLE}}` | e.g. `@swyx` |
| `{{X_1_DATE_EN}}` | English date (e.g. `Jun 19`) |
| `{{X_1_DATE_CN}}` | Chinese date (e.g. `6月19日`) |
| `{{X_1_BODY_EN}}` | English post text |
| `{{X_1_BODY_CN}}` | Chinese translation |
| `{{X_1_LIKES}}` | e.g. `12,209` |
| `{{X_1_URL}}` | Post URL |
| `{{SPOTLIGHT_COUNT}}` | Total Spotlight articles |
| `{{YT_COUNT}}` | Total YouTube videos |
| `{{X_COUNT}}` | Total X.com posts |
**Content selection rules (both styles):**
- Spotlight hero: pick the single most important article (Official Updates first, then top news)
- Rationalist YouTube hero: pick the most relevant or highest-quality video; remaining go to cards
- Modernism YouTube: no hero — all long-form videos go into the card grid; exclude Shorts
- X.com posts: include all posts sorted by likes descending
- Both EN and CN panels share the same URLs and source names — only titles, summaries, tags, post bodies, and date formats differ by language
---
## Output Format (Markdown file — two separate language sections)
The file has two self-contained halves. Do NOT interleave Chinese and English line by line. A reader should be able to read one section start to finish in their language.
```
# AI Daily Brief — YYYY-MM-DD · 北京时间 / Beijing Time
---
# 中文版
## 官方动态 (N)
- **[中文标题]** — [来源] · [日期]
[1句中文摘要]
URL
## 独立新闻 (N)
- **[中文标题]** — [来源] · [日期]
[1句中文摘要]
URL
## YouTube 精选 (N)
- **[中文标题]** — [频道] · [日期]
[1句中文摘要]
URL
注意:中文版的官方动态、独立新闻、YouTube精选,标题必须是中文翻译,不保留英文原标题;链接只在摘要下方单独一行显示,标题不加超链接。
## X.com 热议 (N,按互动量排序)
- **[@username]** · ❤ [likes] · [日期]
[推文内容的中文翻译,限100字以内]
[URL]
---
*共收录:官方动态N条 · 独立新闻N条 · YouTube N条 · X.com N条*
---
# English Version
## Official Updates (N)
- **[Title]** — [Source] · [Date]
[1-sentence English summary]
URL
## Independent News (N)
- **[Title]** — [Source] · [Date]
[1-sentence English summary]
URL
## YouTube Picks (N)
- **[Title]** — [Channel] · [Date]
[1-sentence English summary] · [URL]
## X.com Top Posts (N, sorted by engagement)
- **[@username]** · ❤ [likes] · [Date]
[Original post content, truncated to 100 chars]
[URL]
---
*Total: Official N · News N · YouTube N · X.com N*
```
---
## Security Note
Your X.com credentials are personal — never commit `x-creds.json` to any repo. The `.gitignore` in this repo already excludes it.