Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ghostwriter Core Laravel Package

1994/ghostwriter-core

Ghostwriter Core provides the shared Laravel/PHP foundations for Ghostwriter, including core services, configuration, and integration points for building AI-assisted writing workflows. It is a lightweight base package for related Ghostwriter modules.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a core library, not an installable feature set. In practice you will rarely composer require 1994/ghostwriter-core directly — you install one of the host addons (Statamic, Filament, or Craft), which pins and pulls this package in. v1.10.0 is explicitly the shared engine for Ghostwriter 1.3 (Statamic and Craft) and Ghostwriter for Filament 1.2; check your addon's pinned core version before assuming any feature below is present. Only add the core to your own require block if you are building a fourth integration or extending it.

Minimal steps to get oriented, in this order:

  1. Start with Connections, not the Writer. The new Connections\Connections resolver is the front door to every outside service and is the fastest thing to get working. Connections\Services enumerates every supported service (Anthropic, OpenAI, Gemini, OpenRouter; Unsplash, Pexels, Pixabay, Openverse; Shutterstock) with its key page, fields, and env variable names. Read Connections\KeyCheck (ChecksKeys) and note the "Check & save" is one small live call per service, tried once — you can validate every key end-to-end before running a single model job.
  2. Read the contracts. The package is still framework-free, so the interfaces remain the real documentation. The contract surface has roughly tripled: Ai\Ports\Credentials, Connections\KeySources, Connections\CredentialStore, ChecksKeys, Suggest\EntryIndex, Gaps\AssetAlt, Gaps\SeoFields, RevisitStore, EntrySource, LinkProbe, LinkIndexContract, SeoWriterContract, ProposedLinksContract, RenderProfiles. Grep interface and triage by what you actually need.
  3. Then read the tests and docs/. There is now real prose documentation (docs/connections.md and friends), which is new — use it. But the test suite is still the executable spec, and Testing\FakeKeyCheck, Testing\InMemoryCredentialStore, MemoryEntryIndex, MemoryEntrySource, and InMemoryRevisitStore are deliberately shipped fakes: they tell you the intended unit-of-work boundaries better than any class does.
  4. Check the host addon's service provider, not this package's. The core still registers no Laravel bindings, facades, or config publishing. Connections\Connections reads "the environment or config first, which always wins, then what was set up on the Connections page" — but that ambient reading is satisfied by the host addon, not by the core. Find the addon provider before debugging any "target not instantiable".
  5. Confirm the version you actually have. Run composer show 1994/ghostwriter-core and record the exact reference. With two addons at different versions (1.3 vs 1.2) consuming this release, mismatches are now more likely, not less.

First use case: run the free checks first — Suggest\Findings::standard()->report($checkContext) makes no model call and no HTTP request, and gives you past-year detection, closing dates, empty link text, long sentences, SEO description problems, and heading-length issues immediately. That is a genuinely free, zero-risk way to confirm wiring. Only then move to one Studio job against one provider with a hand-written prompt.

Implementation Patterns

Wrap the core behind your own service. Still the right call, and more so now: this release adds Connections, Suggest, Revisit, and an entire SEO layer, which is a large pre-1.0-shaped surface. Put App\Ghostwriter\Writer in front of it and own provider selection, prompt assembly, and persistence there.

Bind every port explicitly in one provider. The list of seams is much longer than before. Bind, at minimum: CredentialStore (Eloquent/encrypted in app, InMemoryCredentialStore in tests), EntrySource and EntryIndex, RevisitStore, LinkIndexContract, SeoWriter (or use PlainSeoWriter), LinkProbe (or HttpLinkProbe), AssetAlt, SeoFields, RenderProfiles. One GhostwriterServiceProvider, interfaces only, so the seam is discoverable.

Respect the credential precedence — do not fight it. Connections\Connections resolves environment/config first, then the encrypted store, and status() returns one of: connected (with masked ending), not set, set in .env or config, key stopped working, no key needed. That means a key saved on the Connections page will be silently shadowed by an env var. When someone says "I saved my key and it still fails," status() is the first thing to print, not the HTTP response.

Treat every Studio entry point as slow, external, and now multiplicative. A single page can trigger: one seo-editor call, one seo-verifier call, a reviewer call per part (ReviewInput::calls() splits long pages), a verifier call per part that kept anything, plus reworder on demand. A long page is therefore ~2N calls. Queue all of it, set explicit timeouts, never call from a saving event, and debounce anything keystroke-driven.

Use the free-checks layer as your cost governor. Revisit\RevisitScanner ranks pages by free checks before any model is involved, with Priority::SEO_CAP capping all search reasons at 25. Drive your "what should we work on next" UI from RevisitRow::priority() and only spend tokens on what a human opens.

Persist decisions, and understand what persistence buys you. Quiet/Quieted make "It's still right" and Dismiss stick for 12 months or until the passage is edited; Quiet::CHECKED remembers what the reviewer or verifier already judged fine. This is a feature, but it means re-running a suggestion test twice will produce different results — reset the quiet store between test cases.

Run the scheduled things on a schedule you own. Nothing here self-schedules. You need: RevisitIndex::refreshOne() on save, deleted() when an entry is removed, refresh() daily plus a full pass weekly, and the link index's daily Seo\LinkPlan pass (caps: 5,000 rows per group, 20,000 per site). Put all of these in your scheduler with overlap protection.

Write SEO output only through the policy layer. Seo\MetaPolicy/SeoProvenance exist so a generated search title or description never silently overwrites a person's text, and Seo\SeoMetaCheck/MetaRange keep it honest about length. Use SearchFields::apply() for "Use this draft" and SeoPass::retryMeta()/editMeta()/useMeta() for the Search section rather than writing columns yourself.

Keep the deterministic pieces as pure functions. Seo\HeadingFixer (no model), Seo\SlugRules, Seo\FilenameRules, Seo\Stemmer, Seo\LinkCandidates ranking, and the layout algorithms are all deterministic. Call them directly in tests, cache by input hash, never recompute per render.

Prefer suggestion-only entry points where they exist. SeoPass::suggestLinksFor() returns Seo\LinkProposals and writes nothing. For existing pages, that is the safe path; use the write path only for brand-new drafts.

Treat prompts as versioned data. Now there are at least reviewer.md, verifier.md, reworder.md, plus seo-editor and seo-verifier. Log prompt name + version alongside model id and request id, or you will never diagnose a quality drift across five cooperating prompts.

Gotchas and Tips

  • License is still NOASSERTION, and this release did not change it. Still the single biggest blocker for commercial deployment. Get the LICENSE file or composer.json license key reviewed before shipping. NOASSERTION means the tooling could not identify a recognized license — it does not mean permissive.
  • Pin against your addon, not against features. v1.10.0 backs Ghostwriter 1.3 (Statamic/Craft) and Filament 1.2. If your addon pins ^1.9, requiring ^1.10 will conflict or, worse, resolve mismatched integration points. Let the addon own the constraint; pin to a commit SHA in composer.lock if the tag looks off.
  • Zero stars, zero dependents, bus factor one. Assume no upstream fixes. If any of Suggest / Revisit / SEO becomes load-bearing, vendor or fork it.
  • "Framework-free" still means no config(), env(), app(), or facades inside the core. Connections appears to read env — that expectation is satisfied by the host addon's KeySources. Follow the chain before filing a bug.
  • A 401 may not surface as an exception. Connections\KeyWatch watches the site's HttpClients and flips a service to "Key stopped working" via markBroken(). When debugging auth, check status() and the KeyWatch state before you go looking for a thrown error.
  • Never log keys, raw prompts, or drafted page content. Masked endings (Mask::ending(), Mask::fingerprint()) exist so you can log something safe. Keep Http::debug and Guzzle verbose middleware off in production — the credential layer passes keys around by design.
  • The external link check is opt-in and default-off for good reason. RevisitOptions::$externalLinks enables ExternalLinkCheck: one request per second per host, 10s timeout, max 500 per run, and a link is broken only after two consecutive failures. If you turn it on, it must be queued and scheduled, and you must be comfortable making outbound requests to arbitrary third-party URLs from your app.
  • Many things silently drop rather than throw. LinkValidator checks every pick in code before the verifier sees it; SuggestionValidator drops unanchored suggestions; Seo\LinkGuard turns links to unknown pages into "links to choose"; Quiet::CHECKED suppresses repeats. "I got no suggestions" is usually a filter, not a failure — instrument the drop reasons before assuming the model is broken.
  • Known non-targets for internal links. Seo\Linkable refuses unpublished, scheduled, expired, address-less, noindex, home, and utility pages. If a page never gets linked, check Linkable first.
  • Respect the link caps; they are not advisory. ~1 link per 250 words, 2–5 total, never two in one paragraph, at most two per section (LinkValidator::PER_SECTION), never forced. Do not "fix" a draft by injecting more.
  • Long pages fan out — and split review context. ReviewInput::calls() splits a page into parts, each with its paragraph, heading, and the page's title/kind/date. Cost and latency scale with page length; a 5,000-word page is a budget conversation.
  • Localised phrase lists are finite. Phrases::for(), Dates, and the resources/lang/{en,de,fr,nl,es} files cover English, German, French, Dutch, and Spanish. Free checks in other languages will under-detect — that is not a bug, it is a coverage boundary.
  • Exceptions are still generic. Wrap provider and verifier calls in your layer, catch \RuntimeException, and attach provider, model, prompt name/version, and request id before logging and rethrowing.
  • Extension points, in order of preference: custom AI provider → CredentialStore → KeyCheck/LinkProbe (the small outbound-call ports) → store/index ports (RevisitStore, EntrySource, LinkIndexContract) → prompt source. Avoid forking Stemmer, LinkValidator, HeadingFixer, or the layout algorithms — they are the densest, most-tested code here.
  • When something breaks, reproduce outside the host addon first. The shipped in-memory fakes (InMemoryCredentialStore, MemoryEntryIndex, MemoryEntrySource, InMemoryRevisitStore, FakeKeyCheck) let you stand up the whole thing in plain PHPUnit with no Laravel bootstrapping. Fails there = core bug; fails only inside Filament/Statamic/Craft = container binding or config in the addon.
  • Testing hook worth knowing: Connections\Services::withTestServices() gives you an end-to-end connections fixture without touching real providers.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console