Files
tiennm99 baab3d443a refactor(skills): invoke the Node engine and centralise the command reference
Switches every call site to `node scripts/newsletter` and, in the same pass,
fixes three structural problems in the skill layer.

The seven subcommands were spelled out in five files; they are now enumerated
once in docs/newsletter/engine-commands.md, and every other mention is a link
except where a skill inlines the one or two commands it actually invokes. The
shared post mechanics move out of mt-add-url's directory into docs/newsletter/,
so no skill owns another skill's documentation and both runtimes reach it by a
repository-relative path. The Codex adapters become symlinks to their canonical
counterparts, which removes the parallel frontmatter that could drift.

Two skills are renamed for a uniform mt-<verb>-<object> scheme: mt-add-post
becomes mt-add-article, since it adds an article to a post, and mt-webfetch
becomes mt-fetch-url. The other four already conformed. Note that $mt-add-post
and mt-webfetch no longer resolve; no alias directories are left behind, because
two names for one skill is the duplication this change removes.

Trigger wording in the frontmatter descriptions is left alone apart from the
renamed tokens and the corrected fetch-tier summary: those strings are how the
runtimes decide whether to invoke a skill.

Setup documentation now states the npm ci prerequisite. The engine has
dependencies, so unlike its predecessor it does not run on a bare clone.
2026-09-18 16:24:34 +07:00

98 lines
6.5 KiB
Markdown

---
name: mt-add-image
description: 'Image handler for the Hugo blog newsletter. Adds an image URL to the target newsletter post''s Bonus → Images with a detected label. For Substack-hosted images it finds the source post (ByteByteGo and other configured publications) via RSS then sitemap, using the figure caption or post title as the label; for other sites it is best-effort; if no label is found it asks you to type one. Normally invoked by the mt-add-url meta skill after classification, but can be used directly for a known image URL.'
---
## Overview
`mt-add-image` is the **image handler**: given an image URL, it resolves a human-readable **label** and inserts `![label](image_url)` into the target newsletter post's **Bonus → Images** (today's post unless the user pinned another one). It does not classify/route — that is `mt-add-url`'s job.
Shared scripts: `scripts/newsletter/`. Shared procedure: `docs/newsletter/post-mechanics.md` — **follow it** for post find/create, Bonus insertion, and language rules.
**Label priority:** figure caption → source post title → your typed input.
**Label language:** Preserve the selected label's original source-language wording, capitalization, and punctuation. Do not translate or localize it unless the user explicitly asks. Preserve user-typed labels exactly as entered.
## Input
A clean image URL (passed by `mt-add-url`, or given directly).
## Workflow
### 1. Detect source
```bash
node scripts/newsletter detect-image-source "<url>"
```
→ `{ original_url, clean_url, isSubstack, uuid?, innerUrl? }`.
When invoked **directly** (not via `mt-add-url`), first run the router to get accessibility + duplicate status and skip accordingly:
```bash
node scripts/newsletter add-url "<url>" # expect route:image; skip if duplicate/!accessible
```
(When dispatched by `mt-add-url`, that check already ran — don't repeat it.)
### 2a. Substack image (`isSubstack: true` with `uuid`)
**First check this session.** If an earlier image in this session already resolved to a source post, check whether this image belongs to the same post before crawling again — a batch of images pasted together usually comes from one post. Reuse that post's `candidates` list directly; the deep crawl is slow and re-running it per image wastes minutes.
Otherwise, find the source post:
```bash
node scripts/newsletter find-substack-post --uuid <uuid>
```
- `found: false` → retry with the deeper sitemap crawl. The quick RSS pass only covers the newest posts, so a miss is the normal result for anything older than the current feed window — go straight to `--deep` rather than treating the miss as a dead end. It is slower (fetches posts ~3 months back, capped at 40 fetches total across all publications), so warn the user it may take a while:
```bash
node scripts/newsletter find-substack-post --uuid <uuid> --deep
```
On a miss the result reports `scanned` (posts fetched), `budget` (the 40-fetch cap), and `cutoff` (oldest date looked at) — mention how far back it looked.
- `found: false` after `--deep` → no source post; go to step 3 (ask) and/or step 4 (add publication).
**When `found: true` — pick the label (confirm-from-candidates):**
ByteByteGo doesn't attach captions to images (verified: empty `alt`, no `<figcaption>`), and image order can't be mapped to titles automatically because sponsor/video items interleave. So the script returns `candidates` — the newsletter post's topic titles from the post's TOC. Pick the label this way:
1. If `caption` is non-empty (some publications do caption images) → propose it as the default.
2. Otherwise present `candidates` to the user via `AskUserQuestion` (the correct title is in this list):
- "Image is from **[postTitle]** (`postUrl`). Which title matches it?"
- Offer the candidates. If there are more than 4, show them as a numbered list in text and ask the user to pick a number (AskUserQuestion allows max 4 options).
- Always include a "Type my own" escape.
3. Label = the user's pick (or the typed value). Preserve its original language, wording, capitalization, and punctuation; do not translate unless the user explicitly asks.
**Presenting candidates — rules that prevent mis-labelling:**
- **Dedupe first.** The list can contain the same title twice, once with literal escape sequences (e.g. `\u2019` instead of `'`). Show each title once, in its readable form.
- **Number once, never renumber.** Assign numbers on the first image from a post and keep those exact numbers for every later image from that same post. Mark already-used titles as used instead of dropping them and shifting the rest — a renumbered list makes the user's "2" ambiguous.
- **Confirm an ambiguous reply.** If a bare number could refer to two different titles (e.g. because a list was filtered), ask which one before writing. Do not guess.
### 2b. Non-Substack image (`isSubstack: false`)
Best-effort only (no reverse-image-search): if you happen to know the page that contains the image, WebFetch it and read the OpenGraph title / nearby caption. Usually there's no containing page from just an image URL → go to step 3.
### 3. Ask for a label (fallback)
If no label was detected, use `AskUserQuestion`:
- "Couldn't detect a name for this image (`<clean_url>`). What label should I use?" (free-text).
### 4. Add-publication offer (Substack miss only)
If a Substack image wasn't found and the user tells you which publication it's from, offer to append that host to `scripts/newsletter/config/substack-publications.json`, then retry step 2a. This grows coverage for next time.
### 5. Insert into Bonus → Images
Follow `docs/newsletter/post-mechanics.md` to resolve/create the target post, then add under **Images**:
```markdown
**Images:**
![label](clean_image_url)
```
Create `### Bonus` / `**Images:**` if missing.
### 6. Report
```
✅ Image added to Newsletter #[number] (Bonus → Images)
📄 content/post/YYYY/MM/DD/index.md
🏷️ label: "[label]" (source: candidate-pick | caption | user-input; post: [postTitle])
🖼️ [clean_image_url]
📊 [tally]
```
The tally comes from `post-stats` — see *Post tally* in the shared post mechanics.
## Checklist
- [ ] Source detected (substack + uuid, or non-substack)
- [ ] Label confirmed (caption if present, else user picked from candidates, else typed)
- [ ] Candidates shown deduped, with numbering kept stable across images from the same post
- [ ] Entry under Bonus → **Images** (section/subsection created if missing)
- [ ] Label preserves original source language, wording, capitalization, and punctuation unless the user requested translation