FLUX.1 schnell requires width and height to be divisible by 8.
The default OG image size 1200×630 has 630 % 8 = 6 remainder,
causing HTTP 400 on every image generation call.
Changes:
- Pipeline default height 630 → 632 (nearest 8-aligned value)
- Provider now rounds any dimension to nearest multiple of 8 as
a defensive guard before sending to the API
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sanitize_field() fallthrough for JSON fields starting with '['
called sanitize_text_field() which escaped quotes and brackets,
breaking json_decode() on read. This caused subreddits, topic
exclusions, and enabled sources to always read back as empty arrays.
Now properly decodes, sanitizes each item, and re-encodes as clean
JSON for all three input paths: PHP array, JSON string, or CSV.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The $encrypted array was missing prautoblogger_cloudflare_ai_token,
so saving through the admin UI stored plaintext while the read path
called decrypt() — which would fail on non-encrypted data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move system prompt, user prompt, and performance context builders into
PRAutoBlogger_Analysis_Prompts. Analyzer retains orchestration, data
fetch, parsing, and storage. Backward-compatible private proxies kept.
Both files well under 300-line limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract schedule, publishing, analytics, and images field definitions
into PRAutoBlogger_Settings_Fields_Extended. Core class retains sections
and API/Models/Content/Sources fields, merging extended fields via
get_fields(). Both files well under 300-line limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Taxonomy assignment, generation log linking, LLM content sanitization,
default author resolution, and image pipeline attachment moved to
PRAutoBlogger_Post_Assembler as static helpers. Publisher retains
create_post orchestration, publish/draft flow, and meta building.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cron handlers (daily generation, metrics collection), AJAX handlers
(generate now, test connection), atomic generation lock, and model
registry access moved to PRAutoBlogger_Executor. Main orchestrator
retains hook registration, DB migrations, and cron schedule filter.
Backward-compatible proxy methods on PRAutoBlogger.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The custom autoloader converts PRAutoBlogger_OpenRouter_Model_Registry
to class-open-router-model-registry.php (hyphen between Open and Router),
but the files were named class-openrouter-model-registry.php. This caused
a fatal "Class not found" error on every uncached page load.
Renames:
- class-openrouter-model-registry.php → class-open-router-model-registry.php
- class-openrouter-model-normalizer.php → class-open-router-model-normalizer.php
Updates @see references in interface, ARCHITECTURE.md, and CHANGELOG.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move is_rss_available(), is_json_available(), get_rate_limit_status(),
and api_get() to new PRAutoBlogger_Reddit_Availability class.
Brings client from 371 to 267 lines (under 300-line cap).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extracted credential validation, request building, response parsing, and
configuration helpers from the 464-line OpenRouter provider into separate,
focused classes. This follows the pattern established by the Cloudflare
image provider and brings all files under the project's 300-line limit.
Files created:
- class-open-router-validator.php (152 lines): Handles credential validation
with HTTP probes and diagnostic messaging. Delegates from the provider.
- class-open-router-config.php (84 lines): Centralizes admin config resolution
for API base URL and cache TTL settings. Used by both provider and validator.
- class-open-router-request-builder.php (79 lines): Encapsulates request header
building and cURL auth filter registration for hosting environments that
strip Authorization headers.
- class-open-router-response-parser.php (87 lines): Handles response parsing,
error classification, and retry-decision logic.
- Tests: OpenRouterValidatorTest.php with comprehensive credential checks.
Provider shrinks from 464 to 278 lines. All classes are under 300 lines.
Dependencies remain clean: validator and config are independent, request-builder
and response-parser have no circular imports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract parse_atom_feed() and RSS-specific XML parsing logic into a new
PRAutoBlogger_Reddit_RSS_Parser class. This parser is a self-contained data
transformation (Atom XML → normalized post array) with minimal dependencies.
File sizes after split:
- class-reddit-json-client.php: 371 lines (was 468)
- class-reddit-rss-parser.php: 137 lines (new)
Both files now under the 300-line cap. The JSON client retains post/comment
fetching, availability checks, and HTTP infrastructure, while the parser
handles the XML-to-array transformation in isolation.
No callers need updating — PRAutoBlogger_Reddit_Provider uses
fetch_posts() which internally delegates to the parser.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Extract read-only reporting methods from PRAutoBlogger_Cost_Tracker into a new
PRAutoBlogger_Cost_Reporter class. This reduces Cost_Tracker from 313 to 206
lines and Reporter to 141 lines, both under the 300-line cap.
Changed files:
- Create includes/core/class-cost-reporter.php with reporting methods:
* get_monthly_spend(): float
* get_daily_spend(int $days = 30): array
* get_spend_by_stage(string $start_date, string $end_date): array
* get_budget_utilization(): float
- Update includes/core/class-cost-tracker.php:
* Remove reporting methods
* Update is_budget_exceeded() and would_exceed_budget() to use Cost_Reporter
* Keep get_current_run_cost() (tracks current pipeline run only)
* Add @see cross-reference to Cost_Reporter
- Update templates and admin classes to instantiate Cost_Reporter:
* templates/admin/metrics-page.php
* templates/admin/settings-page.php
* includes/admin/class-admin-notices.php
* includes/admin/class-dashboard-widget.php
- Split tests/unit/Core/CostTrackerTest.php:
* Create tests/unit/Core/CostReporterTest.php for reporter methods
* Keep run ID and budget enforcement tests in CostTrackerTest
Autoloader automatically discovers PRAutoBlogger_Cost_Reporter class.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap cost_usd accumulation in is_wp_error() guard so WP_Error
returns from generate_image_a/b don't crash on array access
- Add PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX to test bootstrap
- Add is_wp_error, sanitize_title, esc_html__ mocks to ImagePipelineTest
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Union types (array|WP_Error, int|WP_Error, string|WP_Error) require
PHP 8.0+. Removed return type declarations while keeping @return
docblocks intact for IDE/static analysis support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `would_exceed_budget(float)` to pre-check budget before API calls
- Add `log_image_generation()` for image pipeline cost logging
- Guard Logger::log() against null $wpdb in unit test environments
- Both methods called by image pipeline (commit 1b) but were missing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Completes the image + A/B experiment workstream by integrating
image generation into the post publishing pipeline.
New classes:
- class-image-pipeline.php: Orchestrates A/B generation (article-driven
Image A → featured; source-driven Image B → post meta). Each image
independently fallible; article publishes even if both fail.
- class-image-prompt-builder.php: Synthesizes visual prompts from article
title+content and Reddit source data. Appends CEO-locked 90s infomercial
style suffix. Keeps prompts concise for FLUX.1 quality.
- class-image-media-sideloader.php: Downloads image bytes, creates temp
file, sideloads via media_handle_sideload(), sets alt text and metadata,
cleans up temp files.
Changes to existing files:
- Publisher.attach_generated_images(): Runs image pipeline post-creation,
sets _thumbnail_id (Image A) and _prautoblogger_image_b_id (Image B).
Errors logged but don't block post publication.
- Settings: prautoblogger_image_enabled (toggle, default off).
Tests:
- ImagePipelineTest.php: Orchestration, cost tracking, graceful failures.
- ImagePromptBuilderTest.php: Prompt generation, length limits.
Docs:
- ARCHITECTURE.md: New step 6b in data flow, new files in file tree.
- CHANGELOG.md: Commit 1a/1b breakdown.
Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Foundation for the smart model picker. Fetches, normalizes, and caches
the OpenRouter model list (/api/v1/models — free, unauthenticated).
Daily refresh via the existing prautoblogger_daily_generation cron with
12h idempotency. 3-retry exponential backoff on transient failures;
serves last-good option payload when stale-and-fetch-fails.
New files:
- interface-model-registry.php — Phase 3-aware contract (94 lines)
- class-openrouter-model-registry.php — fetch + cache + query (226 lines)
- class-openrouter-model-normalizer.php — raw → standardized (114 lines)
Zero-coupling: no PRAUTOBLOGGER_* constants inside the service classes.
Config injected via constructor. Phase 2 lift into a shared Composer
package requires only namespace rename, zero internal edits.
Cost impact: $0/month (free endpoint, no LLM tokens).
Agent-Session: cowork-2026-04-16-model-picker-commit1
Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
Implements image-provider interface + Cloudflare Workers AI adapter with pricing and validator. Independent QA verdict: APPROVE on 52fd11e (qa-reviews/prautoblogger/2026-04-15-52fd11e2ce97dd9e659df5cc87ec1382a668bec1.md). All 4 CTO blockers resolved (PHPUnit retry mocks, XSS hardening on error strings, PHPCS on new files, 300-line cap via support-class extraction). PHPCS backlog (586 errors across 38 pre-existing files) remains on continue-on-error; Workstream C material.
Adds two WP options: prautoblogger_ai_gateway_base_url (routes OpenRouter calls through a Cloudflare AI Gateway when set to an https:// URL; empty = direct, the default) and prautoblogger_ai_gateway_cache_ttl (seconds for cf-aig-cache-ttl response caching; 0 = off).
The gateway is a transparent OpenRouter-compatible proxy, so no consuming class, no provider abstraction, and no response-parsing path changes. The http_api_curl belt-and-suspenders Authorization filter is rescoped to the configured upstream host so it still runs under gateway.ai.cloudflare.com.
Rationale: leverages existing Cloudflare infrastructure to satisfy the CTO cost-control rules (caching, cost tracking, rate limiting, fallback) without building equivalent middleware in-plugin. Fully opt-in and reversible from the admin UI.
Docs: ARCHITECTURE.md options + integrations tables updated; key decision #15 added. Uninstall is unaffected (wildcard prautoblogger_% delete).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WordPress's get_the_title() returns HTML-encoded entities (e.g., ’
for smart quotes). Since the React component renders via createElement
(textContent, not innerHTML), these entities displayed as raw text.
Fix applied at two levels:
- PHP REST endpoint: html_entity_decode() before sending JSON response
- JS PostCard component: decodeEntities() safety net for any residual
encoded entities from other sources
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause of the 401 was a corrupted API key: the WordPress auth
salt changed after the key was encrypted, so decryption produced
garbage instead of sk-or-... OpenRouter interprets the garbled
Bearer token as "Missing Authentication header."
Adds the same sk-or- prefix check to the credential validation
method so users see a clear "re-enter your key" message instead
of a cryptic 401 error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hostinger's hosting environment appears to strip the Authorization
header from outgoing wp_remote_post requests before they reach the
cURL transport. This caused OpenRouter to respond with HTTP 401
"Missing Authentication header" even though the header was set in
the wp_remote_post args array.
Fixes:
- Add http_api_curl filter (priority 99) that re-sets the
Authorization header directly on the cURL handle, bypassing any
intermediate header stripping. Scoped to openrouter.ai URLs only.
- Applied to all three OpenRouter call sites: send_chat_completion,
validate_credentials_detailed, and get_available_models.
- Filter is cleaned up in a finally block to avoid leaking.
Also:
- Add pre-flight key format validation (must start with "sk-or-")
to catch corrupted decryption early with a clear error message.
- Fix retry condition: was `>= 429` (incorrectly retried 430-499),
now `=== 429` (only retry rate-limit).
- Add detailed diagnostic logging on 401/403 auth failures.
- Add .env.credentials to .gitignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address CTO audit findings for PRAutoBlogger:
- CI pipeline now runs PHPCS (WordPress standards) and PHPUnit before deploy
- Removed TikTok/Instagram/YouTube stub providers (dead code, not registered)
- Rewrote PublisherTest with real behavioral assertions (post_status, metadata,
taxonomy, log linking, hooks) instead of method-existence checks
- Added cross-system LLM budget coordination docs to ARCHITECTURE.md
- Bumped version to 0.2.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LLMs sometimes ignore format instructions and wrap content in markdown
code fences or full HTML document structures. Added sanitize_llm_content()
to the publisher that strips these before wp_kses_post() runs, preventing
raw ```html blocks and <html><head><body> wrappers from reaching post_content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PullPush.io index was frequently stale/unavailable. Reddit RSS/Atom feeds
work reliably from datacenter IPs (Hostinger) where .json gets 403.
- RSS is now the primary source for post fetching
- .json endpoints kept as fallback for posts and sole source for comments
- Deleted class-pull-push-client.php
- Updated admin UI, test connection handler, docs, and ARCHITECTURE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite encryption with "enc:" prefix to permanently eliminate
the double-encryption bug on settings save
- Add one-time migration to clear legacy non-prefixed encrypted values
- Rewrite content analyzer prompts to request N distinct diverse topics
across 5 categories (question, complaint, comparison, news, guide)
- Pass target_count*2 from pipeline to analyzer for headroom
- Add skip_dedup flag so manual Generate Now always produces content
- Generate Now button now sends force=1 to clear stale locks
- Rename OpenRouter provider files to PSR-4 convention (hyphenated)
- Switch default models to google/gemini-2.5-flash-lite
- Clean up reddit_debug log channel to standard 'reddit'
- Bump version to 0.2.1
Cost: ~$0.003/article with Gemini 2.0 Flash, ~34s pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove Reddit OAuth fields (Client ID/Secret) from admin settings
- Add source status indicator showing PullPush.io + .json fallback health
- Add configurable cache TTL, time window, and posts-per-subreddit settings
- Update Test Connections to report which Reddit source is active
- Bump version 0.1.0 → 0.2.0 across plugin header, constant, and readme
- Switch deploy pipeline from zip/REST API to SSH rsync with --delete
- Add LiteSpeed cache purge step to prevent stale OPcache after deploy
- Update CHANGELOG.md and readme.txt for 0.2.0 release
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WordPress plugin that auto-researches topics from Reddit, generates
SEO-optimized articles via OpenRouter LLMs, runs them through a chief
editor agent, and publishes on a daily schedule.
Includes:
- Full generation pipeline (collect → analyze → score → generate → review → publish)
- Cost tracking and budget enforcement
- Admin UI: settings, review queue, metrics, activity log
- CI/CD pipeline (GitHub Actions → WordPress REST deploy)
- Unit test suite (PHPUnit + Brain Monkey)
- Structured logging with configurable verbosity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>