Download the PHP package giorgigrdzelidze/laravel-stringer without Composer
On this page you can find all versions of the php package giorgigrdzelidze/laravel-stringer. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download giorgigrdzelidze/laravel-stringer
More information about giorgigrdzelidze/laravel-stringer
Files in giorgigrdzelidze/laravel-stringer
Package laravel-stringer
Short Description Telegram-driven LLM blog drafter for Laravel. Generates per-locale drafts for human review; auto-publish opt-in per topic.
License MIT
Homepage https://github.com/GiorgiGrdzelidze/laravel-stringer
Informations about the package laravel-stringer
π§΅ laravel-stringer
A Telegram-driven LLM blog drafter for Laravel β pairs a chat-first interface with a configurable AI pipeline to produce per-locale drafts with auto-generated cover images for human review. Never auto-publishes implicitly. Every part of the pipeline β prompts, field schema, voice card, image style β is editable in Filament without a deploy.
β‘ Quickstart (5 minutes)
Get from zero to your first draft in five steps:
Open /admin/blog-topic-resource/blog-topics β your topic flips from Queued β Drafting β Drafted and a new article appears at /admin/articles ready for review.
π¬ Want the chat-first experience? Add
STRINGER_TELEGRAM_BOT_TOKEN+STRINGER_TELEGRAM_WEBHOOK_SECRET, set your bot's webhook to/webhooks/telegram/{secret}, and message/generate <topic>.
β¨ What it does
stringer is a small back office for blog content:
- π₯ An operator (or scheduler) creates a topic β a hint, a tag, a stray idea.
- π οΈ A queued job composes an LLM prompt from a configurable field schema (
StringerContentField) and a configurable prompt template (StringerPrompt), both editable in Filament. - π€ The LLM returns a JSON object keyed by field name. Translatable fields come back per-locale; non-translatable values pass through as-is.
- π§Ή A deterministic AI-tell sanitizer strips giveaway phrases ("furthermore", "in conclusion", "it's worth noting") before the draft is written.
- π The package hands a
LocalizedDraftto the host'sContentTargetadapter, which writes the draft asstatus='draft'for human review. - πΌοΈ A chained
GenerateCoverImageJobproduces a cover image via the configured image driver (gemini-image/ Imagen 3 / Unsplash / DALL-E 3 / FLUX / Picsum), attaches it to the article, and Spatie Media Library auto-derivesog(1200Γ630),twitter(1200Γ675), andthumb(400Γ225) crops. - π When the operator trusts the pipeline for a recurring topic, flipping
auto_publish+target_statuson the topic publishes it directly on the next generate.
πΌοΈ See it in action
Filament β Topics queue
Inspect status, click Generate Now, or spike rejected hints.
Filament β Articles dashboard
Every draft lands with per-locale columns (EN Β· KA Β· RU), a cover thumbnail, and a category β never auto-published. The operator reviews each one before flipping the status to published.
Telegram β Draft pipeline
| β Draft ready | β Error surfaced |
|---|---|
| Cover image + bold title + excerpt + stats + admin link land as a photo card with the host badge. One notification per drafted article. | Every upstream failure (LLM quota, image-API paid-plan, JSON parse error, slug collision) surfaces in chat with a humanized reason. Never have to grep logs to know what broke. |
Telegram β Menu navigation
1. /start opens the root menu |
2. Drill into a category | 3. Settings & language |
|---|---|---|
Reply-keyboard menu reached via /start or /menu. Five top-level paths: Generate, Topics, Categories, Settings, Help. |
Drill-down with persistent back buttons; per-chat state means you can leave and come back without losing your place. | Per-chat language preference (en / ka / ru), allowed-chat-ID list, and other operator knobs β all editable from inside Telegram. |
Filament β Operator surfaces
| SEO tab β multi-channel | Settings page |
|---|---|
meta_title, meta_description, og_title, og_description, twitter_title, twitter_description β all filled by the LLM per locale, all editable. |
Voice card, body word cap, tag count, cron, timezone, allowed chat IDs β all editable in admin, no deploy. |
| DB-editable prompts | Data-driven field schema |
|---|---|
Three seeded templates: draft, translate, cover_image. Edit the prompt content directly to retune voice, style, or structure β the next generation uses the new template, no deploy. |
12 baseline fields shipped (title, excerpt, slug, body, meta_*, og_*, twitter_*, tags, category). Add your own, mark required, set per-locale constraints β the LLM is told to fill exactly the active schema. |
ποΈ Architecture
Every arrow is mockable in tests. Every box marked "host adapter" is something the host implements; everything else ships in the package.
π― Quality defaults
The seeded defaults are built around what experienced engineers actually want to read:
| Surface | Default |
|---|---|
| Draft prompt | 4 kB template with explicit voice rules, an anti-AI-tell phrase list, code-must-compile rule, heading conventions, and a worked few-shot example output |
| Translate prompt | Preserves code, function names, library names, brand names, CLI flags, file paths, and HTML/JSON keys verbatim |
| Voice card | Senior-engineer voice β first-person where the topic is experience, third-person where it's reference. Plainspoken, opinionated, no marketing language |
| Title field | Max 70 chars, sentence-case English, anti-clickbait, anti-"Ultimate / Complete / Definitive" word list |
| Excerpt field | Max 200 chars, ~25-35 words, active voice, stands alone as meta description |
| Body field | Max 1500 words, opens with a concrete scenario (never a definition), H2/H3 only, working code samples required |
| Tags field | Multilingual β LLM returns {"en":[...], "ka":[...], "ru":[...]} with parallel index ordering. Tech / brand names not translated |
| Post-processing | AiTellSanitizer strips ~30 giveaway phrases (Furthermore,, Moreover,, In conclusion,, It's worth noting,, In today's fast-paced world,, β¦) β case-insensitive, code-block-safe |
| Cover image | Default driver gemini-image (free tier on Gemini, reuses STRINGER_GEMINI_API_KEY). One master 1792Γ1024 + auto-derived og/twitter/thumb crops. Editable visual prompt + style suffix |
Every default is editable in Filament. Hosts that want a different voice, longer bodies, or different fields just change the rows β no deploy required.
π Sample output
Real output from the default prompt on gemini-flash-latest, hint "Idempotent queue jobs in Laravel: deduplication strategies, distributed locks via Redis, handling out-of-order delivery, and observability hooks."
Show generated draft
**Title** *(EN)* > Building Idempotent Queue Jobs in Laravel: Strategies and Implementation **Excerpt** *(EN)* > Ensuring consistency in Laravel background tasks through deduplication, Redis distributed locks, and handling out-of-order execution. **Meta title** *(EN)* > Implementing Idempotent Queue Jobs in Laravel **Meta description** *(EN)* > Technical guide on building idempotent Laravel queue jobs using Redis locks, deduplication, and out-of-order execution handling. **Body** *(EN, first paragraph)* > In distributed systems, queue jobs may be executed more than once due to network timeouts, worker crashes, or manual retries. Idempotency ensures that performing an operation multiple times produces the same result as a single execution. This is critical when handling financial transactions, such as those integrated via the `laravel-bog-sdk` or `laravel-rsge` packages. **Tags** > `laravel`, `backend-development`, `queue`, `idempotency`, `redis` Generated in ~24 seconds end-to-end (one LLM call for the EN draft + all locales). All seven seeded SEO fields were populated in the same call; per-locale translations for EN/KA/RU came back in the same response.π¦ Install
The package registers its own service provider via extra.laravel.providers. After migrate:
The seeders are auto-run on the first console boot if their tables are empty; the explicit db:seed is for forced re-seeds.
βοΈ Configuration
Environment variables
| Variable | Default | Purpose |
|---|---|---|
STRINGER_LLM_DRIVER |
gemini |
LLM driver: gemini, claude, openai, or groq |
STRINGER_GEMINI_API_KEY |
β | API key when driver is gemini |
STRINGER_CLAUDE_API_KEY |
β | API key when driver is claude |
STRINGER_OPENAI_API_KEY |
β | API key when driver is openai |
STRINGER_GROQ_API_KEY |
β | API key when driver is groq |
GEMINI_MODEL |
gemini-2.0-flash |
Per-driver model identifier |
CLAUDE_MODEL |
claude-sonnet-4-5 |
|
OPENAI_MODEL |
gpt-4o-mini |
|
GROQ_MODEL |
llama-3.3-70b-versatile |
|
STRINGER_LLM_HTTP_TIMEOUT |
120 |
HTTP timeout (seconds) for a single LLM round-trip. Bump if you see cURL error 28 on dense prompts |
STRINGER_TELEGRAM_ENABLED |
true |
Skips webhook route registration when false |
STRINGER_TELEGRAM_BOT_TOKEN |
β | Bot token from BotFather |
STRINGER_TELEGRAM_WEBHOOK_SECRET |
β | 16+ alphanumeric chars ([A-Za-z0-9_-]{16,}) used as the URL secret on /webhooks/telegram/{secret} |
STRINGER_TELEGRAM_ALLOWED_CHAT_IDS |
β | Comma-separated chat IDs allowed to drive the bot. Non-allowlisted chats get 200 OK with empty body |
STRINGER_AUTO_GENERATE_CRON |
0 9 * * 1 |
When the weekly auto-generate job fires |
STRINGER_AUTO_GENERATE_TZ |
Asia/Tbilisi |
Timezone for the cron above |
STRINGER_ARTICLE_MODEL |
β | FQN of the host's article model β required only if you use BlogTopic::article() |
STRINGER_SEED_DEFAULTS_ON_BOOT |
true |
Auto-run seeders if tables are empty. Set false in containers that boot many short-lived consoles |
STRINGER_TELEGRAM_ADMIN_BASE_URL |
β | Optional public-facing host used to rewrite admin links sent over Telegram. Useful in local dev where editUrl() returns http://localhost/... β set to an ngrok URL while testing; leave unset in production |
| Cover images | ||
STRINGER_IMAGE_ENABLED |
true |
Master switch β turn off to skip cover generation entirely |
STRINGER_IMAGE_DRIVER |
gemini-image |
gemini-image (default, free tier, reuses Gemini key), imagen, unsplash, dalle, flux, or picsum |
STRINGER_IMAGE_STYLE |
editorial illustration⦠| One-line style suffix appended to every visual prompt. Edit to change the look across all drivers |
STRINGER_IMAGE_MASTER_SIZE |
1792x1024 |
Master image size requested from the driver. Spatie crops down to og/twitter/thumb automatically |
STRINGER_IMAGE_REQUIRE_CONFIRMATION |
false |
If true, Telegram preview + β
/π/β keyboard before the cover is attached (deferred β flag wired, menu node not yet shipped) |
STRINGER_IMAGE_MAX_REGENERATES |
3 |
Ceiling on π regenerate taps per topic β prevents accidental runaway cost |
STRINGER_IMAGE_HTTP_TIMEOUT |
60 |
HTTP timeout (seconds) for one image API round-trip |
STRINGER_IMAGEN_MODEL |
imagen-3.0-generate-001 |
Imagen model identifier |
STRINGER_GEMINI_IMAGE_MODEL |
gemini-2.5-flash-image |
Gemini image-generation model used by the gemini-image driver (free tier on Gemini) |
STRINGER_UNSPLASH_ACCESS_KEY |
β | Access key when driver is unsplash (free path) |
STRINGER_DALLE_MODEL |
dall-e-3 |
DALL-E model identifier |
STRINGER_FAL_KEY |
β | API key when driver is flux (uses fal.ai) |
STRINGER_FLUX_MODEL |
fal-ai/flux/dev |
FLUX model identifier on fal.ai |
πΌοΈ Cover images
Every draft gets an auto-generated cover. The cover job is chained off the draft job β if the LLM succeeds, the image pipeline runs next. Failure is non-fatal: a missing cover never unwinds the article, it just logs and exits.
Pipeline
Drivers
| Driver | Cost (per image) | Reuses existing key? | Strength |
|---|---|---|---|
| Gemini-image (default) | Free tier on Gemini | β
STRINGER_GEMINI_API_KEY |
Zero new config, free baseline (gemini-2.5-flash-image) |
| Imagen 3 | ~$0.03 | β
STRINGER_GEMINI_API_KEY |
Editorial, clean β paid upgrade path from gemini-image |
| Unsplash | Free | β needs STRINGER_UNSPLASH_ACCESS_KEY |
Real photos with attribution, best for lifestyle topics |
| DALL-E 3 | $0.04 | β needs STRINGER_OPENAI_API_KEY |
Strongest baseline quality |
| FLUX dev | $0.025 | β needs STRINGER_FAL_KEY (fal.ai) |
Best open-model output |
| Picsum | Free | β no key | Placeholder images for smoke tests / CI |
Switch drivers with one env change β no code:
Master image + automatic crops
The driver produces one master image at STRINGER_IMAGE_MASTER_SIZE (default 1792x1024). Spatie Media Library then auto-generates the social crops on the host model:
Access from the host:
Visual style
The global STRINGER_IMAGE_STYLE string is appended to every visual prompt the LLM produces. Change it once and the look shifts across every driver:
The visual prompt itself is editable in Filament: open /admin/stringer-prompts, pick the cover_image row, and rewrite the template. Variables available: {{title}}, {{excerpt}}, {{style}}.
Backfill
After enabling image generation on an existing install, dispatch the cover job for every drafted topic that still has no cover:
Telemetry
All failures land in the daily log channel β never raised:
| Key | Meaning |
|---|---|
stringer.generate_cover_image_job.failed |
Driver threw (timeout, 4xx/5xx, bad response) |
stringer.generate_cover_image_job.regenerate_ceiling_hit |
Operator hit STRINGER_IMAGE_MAX_REGENERATES |
Every entry includes topic_id, driver, error, and exception class.
Disabling entirely
The draft job never dispatches the cover job β articles ship coverless. No code change needed.
π οΈ Filament admin
Install the Filament v5 admin panel and register the plugin:
Four surfaces appear under the Stringer navigation group:
π Topics (BlogTopicResource)
The queue. Each row is a BlogTopic β a hint that became, or will become, a draft. Status lifecycle: Queued β Drafting β Drafted (or Rejected / Failed).
Actions per row:
- Generate Now β dispatches
GenerateDraftJobimmediately (same as/generate {id}from Telegram) - Spike β marks the topic
Rejectedwithout generating - Edit β opens the topic record. The per-topic G1 toggles live here:
auto_publishβ whentrue, a successful draft on this topic is published immediately, not held as drafttarget_statusβ overrides the article's status on write (draft,published,reviewβ host's choice)
βοΈ Prompts (StringerPromptResource)
DB-backed prompt templates. The seeder ships two rows:
| Key | Purpose | Placeholders |
|---|---|---|
draft |
Used by DraftGenerator to ask the LLM for the full draft JSON |
{{voice}}, {{source}}, {{hint}}, {{context}}, {{categories}}, {{field_schema}} |
translate |
Used per locale when the LLM returns the primary locale only and per-locale translation calls are required | {{english_text}}, {{target_locale}} |
Edit content directly, toggle is_active to disable, or add locale-specific overrides. Falls back to the baked-in DefaultPromptBuilder constants when no DB row matches.
π§± Content Fields (StringerContentFieldResource)
The field schema β what the LLM is asked to produce on each draft. Each row defines one field on the host's content model.
| Column | What it controls |
|---|---|
name |
The key the LLM uses in its JSON output, and the property name on the host's Eloquent model |
type |
One of seven FieldType values (below) β drives how ArticleTarget dispatches the value |
locales |
Array of locale codes the LLM produces per-locale (['en','ka','ru']) β used by translatable types |
max_length |
Character cap rendered into the prompt |
max_words |
Word cap rendered into the prompt |
min / max |
Generic bounds (e.g. tags: min=3, max=5) |
instruction |
Per-field guidance shown to the LLM (e.g. "open with a concrete scenario, never a definition") |
is_required |
LLM is told it MUST produce this field |
sort_order |
Display + prompt order |
is_active |
Soft toggle without losing the row |
Field types
| Type | Behavior |
|---|---|
translatable_string |
setTranslations($name, $perLocaleValues) on host model |
translatable_markdown |
Same as above; values are markdown β host adapter typically converts to HTML at write time |
string |
Direct property assignment on host model |
markdown |
Direct property assignment; host typically converts to HTML at write time |
tag_list |
syncTags($value) on the host (Spatie tags). Supports both flat (['laravel','queue']) and per-locale ({"en":[...], "ka":[...]}) shapes |
category |
Slug resolved against the host's category model; FK set on the article |
integer |
Cast then direct property assignment |
Add a row to expand the schema; the LLM will be asked for it on the next draft. No package code changes.
βοΈ Settings (ManageStringerSettings)
Single-form page at /admin/manage-stringer-settings. Persists to the singleton stringer_settings row.
| Field | Type | Purpose |
|---|---|---|
voice_card |
text | Voice-card overlay shown to the LLM in the {{voice}} placeholder. Override the package default per host. Leave blank to fall back to the senior-engineer default. |
body_word_cap |
int | Operator-side hint for the body word budget. Visible in the form as documentation of the active target; the binding cap is read from StringerContentField.body.max_words. |
tag_count |
int | Operator-side hint for the desired tag count. The binding count is read from StringerContentField.tags.min / max. |
auto_generate_cron |
string | Overrides STRINGER_AUTO_GENERATE_CRON for the weekly auto-generate job. |
auto_generate_timezone |
string | Overrides STRINGER_AUTO_GENERATE_TZ. |
allowed_chat_ids |
array | Extra chat IDs on top of STRINGER_TELEGRAM_ALLOWED_CHAT_IDS. Currently write-only from the package's perspective; runtime overlay over the env baseline is on the roadmap. |
π‘ The Settings page is a UX anchor for operator-edited config β the canonical
.env-driven binding stays the source of truth. Runtime overlay of env values from the Settings row is on the roadmap.
π¬ Telegram
Set the bot's webhook to https://your-host.example/webhooks/telegram/{your-webhook-secret}, then talk to it:
| Trigger | Behavior |
|---|---|
/help |
List commands |
/list |
Show the 10 most recent topics with id, status, and a truncated hint |
/generate |
Show pending (queued) topics |
/generate write about X |
Enqueue a new Manual topic with the hint "write about X" |
/generate 42 |
Force-dispatch GenerateDraftJob for topic #42 |
/draft β¦ |
Alias for /generate β¦ |
/spike 42 |
Mark topic #42 as Rejected |
| any other text | Enqueued as a new Manual topic |
After a draft completes, the bot replies to the originating chat with Draft #N is ready β <admin edit URL>. Failures also notify the chat with the truncated error message.
ποΈ Scheduler
The provider auto-binds a weekly job: it picks the oldest Queued topic, or synthesizes an Auto-source topic from the host's first project / repository title when nothing's queued. Override the cadence via STRINGER_AUTO_GENERATE_CRON / STRINGER_AUTO_GENERATE_TZ β or override per-tenant via the Settings page.
π Host integration
stringer is opinionated about the pipeline and agnostic about where the drafts land. Two contracts bind the package to the host's Eloquent model:
ContentTarget β where drafts are written
ContextBuilder β what the LLM sees as reference
Bind both in a host service provider:
π‘οΈ The
ContextBuilderconstructor's type signature is the G3 architectural enforcement: type-hinting only public-content + public-taxonomy models prevents finance / admin / user records from leaking into LLM context.
π‘οΈ Guardrails
The package enforces a small set of architectural rules β these are what keep the pipeline safe to run unattended.
- G1 β Default writes drafts. Auto-publish is per-topic opt-in via
BlogTopic.auto_publish+target_status. Hosts can't accidentally publish via the package. - G2 β Never touches
schema_json. The host content model regenerates JSON-LD at render time; package writes would conflict. - G3 β No host coupling in
src/. Only theContracts/interfaces are touched by host code. Host models stay in the host app. - G4 β No public auth on the webhook. Path-secret authentication + chat-ID allowlist; never
auth:middleware. Non-allowlisted chats get200 OKempty body so Telegram doesn't retry. - G5 β Public taxonomy only in
ContextBuilder. The constructor type signature prevents finance / admin / user models from being injected. - G6 β Zero AI footprint. The sanitizer scrubs giveaway phrases the prompt couldn't suppress; commit messages and PR bodies stay in the operator's voice.
- G7 β Single chokepoint for status writes. All
BlogTopic.statusmutations go throughServices\TopicQueue. Activity log and metrics hooks attach there. - G8 β One sanctioned PromptBuilder. Only
Contracts\PromptBuilderimplementations produce strings sent to the LLM. No host code constructs prompts ad-hoc.
β FAQ
Can I use my own LLM provider?
Yes. Implement `Stringer\Laravel\Contracts\LlmClient` (one method, `request(string $message): string`), register it in `LlmManager`, and set `STRINGER_LLM_DRIVER=How do I add a new field that the LLM should produce?
Add a row to `StringerContentField` via Filament. Pick the right `FieldType` (e.g. `translatable_string`), write an `instruction` that tells the LLM what you want, and toggle `is_active`. On the next draft the LLM is asked for it; `ArticleTarget` writes it to the matching column on your host model. No code change required.What happens if the LLM call fails?
`GenerateDraftJob` has `$tries = 2` with `backoff = 30s`. The first attempt failing puts the topic in `Drafting`; the second failure calls `failed()` which marks it `Failed`, stores the error on `BlogTopic.last_error`, and (if Telegram is wired) DMs the requesting chat with the truncated error. No retries beyond the second attempt β the queue worker doesn't loop.How do I customize the voice?
Three layers, in order of precedence: 1. **`StringerSetting.voice_card`** β set via `/admin/manage-stringer-settings`. Per-host override. 2. **`config('stringer.voice.default')`** β set via `.env`. Per-environment override. 3. **Baked-in default** β defined in `DefaultPromptBuilder::renderDraft()`. Conservative senior-engineer voice. You can also edit the seeded `draft` prompt row in `/admin/stringer-prompt-resource/stringer-prompts` if you want to restructure the whole prompt template.What does running this cost?
Per-draft estimates (one prompt + multi-locale output in one LLM call): | Provider | Model | Approx cost / draft | |----------|-------|--------------------:| | Gemini | `gemini-flash-latest` (free tier) | $0 | | Gemini | `gemini-2.5-flash` | ~$0.002 | | OpenAI | `gpt-4o-mini` | ~$0.005 | | Anthropic | `claude-sonnet-4-5` | ~$0.03 | | Groq | `llama-3.3-70b-versatile` (free tier) | $0 | Numbers assume a ~1500-word body + 8 SEO fields + 3 locales. Translation passes only happen if the LLM doesn't already return per-locale output in the first call (Gemini and Claude usually do; older models may not).Does it generate images?
Yes β every successful draft chains into `GenerateCoverImageJob`, which produces a master cover image via the configured driver (`gemini-image` by default, free tier; alternatives: `imagen`, `unsplash`, `dalle`, `flux`, `picsum`). Spatie Media Library auto-derives `og` / `twitter` / `thumb` crops on the host. See the [Cover images](#%EF%B8%8F-cover-images) section for the pipeline, drivers, and backfill command.Can I run it without Telegram?
Yes. Set `STRINGER_TELEGRAM_ENABLED=false` and the webhook route stops registering. Drive the queue from Filament (Generate Now action on a Topic row), from the scheduler (auto-generate weekly), or from tinker (`GenerateDraftJob::dispatch($topicId)`).How do I rotate the Telegram webhook secret?
1. Generate a new 32-char random string: `openssl rand -base64 24 | tr -d '/=+\n' | head -c 32` 2. Update `STRINGER_TELEGRAM_WEBHOOK_SECRET` in `.env` 3. Update the webhook URL with Telegram: `curl "https://api.telegram.org/botπ Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
cURL error 28: Operation timed out after 30s |
Default Guzzle timeout on dense prompts | Bump STRINGER_LLM_HTTP_TIMEOUT=180 (or higher) in .env |
HTTP 429: quota exceeded |
LLM provider rate limit | Wait the cooldown window, switch model (gemini-flash-latest vs gemini-2.5-flash), or move to a paid tier |
HTTP 503: model is overloaded |
Transient provider capacity | Retry after ~60s; this is upstream, not the package |
SQLSTATE[22003]: Out of range value for column 'requested_by_chat_id' |
Telegram supergroup IDs need 64-bit signed | Already fixed in v0.1.0 migration; if you migrated from an earlier internal build, re-run migrations |
Tags render as {"en": "..."} in the UI |
Filament option label not localized | ->getOptionLabelFromRecordUsing(fn ($t) => $t->getTranslation('name', app()->getLocale())) on the Select |
| Body shows raw markdown on the public site | Host renders body as HTML but stringer wrote markdown |
Convert in ArticleTarget::write() with Str::markdown($value) for Markdown / TranslatableMarkdown fields |
| Bot replies in the wrong language | The seeded replies are English; older internal builds shipped Georgian | Re-run composer update to pull the v0.1.0 strings |
Queue worker runs but topic stays Drafting |
Worker is using stale opcache after a package update | Restart the worker process; php artisan queue:restart |
Failed to open stream: StringerServiceProvider.php |
Path-repo composer install in copy mode hasn't synced | composer update giorgigrdzelidze/laravel-stringer then php artisan optimize:clear |
πΊοΈ Roadmap
Planned β runtime Settings overlay (DB row wins over .env at request time), per-locale voice overrides, ship the β
/π/β confirmation keyboard (the STRINGER_IMAGE_REQUIRE_CONFIRMATION flag is wired but the menu node hasn't shipped yet).
Speculative β fact-check pass via a second LLM, semantic deduplication of generated bodies against existing articles, internal-linking suggestions from the host's ContextBuilder output, two-pass outline β expand for long-form drafts.
π§ͺ Quality
- 𧬠PHPStan level 6 + Pint (Laravel preset, strict types, strict comparison, single quotes)
- π§ͺ Pest 4 + Orchestra Testbench, 175 tests / 470 assertions, CI matrix across PHP 8.3 / 8.4 Γ Laravel 12 / 13
- π Behind
Http::fake()β every driver is mockable without live API calls - π PHP
^8.3(Pest 4 floor), Laravel^12.0 || ^13.0
π License
MIT β see LICENSE.
All versions of laravel-stringer with dependencies
illuminate/database Version ^12.0 || ^13.0
illuminate/http Version ^12.0 || ^13.0
illuminate/queue Version ^12.0 || ^13.0
illuminate/support Version ^12.0 || ^13.0
spatie/laravel-tags Version ^4.0 || ^5.0