Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,60 @@ SUNO_API_ENABLED= # set to 1 to enable Suno calls (gate against acc
ELEVENLABS_API_KEY= # Eleven Music + Eleven TTS
IDEOGRAM_API_KEY= # Ideogram 3 Turbo / Default / Quality

# ───────────────────────────────────────────────────────────────────────────
# Publishing — post-publisher (v2.21.0+)
# ───────────────────────────────────────────────────────────────────────────
#
# These are APP credentials and live here. The short-lived USER tokens they
# produce live in ~/.skills-tokens.json and are written only by the auth flow:
#
# python3 -m common.runners.cli.auth --platform threads
# python3 -m common.runners.cli.auth --status
#
# Never hand-edit the token file. Setup per platform (which app to register,
# which scopes, what account type is required) is in
# post-publisher/references/oauth-setup.md.
#
# Start with Telegram: no OAuth, no app review, works in five minutes.

TELEGRAM_BOT_TOKEN= # from @BotFather; the bot must be a channel ADMIN
TELEGRAM_CHAT_ID= # @your_channel, or the numeric -100... id

THREADS_APP_ID= # developers.facebook.com → Threads API use case
THREADS_APP_SECRET=

INSTAGRAM_APP_ID= # Instagram API with Instagram Login (no FB Page needed)
INSTAGRAM_APP_SECRET= # requires a Business/Creator account + S3 below

TIKTOK_CLIENT_KEY= # developers.tiktok.com → Content Posting API
TIKTOK_CLIENT_SECRET= # direct publishing needs app audit; --draft does not

X_CLIENT_ID= # developer.x.com → OAuth 2.0, incl. offline.access
X_CLIENT_SECRET=

YOUTUBE_CLIENT_ID= # Google Cloud console → YouTube Data API v3
YOUTUBE_CLIENT_SECRET= # 1600 quota units per upload of 10000/day

LINKEDIN_CLIENT_ID= # linkedin.com/developers → Share on LinkedIn
LINKEDIN_CLIENT_SECRET= # w_member_social posts as you, not as a company page

# Optional overrides — vendors deprecate on their own schedule, so the moving
# parts are env vars rather than constants. See references/platform-limits.md.
INSTAGRAM_API_VERSION= # default v21.0
THREADS_API_VERSION= # default v1.0
LINKEDIN_API_VERSION= # default 202401; rejected once ~a year old
TIKTOK_PRIVACY_LEVEL= # default PUBLIC_TO_EVERYONE
YOUTUBE_PRIVACY= # default public; --draft forces private regardless
SKILLS_OAUTH_PORT= # default 8723 — the loopback redirect listener

# ───────────────────────────────────────────────────────────────────────────
# Storage (optional — local FS used if all unset)
# ───────────────────────────────────────────────────────────────────────────
#
# REQUIRED for Instagram, for Threads with media, and for TikTok photo posts:
# those platforms fetch media from a URL and never accept uploaded bytes.
# Files are staged as private objects and handed over as presigned links that
# expire in an hour — the bucket is never made public.
# Set these to ALSO upload every generated asset to your bucket. Local copy
# always written first. URL printed alongside the local path.
S3_BUCKET= # bucket name
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ __pycache__/
generated/
.runners-venv/

# post-publisher: OAuth tokens for the user's social accounts. Belongs in the
# home directory, but a stray copy inside the repo would be a live credential
# for someone's Instagram — ignore it wherever it lands.
.skills-tokens.json
posted.json

# proposal-maker: client brand profiles stay local. A saved profile carries a
# named client's tokens, logo and the authored structure of their real offer —
# publishing that is their call, not ours. The repo ships only the author's own
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@ Releases are cut manually. Commit messages use [Conventional Commits](https://ww

## [Unreleased]

### Added — new skills

- **`post-publisher`** (orchestrator). Publishes finished assets to Instagram, Threads, TikTok, X, YouTube, Telegram and LinkedIn through the official APIs. Composes with `carousel-builder` / `reel-builder` — it takes their output directory, reads `captions.md`, and sends it.

### Added — the collection can finally publish what it makes

Every skill here stopped at `./generated/`. Three separate places said so on purpose: `carousel-builder/SKILL.md` ("output is files you upload via the platform's UI / API"), `reel-builder/SKILL.md`, and `docs/walkthroughs/research-to-carousel-reel.md` ("that's a deliberate boundary — each platform's API has different OAuth flows"). The boundary was defensible when it was drawn and had simply stopped being: the OAuth flows differ, but they differ in ways one adapter layer absorbs.

`common/runners/publishers/` is a sibling of `providers/`, not a subclass of it. A provider turns a prompt into bytes, costs money, and can be retried for free; a publisher takes bytes that exist and does something irreversible with them. Shoehorning the second into `generate(prompt, **kwargs) -> bytes` plus `estimate_cost()` would have inherited the wrong invariants. So there is no cost estimation in the publishing layer, and in its place there is `preflight()` — which every caller runs before `publish()`, and which is a concrete method rather than an abstract one precisely so a subclass cannot forget to call `super()` and skip the file-exists check.

**Dry-run is the default and `--yes` is what leaves it.** That inverts the convention everywhere else in this repo, where `--yes` means "skip the cost prompt". The asymmetry is the point: a wasted generation costs cents, a wasted publish costs an audience. Even with `--yes`, each platform confirms separately — approving an Instagram post is not approval to also post to X.

`posted.json` receipts sit next to the assets, keyed on (platform, content hash). Re-running the identical command is refused; editing the caption makes it new content and it goes through. The hash fingerprints media by name and size rather than bytes, because hashing a 1 GB video on every dry run would be absurd and the case worth catching is "ran the same command twice".

State matters in that check, and a first pass that ignored it broke the workflow drafts exist for: staging a container, reviewing it, then publishing is the documented path for Meta and TikTok, and treating the draft as "already done" turned the second half into a `--force`. A publication is blocked only by a previous publication; a draft is blocked by either, since re-staging something already staged or already live has no purpose.

Four platform-specific decisions worth recording:

- **TikTok defaults to the inbox, not to publishing.** Direct posting requires passing TikTok's app audit; an unaudited app has every post silently forced to SELF_ONLY — the API returns success and nobody can see the post. That is the one failure mode in the set that reports itself as a success, so `--draft` routes to `/inbox/video/init/`, which needs no audit, and preflight warns loudly on the direct path.
- **Instagram uses Instagram Login, not Facebook Login for Business.** The older route additionally requires a linked Facebook Page, which most people publishing their own content neither want nor need. Business/Creator is still mandatory — there is no API path to a personal account, and `references/browser-fallback.md` says so rather than implying one exists.
- **The browser fallback is instructions, not code.** A Playwright robot holding social-network session cookies would be a worse thing to own than the problem it solves, and selectors rot on every redesign. It drives Claude in Chrome against the user's already-logged-in browser, stores nothing, and requires explicit consent per post.
- **Meta drafts became genuinely two-step.** A staged container is only useful if you can publish it later, so `--publish-container <id>` exists. Without it `--draft` on Instagram would have been decorative.

`common/runners/tokens.py` is a second credential store rather than an extension of `keysfile.py`. App credentials are long-lived and pasted by hand; user tokens expire in hours and arrive from an OAuth flow. Merging them would have meant sixteen generation providers carrying expired social tokens in `os.environ` at every runner startup. Tokens are never loaded into the environment — they are read on demand, refreshed transparently inside a 300-second skew window, and masked everywhere they are printed. `common/runners/atomicfile.py` now holds the secure-write helper both stores use.

Also: `scripts/validate.sh` only resolved `references/*.md` links inside the same skill, so any cross-skill reference was reported broken. It now matches `../other-skill/references/*.md` too.

144 new unit tests, none of which touch the network. Five of them exist because a review pass found the bugs they now pin: an explicit `--title` silently discarded on YouTube whenever the caption was empty (operator precedence — `(title or first_line) if text else "Untitled"`); a TikTok chunk plan that declared a 20 MB chunk for a 6 MB file, which TikTok rejects; Threads permanently unrefreshable because `tokens.py` hardcoded `"instagram"` as the one platform allowed to renew without a refresh token; a one-shot OAuth listener that a browser's `/favicon.ico` request could consume instead of the callback; and a partial-alt-text warning that the documentation described and the code did not implement.

### Fixed — the npm package and the Docker image shipped a 17-skill subset

Caught while deciding whether the release was safe to cut. Tagging `v*.*.*` triggers the Docker build, and the image would have gone out without `post-publisher` — along with 24 other skills.

`Dockerfile` and `package.json` both listed the same seventeen directories: exactly the v1.x prose set, frozen since around v2.3 while twenty-five skills were added around them. `skills.json` advertised all forty-two, so `install.sh` running inside the container warned about twenty-five missing skills — inside the artifact meant to contain them. The Dockerfile's own header claimed it ships "all skill markdown", which had quietly stopped being true.

The cost was never size. These are markdown directories; the rebuilt image is 117 MB and the skills contribute almost nothing to that. The subset was drift, not a decision.

Both lists regenerated from `skills.json`, and `check-docs-consistency.sh` gained gate 7 to compare them against it. That gate is the actual fix — the lists drifted for eighteen releases precisely because nothing compared them to anything. Verified it fails by removing a skill from each and watching it name both.

### Fixed — nothing was checking the launch copy

Caught by the user asking whether the rewritten posts had been run through the skills. They had not, and two layers of blindness met — both self-inflicted.
Expand Down
60 changes: 44 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,51 @@ COPY writer/examples/ /skills/writer/examples/

# Copy remaining skills (read-only reference material — useful inside the image
# but not needed for lint.py to run).
COPY viral-text/ /skills/viral-text/
COPY prose-edit/ /skills/prose-edit/
COPY essay-write/ /skills/essay-write/
COPY style-check/ /skills/style-check/
COPY translation-sync/ /skills/translation-sync/
COPY canon-check/ /skills/canon-check/
COPY audio-mix-maker/ /skills/audio-mix-maker/
COPY avatar-maker/ /skills/avatar-maker/
COPY banner-maker/ /skills/banner-maker/
COPY bg-remover/ /skills/bg-remover/
COPY canon-check/ /skills/canon-check/
COPY carousel-builder/ /skills/carousel-builder/
COPY cold-email/ /skills/cold-email/
COPY cover-maker/ /skills/cover-maker/
COPY essay-write/ /skills/essay-write/
COPY flyer-maker/ /skills/flyer-maker/
COPY gif-maker/ /skills/gif-maker/
COPY image-prompt/ /skills/image-prompt/
COPY landing-copy/ /skills/landing-copy/
COPY logo-maker/ /skills/logo-maker/
COPY meme-card-maker/ /skills/meme-card-maker/
COPY microcopy/ /skills/microcopy/
COPY music-prompt/ /skills/music-prompt/
COPY pelevin-digression/ /skills/pelevin-digression/
COPY skills-update/ /skills/skills-update/
COPY tone-shifter/ /skills/tone-shifter/
COPY cold-email/ /skills/cold-email/
COPY image-prompt/ /skills/image-prompt/
COPY video-prompt/ /skills/video-prompt/
COPY microcopy/ /skills/microcopy/
COPY release-notes/ /skills/release-notes/
COPY rfc-writer/ /skills/rfc-writer/
COPY landing-copy/ /skills/landing-copy/
COPY common/ /skills/common/
COPY post-publisher/ /skills/post-publisher/
COPY proposal-maker/ /skills/proposal-maker/
COPY prose-edit/ /skills/prose-edit/
COPY quote-card-maker/ /skills/quote-card-maker/
COPY reel-builder/ /skills/reel-builder/
COPY release-notes/ /skills/release-notes/
COPY research-brief/ /skills/research-brief/
COPY rfc-writer/ /skills/rfc-writer/
COPY skills-keys/ /skills/skills-keys/
COPY skills-styles/ /skills/skills-styles/
COPY skills-update/ /skills/skills-update/
COPY style-check/ /skills/style-check/
COPY style-suggest/ /skills/style-suggest/
COPY style-transfer/ /skills/style-transfer/
COPY subtitle-burner/ /skills/subtitle-burner/
COPY thumbnail-maker/ /skills/thumbnail-maker/
COPY tone-shifter/ /skills/tone-shifter/
COPY transcribe-maker/ /skills/transcribe-maker/
COPY translation-sync/ /skills/translation-sync/
COPY upscaler/ /skills/upscaler/
COPY video-prompt/ /skills/video-prompt/
COPY viral-text/ /skills/viral-text/
COPY voiceover-maker/ /skills/voiceover-maker/

# Shared runner layer + style libraries. Not a skill, so not in skills.json,
# and therefore not in the generated list above — it needs its own line.
COPY common/ /skills/common/

# Helper scripts the entrypoint dispatches to
COPY scripts/coverage.py /skills/scripts/coverage.py
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

AI content toolkit for [Claude Code](https://docs.claude.com/en/docs/claude-code/skills) — prose editing without LLM-shaped output, AI media generation (image / video / music) with optional in-line execution, and end-to-end content orchestrators (research → carousel / reel). Russian-first, English-capable.

**Forty-one skills** across five layers: one base + twenty-one wrappers + three linters + thirteen orchestrators + three meta. Plain markdown, MIT-licensed, no required external deps (ffmpeg is optional for reel stitching + subtitle burning + GIF conversion + audio mixing).
**Forty-two skills** across five layers: one base + twenty-one wrappers + three linters + fourteen orchestrators + three meta. Plain markdown, MIT-licensed, no required external deps (ffmpeg is optional for reel stitching + subtitle burning + GIF conversion + audio mixing).

---

Expand Down Expand Up @@ -147,6 +147,7 @@ The shared LLM prompt chains behind the visual skills: [image chain](common/visu
| [`transcribe-maker`](transcribe-maker/) | wrapper | en/ru | Transcribe audio / video to SRT / WebVTT / JSON / plain text via OpenAI Whisper. Auto-detects language or accepts ISO-639-1 hint. ~$0.006/min. Closes the loop with subtitle-burner — produce captions from video, then burn them in. 25 MB file limit. |
| [`style-suggest`](style-suggest/) | orchestrator | en/ru | Visual-style generator — turn a text description and/or reference image into a fully-structured style entry that drops into common/visual-prompt-library/styles/. Adapted from figma's StyleSuggestAgent: takes free-form input, checks the existing catalog for duplicates (similarity ≥ 0.72), otherwise produces a new entry with all v2.15.0 schema fields (background / accents / elements / mood / accent_text_color / typography / composition_signature / when_to_use). The new style is immediately usable by every visual-output skill via the shared chain. |
| [`proposal-maker`](proposal-maker/) | orchestrator | en/ru | Turn a raw, telegram-style commercial offer into a self-contained, brand-faithful HTML proposal whose style is copied from a website. Default flow is LLM-authored: a Python step builds a brand kit (site screenshot + logo + accent/font tokens + per-item catalogue photos + BRIEF.md), then the orchestrator authors bespoke HTML mirroring the brand. Items missing a photo get an on-brand generated image; prices and links stay exact; prints to a Ghostscript-compressed PDF. A --quick mode renders a deterministic theme (editorial/invoice/dark). Output: ./generated/proposal/<slug>/. |
| [`post-publisher`](post-publisher/) | orchestrator | en/ru | Publish finished assets to Instagram / Threads / TikTok / X / YouTube / Telegram / LinkedIn through the official APIs. Takes a carousel-builder or reel-builder output directory, reads captions.md, runs a per-platform preflight (char limits, media count, file size, posting caps), previews, confirms per platform, then publishes and writes a posted.json receipt so the same content cannot go out twice. Dry-run by default; --yes leaves it; --draft where the platform has drafts. OAuth tokens live in ~/.skills-tokens.json with automatic refresh. |

<!-- END skills-table -->

Expand Down Expand Up @@ -263,7 +264,7 @@ skills/
├── CODE_OF_CONDUCT.md # short; scales up if the project does
├── SECURITY.md # how to report a vulnerability
│
├── <skill-name>/ # 41 skills, one folder each
├── <skill-name>/ # 42 skills, one folder each
│
├── common/
│ ├── references/ # shared anti-pattern catalogues (hype words, preambles, …)
Expand Down
4 changes: 2 additions & 2 deletions carousel-builder/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This skill does NOT:
- Compose the slides into a single tall image — Instagram / LinkedIn handle multi-image posts natively.
- Add text overlays via a design tool — text either gets generated INSIDE the image (gpt-image-2 / Ideogram / Imagen) via `--text-mode embedded`, or is left to the user's editor (`--text-mode overlay`).
- Generate animated carousels (those are reels — use `reel-builder`).
- Post to platforms — output is files you upload via the platform's UI / API.
- Post to platforms — that is `post-publisher`, which takes this skill's output directory as its input.
</objective>

## ROLE
Expand Down Expand Up @@ -249,4 +249,4 @@ If the user gives a topic but no platform: default to `instagram`, ask once if L

Defaults: `--slides 8 --platform instagram --aspect portrait --text-mode embedded --model auto`. Without `--execute`, returns prompts + caption text for manual paste. With `--execute`, generates slides.

This skill is downstream of `research-brief` (consumes the brief) and upstream of any manual post — final step is uploading the slides to the platform's UI.
This skill is downstream of `research-brief` (consumes the brief) and upstream of `post-publisher`, which reads this skill's output directory — slides plus `captions.md` — and sends it to Instagram / Threads / TikTok / LinkedIn. When the user says "и выложи" after a deck is generated, hand the output directory to `post-publisher`; it dry-runs by default, so nothing goes out without a confirmation.
Loading
Loading