Skip to main content
The current release is 0.13.1. Every release below is published on npm.

All twelve packages move together

Twelve published names share one version number and are released at the same time. Eleven are scoped: shared · blocks · preview-adapter · site-sdk · orchestrator-core · richtext · migration-sdk · mcp-server · astro · skills · cli From 0.9.0 the unscoped create-avocado-site moves with them, which makes twelve. It is the scaffolder, so the versions it installs into a new project are pinned to the release it came from; a scaffolder on a different number is a scaffolder writing a lockfile nobody tested. This surprises people, so it is worth stating why. The packages depend on each other through workspace:^, which the publish step rewrites to the exact current version when it packs. If only some packages were released, the ones that shipped would carry manifests pointing at versions nobody published, and the install would fail. So a package with no changes in a release still gets the new version. The practical consequence: do not mix versions. Pin all @avocadostudio-ai/* dependencies to the same version, and upgrade them in one step.
A minor bump (0.6 → 0.7 → 0.8) means something in the release is breaking for some callers. A patch bump is safe. Ranges like ^0.6.0 admit 0.6.1 but not 0.7.0, which is exactly why breaking changes are never shipped as patches.

0.13.1

Undo worked and nobody could see it. POST /history/undo answered applied, the draft really did revert, and the preview went on showing the text you had just undone. Each half tested fine on its own: history restored a snapshot carrying the updatedAt it was taken with, and the live-preview store reads that field as the page’s version and drops any render that is not strictly newer. So a restored page always looked stale to the preview, and undo, redo, restore and discard were all invisible on screen. Restoring a snapshot now stamps it with the current time, as every other write already did. If you drive history from library mode, that is the whole reason to take this release. The rest is carried by @avocadostudio-ai/cli, which vendors the editor build — a republish is the only way these reach a self-hosted install. Dark mode follows the system again (the theme was applied only from a store subscription, which never fires when the first paint already matches). The chat thread no longer jumps when you press Undo. The preview’s rounded corners no longer show the page behind them, and no longer flash white in dark mode. And a new setting, Open properties on select, stops the right panel opening every time you click a block. Nothing in this release changes an API.

0.13.0

?__editor=1 alone was enough to read an unpublished Astro draft in production. Any route @avocadostudio-ai/astro made renderable answered a request carrying only that parameter — no cookie, no secret — with the draft. The parameter is a routing hint the editor puts on the iframe URL and it authorizes nothing; the Next path has enforced that since resolveDraftContextCore, and Astro never got the same gate. Every release from 0.8.0 to 0.12.1 has it. Two things authorize now: the signed cookie /api/editor/draft?secret=… mints, and a valid secret on the request itself. Development is unchanged. If you serve Astro on-demand routes, take this release. Breaking — editorApiPath is removed. Delete it from your avocado({…}) block; the editor API is always at /api/editor. The other end of the contract is spelled out in three packages, so moving only this half mounted the API where nothing would call it and the manifest answered 404 against a config that read correctly. A site that needs another path mounts the route itself with createAvocadoEditorApi({ basePath }). editorOrigins now reaches CORS. The list in astro.config.ts decided the postMessage target and nothing else, so a correctly configured deployment answered every editor fetch with no Access-Control-Allow-Origin. It now decides both, and you no longer need EDITOR_CORS_ORIGINS as well — that still works and is additive. The preview bridge also validates it now, and refuses to attach to an unlisted origin rather than degrading to your first entry the way the server does. Three more Astro fixes: a block rendered outside <main> never updated in the preview, Astro.locals.avocado had no type under astro/tsconfigs/strict (which is what astro create writes), and a site that named its production editor origin could not be opened from a local one. See the Astro integration page, whose Production section now covers the three settings that are inert in development. A minor bump rather than a patch: an option left the options type and a production deployment answers differently. Both are corrections, and both can change what a deployment does at upgrade time.

0.12.1

A real customer’s filename shipped to npm inside a JSDoc comment. Three built files — shared/dist/links.js, its .d.ts, and orchestrator-core/dist/checks/rules-draft.js — each carried a doc block that used a real customer’s PDF filename as a worked example of fuzzy path matching. The example works because such a filename is long and easy to mistype, which is why a real one got reached for. The build does not strip comments, so every release from 0.4.0 onward carried it. This is the first version you can install that does not. Comments only — no API, no behaviour, no contract change — so a patch. If you are pinned below this, upgrading is the only way to stop redistributing it.

0.12.0

Prompt caching was off for everyone who never set it. ANTHROPIC_PROMPT_CACHE defaulted to false, and for this setting the default was the whole story: only this repository’s own .env ever named it — not .env.example, not either dispatch workflow, not any integrator’s environment. Every Anthropic request from the package sends the same large stable system prefix and the same submit_edit_plan schema, which is exactly the shape caching exists for, and it billed at full price everywhere the default applied. It is on by default now.
The 5-minute TTL is deliberate and should not be raised. Replaying 651 real planner calls from telemetry, most sit under a minute apart; only 44 fell in the 5-to-60-minute window that a 1-hour entry would rescue. At 2x to write against 1.25x, buying those back came out 20–32% more expensive across the set. The reasoning now sits next to the code, so the next reader does not “fix” it upward.
Two package defaults that were also wrong for everyone. Library-mode planning still named models retired a year ago, now resolved through one defaultModelLookup() exported from the public API; and a published package wrote [create_site] progress onto whoever’s stdout it happened to find.

0.11.9

The announcement bar reads as one. The Banner’s info variant was a near-white mint between a white header and a pale hero — three washed-out bands stacked, with the one carrying the announcement reading as the gap between the other two. It is solid brand now, with an inverted CTA. Follow-up chips no longer ask questions. “Edit heading”, “Edit CTA” and the rest sent a field name with no value, so the only possible reply was “edit it to what?” — zero operations, and real money on a keyed plan. What remains names complete actions. Setup recommends Anthropic as the best-tested planner rather than listing three providers as equals, and says that Avocado Studio is a research preview — which the docs now say too. The demo points at where to onboard your own site, and points at Bring your site in rather than the Next.js wiring guide, which is the right page only once you have decided. avocado-register stopped sending people after a service they do not need. It defaults to http://localhost:4200, the standalone orchestrator’s address, while most integrations mount the orchestrator inside their own Next app — so it failed and told the reader to run pnpm dev:orchestrator, a script in Avocado’s repo and not in theirs. The failure now names library mode and the exact --orchestrator URL to pass, .env.local outranks the built-in default so a second run does not fall back to 4200, an unreachable orchestrator reports itself and exits 0 rather than calling a mostly successful run a failure, and ORCHESTRATOR_URL is written only once something has answered there.

0.11.8

Setup offers to start the project for you. It had already chosen ports, generated secrets, installed everything and written your key — then handed back two commands to type. It now asks Start it now? and, on yes, runs the dev server for you. Declining prints the commands as before. The key prompt says what it is and which keys it takes. On the directory-argument path there was no intro at all, so the first thing a stranger met was an unexplained request for a secret. It now names the product, says that everything except chat works without a key, lists the three accepted variables, and — when you paste one — says why it chose the variable it did and how to change it. The site list no longer shows the same site twice. A scaffolded project was seeded with a built-in demo preset it had never owned, which since 0.11.5 also rendered under the same name.

0.11.7

Production editing works again. The editor could not select, edit or inspect anything once a site was built for production — no block markers, no overlay, no messages. The SDK requires the site’s DRAFT_MODE_SECRET on the preview URL there, and the CLI had no way to send it: no flag, no environment variable, no field. avocadostudio start now takes --draft-secret (or reads DRAFT_MODE_SECRET from the environment), the scaffold forwards it automatically, and the banner warns when it is missing. The most likely first message works. Change the headline to "…" produced no operations on the keyless planner — “headline” was not among its keywords, and the branch required a block to already be selected. It now finds the page’s first heading itself. The setup asks whether you want to add an API key, and Enter skips it. Everything except chat works without one; the reason to ask early is that keys are read at startup, so a key added during setup needs no restart.

0.11.6

Publish was impossible from the editor, and the cause was a secret baked into the published CLI.
If you installed @avocadostudio-ai/cli@0.11.4 or @0.11.5, rotate your publish token. Both were built where a developer .env.local was present, and Vite inlines every VITE_* variable at build time — so both bundles carried a real publish token, the draft-mode secret, and the agent-CLI opt-in switched on. 0.11.3 and earlier are unaffected. A release build now reads no local env files, and a check in the publish step fails if anything is inlined.
Publish works again. x-publish-token was missing from Access-Control-Allow-Headers, so the browser refused the preflight and the request never left it — the server logged a successful OPTIONS and nothing else, and the editor said only “Failed to trigger publish”. The allowed headers are now derived from one list that is checked against the editor’s own source. The publish review shows page metadata. Title, description and Open Graph image had no representation in the diff, so the panel that exists to say what will change omitted every SEO edit — a page whose only change was its title reported none. Also: uploaded attachments no longer render as broken images; production no longer offers chips the planner cannot execute; voice input is hidden where it cannot work; the keyless notice can be reopened after dismissing; and Settings says when no model is available instead of naming one.

0.11.5

The paperclip on the first screen of every new project answered 405. Chat attachments work in library mode. POST /attachment/upload existed only in the standalone orchestrator, while the editor posts every chat attachment to it unconditionally — so the composer’s Attach file button shipped enabled on the first keyless screen of every scaffolded project and rendered Method POST /attachment/upload not handled by createOrchestrator() in red underneath it. The route is implemented now rather than hidden: the storage and the serving route already existed, only the route was missing. PDFs are accepted and servable. A missing asset can fail legibly. The CLI’s static server answered any unknown path with 200 text/html, so a renamed or cache-busted asset came back as the app and the browser threw SyntaxError: Unexpected token '<' naming no file. Paths with an extension now 404; extensionless paths still get the SPA fallback. The /ops envelope is discoverable from its own first error. It now names the key it wanted and carries a valid minimal example, instead of taking three round trips to reveal the envelope, the discriminator and the shape one at a time.
Nothing to change in your project. Every change here is additive: a route that did not exist, a 404 where a misleading 200 used to be, and a longer error body on a request that was already failing.

0.11.4

The third CORS defect in three releases, and the reason there were three. The editor was dead to the origin the CLI prints first. localhost and 127.0.0.1 are the same machine and two different origins. The CLI binds 127.0.0.1 and its banner leads with it; every doc and generated .env.local says localhost. A production site that named one allowed only that one, so a browser at the other had every request answered 200 with no access-control-allow-origin and thrown away before any code could see it — while the frame-ancestors list, which had expanded both spellings since it was written, allowed the preview frame. The editor painted and was inert, and curl reported everything working the whole time. Everything that decides whether a browser may talk to a site now expands both spellings: the SDK’s editor routes, every orchestrator route, and the ORCHESTRATOR_CORS_ORIGINS check behind publish/diff.
Nothing to change in your project. If you already set EDITOR_CORS_ORIGINS or NEXT_PUBLIC_EDITOR_ORIGIN, both spellings of what you wrote now work. Only these two loopback names, and only when the origin you configured already uses one — it is not a normaliser and not a wildcard.
The editor showed the wrong site name and the wrong first-screen suggestions. Two symptoms of one cause: identity that arrives after the screen is drawn, and nothing that redraws it. GET /sites now reports the configured siteName and demoContent; the editor’s registry fills a name that is only the id in title case (and still never one you typed); and the welcome message is re-derived when the site’s identity changes rather than frozen at first render. A project in demo-a/ configured as “The Avocado Hub” was called “Demo A” everywhere, and a deployed demo offered three suggestions its own keyless planner cannot execute while the same build in next dev did not.

0.11.3

A production deployment with an access gate configured could not load the editor’s component manifest. The editor stamps its access token on every request whose origin matches the orchestrator’s — not whose path does. In library mode /api/avocado/* and /api/editor/* share an origin, so both route groups receive the x-access-token header. The orchestrator’s preflight listed it; the SDK’s editor-route preflight did not. So once a gate was configured and a token minted, the browser refused the preflight for /api/editor/blocks and /api/editor/pages — the manifest the whole editing surface is derived from — and the requests never left the browser. The server saw nothing and logged nothing. Development was unaffected, because with no gate there is no token to stamp.

0.11.2

Every route answered 200 in production and the browser discarded all of them. The orchestrator route create-avocado-site generates was never told which origin the editor is served from, so with NODE_ENV=production it answered every request with no access-control-allow-origin at all. The editor reported that it could not reach a server that was answering 200 to curl. A project scaffolded on 0.11.2 or later writes three variables into .env.local, each read by different code:
An older project needs these added by hand. The values are not interchangeable — the first two name the editor’s origin, the third names the site’s. The generated .env.local carries a comment for each explaining which code reads it. Without the first, a production deployment’s editor cannot reach its own orchestrator; without the third, the Publish panel’s diff answers 400 siteOrigin is not an allowed URL.

0.11.1

Two buttons nobody had ever pressed, both of which answered ok: true. 0.11.0 tested publishing and fixed what that found. This release comes from a run against the published tarballs that pressed two more: a publish carrying nothing, and a login against a mount configured to refuse everything. Neither was a hole an attacker could walk through. Both were the system saying yes while doing something else.

Breaking: a publish that removes every page is refused

POST /api/editor/publish checked the shape of pages and nothing else. [] is an array, so a publish that deleted the entire site was indistinguishable from one that fixed a heading — authenticated, under NODE_ENV=production, {"pages":[]} returned {"ok":true,"slugs":[]} and every URL on the site became a 404. The reason this is a guard and not a warning is what actually produces an empty array. It is rarely somebody deleting their site one page at a time; it is a client publishing what it thinks it has after its own state failed to load. Losing a site to a failed fetch is not a decision anyone made. The rule is the narrowest one that holds: a publish may not remove every page. Removing one page of three stays an ordinary edit. Emptying the site needs "allowDelete": true in the body, which is something somebody types on purpose and cannot arrive at by omission, and the refusal is a 409 naming it:
A site that is already empty may still publish empty — that removes nothing, and refusing it would fail a new integration on its first publish. createEditorApiHandler passes its own getPages as the baseline, which is how the refusal can say what it protected; a baseline that throws, a CMS read that timed out, does not open the gate, because not knowing what is there is not a reason to overwrite it with nothing. maxPagesRemoved bounds it further for sites that want tighter than “not all of them”, and the rule itself is exported as checkDestructivePublish from site-sdk/routes.
What you have to change. Nothing, unless something you own publishes an empty page set on purpose — a migration script, an agent, a test fixture. Those need "allowDelete": true in the request body now, and otherwise get a 409 whose reason says so. A publish that changes or removes some pages is unaffected.

Breaking: POST /auth/verify refuses on a closed mount

A library-mode orchestrator under NODE_ENV=production with no credential configured resolves to closed — there is no password and no token to check one against, so every route behind the gate answers 401. /auth/verify answered {"ok":true,"accessToken":"..."}, to any body at all, including {}. It was never exploitable: all three transports (Authorization: Bearer, x-access-token, ?accessToken=) were checked, and the token opened nothing. That is what made it expensive. A login that says yes while the system is shut leaves the operator holding a token and 401ing everywhere with no way to connect the two. The route now answers 503 with error: "unavailable" and the same reason string /auth/status already returns, which names the two variables that fix it.
What you have to change. Nothing if your mount is configured. A client that treats every non-200 from /auth/verify as a wrong password should tell the 503 apart: it means the server has no credential to verify against, not that the one you sent was wrong. Set ACCESS_PASSWORD_HASH or ORCHESTRATOR_ACCESS_TOKEN, or pass auth to createOrchestrator().

Fixed: a refused publish now explains itself in the editor

The two refusals /api/editor/publish gained across this release and the last one — the 409 above and the unconfigured-publish 401 below — each carry a machine-readable verdict in error and the sentence a person can act on in reason. The orchestrator’s publish target read only error, so the second half died one hop from the person who tripped it: the editor showed the word unauthorized and nothing about PUBLISH_TOKEN, and would have shown refused and nothing about allowDelete.

Also in 0.11.1

  • The keyless “chat is running without a key” panel rendered two of its paragraphs at 1.65:1 contrast — effectively invisible. Not a colour choice: the panel mounts inside the chat header, whose .chat-header p { color: var(--muted) } outranks a bare class and painted the header’s grey onto the panel’s near-black ground. Only the two <p> elements were affected, which is why the panel looked half-rendered rather than broken.

0.11.0

Publishing, which nothing had ever tested. A clean-room run took the same path as the one before it and went a step further: it pressed Publish. Three defects, and they compound — the path the editor uses wrote nothing, the path that writes was unauthenticated, and what it wrote the site could not read. Nothing in the repository referenced createJsonFilePublishHandler; writeOnPublish appeared only in the adapter that defines it.

Breaking: the publish route refuses to run unconfigured in production

publishSecret was optional on a route that overwrites a site’s content, and every scaffold and example wired it to process.env.PUBLISH_TOKEN against a variable none of them ever wrote — not set, not commented, not mentioned. So the value was always undefined, the guard was dead code, and one unauthenticated curl replaced every page on the site. The deployment that proved it had ACCESS_PASSWORD_HASH set exactly as its own README instructs, and was correctly answering 401 on /api/avocado/* at the same moment: the two route groups are different handlers, and only one had ever been asked about auth. Under NODE_ENV=production with no secret configured, the route now answers 401 with a reason naming the variable. Development is unchanged — publishing to your own machine is the point — and warns once, through the same code path that ships.
What you have to change. If you publish to a production site, set PUBLISH_TOKEN there and give the orchestrator the same value: it sends it as the x-publish-token header. Supplying publishSecret to createEditorApiHandler() directly does the same thing. With neither, every publish to that deployment is refused, and the 401 says which variable is missing. npm create avocado-site generates a token into .env.local rather than leaving the line blank.

Breaking: createJsonFilePublishHandler writes PageDoc[] by default

It wrote { pages, siteConfig }, three lines beneath a docstring saying it wrote an array, and three of its four call sites read the file back as one. So a successful publish broke the site that had just published: a reader doing JSON.parse(...) as PageDoc[] throws on an object — the try/catch around the read catches a parse error, never this — and a reader expecting a slug-keyed object silently found no pages at all. array is the default now, because it is what the readers and the adapter expect.
What you have to change. If whatever reads your published JSON expects { pages, siteConfig }, pass shape: "wrapper". A reader that wants siteConfig in the same file needs it: a plain array has nowhere to put one, so site-level settings — name, logo, navigation — are dropped on publish. If your reader expects an array, you were already broken and this fixes it.

Fixed: publishing from the editor wrote nothing and reported success

jsonFileAdapter({ path }) without writeOnPublish defaults to false, so the adapter had no onPublish at all, and the scaffolded README stated plainly that publishing from the editor rewrites the content file. After dozens of chat edits through the real pipeline the file was byte-identical to the scaffolded original. The demo publishes for real now. The summary under the green tick also read adapter has no onPublish; publish is a no-op — two audiences given one sentence, and the first-run user got the developer’s. The API keeps that reason; the editor prints a sentence written for the person who pressed the button.

Fixed: a deployed site cannot rewrite its own content file

writeOnPublish works on your machine and fails on a serverless host and in any container built from an image, because the runtime filesystem is read-only. That is now said where it is relevant rather than discovered on the first production publish. Publishing from a deployment goes to something that persists.

Also in 0.11.0

  • The editor no longer asks for CMS media on first load from a server that has already reported features.cmsMedia: false on /status/planner. A new user’s first impression of their own log was a red 404 on POST /media/cms.
  • “Add an API key” is a button. It was a <span> with a title attribute — hover-only, on an element with nothing marking it interactive — and it was the only permanently visible place in the editor naming .env.local. It now opens a panel naming the file, the folder it lives in, the variables and what each one buys, and the restart.
  • The scaffold’s build gate probes POST /api/editor/publish under a production server with no token configured and asserts the 401, that the refusal names PUBLISH_TOKEN, and that the content on disk is unchanged. It then runs a publish round trip in development: publish through the editor’s path and assert it wrote, publish through the other path and assert the shape, then re-read the file the way the site does and ask the site for the page.

0.10.0

The first run, as a stranger performs it. 0.9.0 shipped npm create avocado-site and verified it the way the person who built it would: scaffold, boot, click the thing you know works. A clean-room review then ran it the way a new user does — npm run dev, npm run build, npm start, click the buttons the product puts on screen — and found eight defects, every one of them in a path no test could see, because no test had ever built and served what the scaffolder emits.

Fixed: a production build served an editor that could load nothing

Library mode is closed under NODE_ENV=production with no credential, which is the right default. What was wrong is that nothing said so. Two questions had been collapsed into one flag: gateEnabled answers “should I prompt for a password?”, and a closed mount has no password to prompt for, so it answered false. The editor read that as “open”, rendered itself, and 401’d on every request behind it — no password box, and no way to reach one. /auth/status now also reports mode, and a closed mount’s 401 carries a reason naming the variable to set. Withholding that was protecting nobody: in this state no credential exists, so there is no caller to withhold it from — only the operator, looking at a 401 on their own deployment. error: "unauthorized" is unchanged, because that exact value is what the editor’s fetch shim matches on to re-prompt.
mode is additive and gateEnabled keeps its meaning and its value. The values mode can take are token, hook, open-dev and closed; only closed carries a reason.

Fixed: in development, no page had a title and no unknown slug 404’d

A configured siteId is the site’s own identity. It was being read as evidence that the editor was asking — and since every integration configures one, every anonymous request in development resolved an editor context. generateMetadata short-circuits to noindex and nothing else for an editor render, and the render took the draft path, where a missing page is “draft unavailable” at HTTP 200 rather than notFound(). So titles, social cards and 404 behaviour were all unobservable in the only mode anyone develops in. An editor render now requires a signal that the editor sent the request: siteId, session, editorOrigin or __editor on the URL, draft mode, the draft cookies, or a valid secret. Production behaviour is unchanged by construction — every way of passing the existing authorization gate is itself one of those signals.

New: createSitePage({ siteUrl })

Three tags cannot be derived from a page’s own content, because none of them is knowable without knowing where the site lives: <link rel="canonical">, og:url, and an og:image resolved to an absolute URL. A page that stores its image as a relative path is correct in an <img src> and ignored by every social crawler, so a site can pass a “has an og:image” check and still render a blank card.
Unset, the SDK still emits none of the three: a wrong canonical is worse than an absent one. Reading it from an environment variable at the call site is the intended shape, so preview deployments describe themselves instead of all claiming to be production.

Fixed: suggestion chips that did nothing

Eight of nine chips did nothing on a keyless install, including all three on the first screen, under a banner reading “No API key needed to look around”. A chip’s label is the prompt — it is sent verbatim — so a phrasing the answering planner cannot parse is a button that does nothing. The chips were written against a model; with no key the answer comes from a short list of English substring matchers. The first-screen trio is now chosen against the planner that will answer it, and the follow-up chips are filtered through that planner itself rather than against a second list of blessed phrasings — so if the matcher stops answering something, the chip for it stops being offered in the same commit. Returning fewer chips, or none, is the intended outcome. The curated chips also look at the page now, instead of offering “Add an FAQ section” on a page that already ships one.

Also in 0.10.0

  • A keyless reply no longer credits a model that did not run. With no provider key configured at all, the per-message model chip reported the lookup’s default. Narrow on purpose: a deterministic plan produced while a key is configured still names the model, because that model would genuinely have run.
  • The assistant no longer offers to revert an edit it cannot revert. Two planner prompts instructed the model, in as many words, to suggest “Revert to previous”. It has no access to the undo stack, so the suggestion resolved to a refusal or to fabricated copy presented as a restoration, while the editor’s own working Undo button sat in the toolbar.
  • Every demo page declares an ogImage, three of them no longer carry a different product name in the <title> a crawler sees, and the scaffold’s 404 page exports metadata — a correct 404 previously still had no title.
  • scripts/scaffold-serve-check.mjs, in pnpm test:build. The only thing in the repo that built a real app and read the HTML built an example in mode: "static". The scaffolder emits mode: "auto" — a different branch of createSitePage, and the one every new user runs. The new check generates the scaffold from its own templates, overlays the working tree, then builds and serves it in both modes and asserts on the bytes: metadata and a real 404 in development, and in production the closed gate naming itself.
  • A gate that fires the editor’s own chips at the planner that answers them. The chip builder lives in the editor and the keyless planner lives in the orchestrator; nothing connected them, so nothing noticed when they drifted apart — and they had.

0.9.0

There was no way to see Avocado without cloning it.

New: npm create avocado-site

One command starts the site and the editor beside it, on a nine-page demo — the Avocado Hub — with every renderer in the library on screen at once. No API key is needed to look around. Rendering, click-to-select, the property panel, undo and publish all work without one; chat is the single thing that does not, and it says so rather than failing. The orchestrator runs inside the scaffolded Next.js app at /api/avocado, in library mode. There is no third service and nothing to deploy separately.
The package existed before under a name it could never have been invoked by: npm create requires a create-* package name, and it was published under none. So this is the first release in which the repository’s own front door actually works.

New: siteName and demoContent on createOrchestrator

The editor asks the orchestrator what it is mounted on. It used to guess. siteName is what the editor greets you with; without it the site id gets title-cased, so a mount called my-shop is introduced as “My Shop”. Reasonable for an id that is a name, wrong for one that is a directory. demoContent: true says this mount serves Avocado’s shipped demo pages, and turns on the first-run suggestions written against them. Those suggestions — and the demo greeting, and the site’s own name — were previously keyed on a hardcoded list of four site ids, all of which live in the Avocado repository. Any site anyone else stood up was excluded by construction from the onboarding written for it. Both are reported on /status/planner. Neither is required.

Fixed: a keyless editor that looked broken rather than keyless

With no provider key, chat falls back to a rule-based planner that handles a slice of literal edits. Everything outside that slice used to come back as “I need one clarification: what section should I change and what exactly should be updated?” — which reads as a planner that cannot understand plain English, not as one that was never given a key. It now says which variable to set and where. The header badge says so too.

Fixed: “review this page” never reached a model

Two detectors claimed the message and the deterministic one was tested first, so a judgement question was answered from a template in twelve milliseconds. The template was written against every block type registered in the process — which, because importing anything from @avocadostudio-ai/shared registers the built-ins transitively, is Avocado’s own type names and not yours. A site rendering its own fourteen types was told it was missing Hero, CTA and FAQ, and advised to add three blocks it has no renderer for. add_block for one of those applies cleanly, reports success and draws nothing. Judgement questions route to the model now, with the page’s real props in context. What survives from the template is pageObservations: facts that were looked up rather than inferred — a declared image field with nothing in it, a page nothing links out of, a missing meta description. The same leak is fixed in two more places: the block catalogue answer no longer suggests adding types your site cannot draw, and neither does the planner’s specify-a-type clarification.

Also in 0.9.0

  • The overlay’s missing-editable-targets warning is withdrawn. Every block wrapped and no field marked is not a fault, and reporting it through console.error painted a red Console Error over a working integration. site-sdk/coverage still measures it for anyone who asks.
  • The editor’s unreachable-orchestrator screen retries on its own, backing off to fifteen seconds. An orchestrator is unreachable for a few seconds on every restart, and the screen could previously only be left by clicking Try again at the right moment.
  • The model dropdown offers Claude only. The planner’s prompts, its structured-output path and the eval set are all tuned against it. UI only — the orchestrator still advertises and accepts all three providers.
  • Inline editing is its own documented step rather than step 2 of the Next.js integration, where it was deferred and forgotten.
  • The demo content is one coherent nine-page site rather than six narrower authored pages, with the dead CTA, the nav entry pointing at /be-shorter-unsplash and the alt attributes holding image-generation prompts all fixed at the source.

0.8.0

A tenth package, and a field kind for the markup most templates actually store.

New: @avocadostudio-ai/astro

Avocado now integrates with Astro. Your .astro components keep doing the rendering — no React, no islands, nothing ported. Avocado supplies the schema, the draft props, the editable markers and publishing.
astro.config.ts
That is the wiring. See the Astro integration guide for the content module, why editablePages is a list, and what a build does with all of it (nothing — the output stays as static as it was).
Astro support shipped from one pilot integration. It renders, edits, previews and publishes; what it has not yet had is a second site. Expect gaps around anything the pilot did not exercise.

New: kind: 'html' for fields whose stored value is markup

A component that takes a prop either as a value or as a slot renders it with set:html — or dangerouslySetInnerHTML, or v-html — and what is stored is a string of markup. The closest kind that existed was richtext, which means a document, so the property panel rendered the markup literally:
Nobody can edit that without breaking it, and editing it anyway writes the broken version back to your source. html stores the string your template renders, edits it as a document, and converts both ways — preserving the elements and attributes it cannot model rather than dropping them.
See when the stored value is HTML.

New: stringList and imageList field kinds

Both were declarable and neither was drawn — a declared, schema-valid field reached the property panel as nothing at all. They now have controls: rows of inputs for a string array, and the image widget per row for an image array, so the asset picker and alt text behave the same whether an image is on its own or one of twelve.
What you have to change. registerFieldTable now refuses a kind it does not know instead of silently emitting text. If your field table has a typo in a kind, registration throws and names it. That is the failure this release converts from invisible into loud — the field had been reaching the panel as nothing.

New: the editor API and page render without Next.js

Three things createSitePage used to decide and throw away are now values any host can consume: resolvePageRender (page / not-found / draft-unavailable), decideEditorRewrite, and renderPageMetadata. createEditorApiHandlerCore and site-sdk/routes/core are the editor API with no framework in them, and preview-adapter/bridge-controller is the live-preview protocol with no React — which is why React is no longer a mandatory peer of preview-adapter. Nothing about the Next.js path changes. These are the seams every non-Next host was reimplementing from the source.

Fixed: a list the property panel could not draw

A field table declaring { kind: 'list' } emitted a schema saying only “array of objects”, so the panel showed neither rows nor an Add control — and 0.7.0’s check that a new row must name its type was inert for the same reason. The metadata was correct all along; the panel re-derives from the manifest’s JSON schema, and a key the schema does not expose is discarded rather than refined.

Fixed: the editor could push one site’s pages under another site’s id

The editor bootstrapped its draft before the site registry answered, so for every site but the build-time default, the first push carried the default site’s pages — which then stayed, counted in the Publish badge, one click from being written to your repository. The editor now waits for the registry, and /draft/bootstrap refuses a push whose stated origin contradicts the site’s registered previewUrl.
What you have to change. Nothing, unless you drive /draft/bootstrap yourself. Only a contradiction is refused: a caller that sends no origin, and a site registered without a previewUrl, are both unaffected.

Fixed: a publish diff describing a different site

When a site’s /api/editor/pages did not mention a siteConfig — an Astro site answers {pages} and nothing else — the bundled demo’s header config was used to fill the gap, so the publish diff reported the demo’s name, logo and navigation as your “before”. Absent now means “no header change”, which is what it meant.

Also in 0.8.0

  • site-sdk/draft/core no longer throws at module load where Next.js is not installed. It imported one four-line helper from the module beside it, whose first line is import { draftMode, cookies } from "next/headers".
  • site-sdk and the CLI no longer ship compiled test modules in their tarballs.
  • The demo content the editor seeds in demo mode is one coherent site again, rather than sediment from old chat sessions.
  • Package manifests no longer link to a repository that answers 404. There is no public repository, so npm renders no Repository link at all; bugs points here.

0.7.0

Breaking: add_item requires a list row’s discriminator

A polymorphic list declares which prop names a row’s type and which types it admits. add_item used to accept a row that left that prop out. It now rejects it, naming the admissible types.
What you have to change. If you call POST /ops with add_item by hand — from a test, a script or an agent — check that every new row for a polymorphic list sets its discriminator prop. A row that omits it used to be created and then fail downstream, or write a typeless row into your CMS.Only omission is rejected. A value that names no declared branch is still accepted, because your manifest may enumerate fewer types than your CMS has. Chat-driven edits are unaffected — the planner always sets it.

New: field-table lenses for CMS-backed sites

Four things have to agree about every block on a CMS-backed site: the Zod schema an AI edit is validated against, the metadata the property panel draws with, the projection that turns a CMS document into props, and the merge that writes edited props back. Written separately, they disagree within a week. @avocadostudio-ai/site-sdk/lens derives all four from one table:
Packs ship for Storyblok (site-sdk/lens/storyblok) and Sanity (site-sdk/lens/sanity). Another CMS answers six questions and inherits the rest. roundTrip is part of the API: run a projection through its own inverse over your real content, and anything that moves is a codec that is not its own inverse for some value in your documents — invisible in the editor, and visible later as a publish wanting to rewrite pages nobody opened. See the field table guide for the shape, the wiring and the two write rules.
One sharp edge. localized: false means the bare key, not the default language’s path. Those coincide on a CMS that localises into a suffixed sibling and diverge on one that localises into an object under the key. There is deliberately no implicit fallback — a read that fell back would pair with a write that did not, which is how a lens corrupts a document.

New: keep third-party scripts out of the preview

isEditorRender() tells a layout whether it is rendering inside the editor’s preview iframe. A layout receives no searchParams, so consent banners, analytics and chat widgets could not be gated the way a page can — they loaded inside the preview and sat on top of the content being edited. The preview rewrite now stamps a header a layout can read.

Fixed: the optional peer dependency from 0.6.0, completed

0.6.0 declared @anthropic-ai/claude-agent-sdk as an optional peer of orchestrator-core — twice, in two blocks of the same manifest, and JSON parsing keeps the last one. The 245 MB really did leave library-mode installs, but a consumer of orchestrator-core/agent/* got a bare MODULE_NOT_FOUND instead of a package manager naming what to install. Fixed in 0.7.0.

Also in 0.7.0

  • editableScopeProps({ display: "contents" }), for a list whose rows map straight into a flex column and have no ancestor to hang an editable scope on. Off by default — a row already inside an <li> should carry the scope there.
The bundled editor did not change in this release, so the CLI’s editor bytes are 0.6.0’s. The package moves to 0.7.0 with the rest for the lockstep reason above.

0.6.0

Breaking: the Claude Agent SDK is an optional peer, not a dependency

@anthropic-ai/claude-agent-sdk used to be a dependency of orchestrator-core, which library mode puts into your public site’s dependency tree. The SDK is 4 MB; its platform package is a 245 MB binary — larger than every other dependency put together.
What you have to change. Nothing, if you mount createOrchestrator and nothing else — that path never imports it, and your install gets smaller.If you import @avocadostudio-ai/orchestrator-core/agent/sites-agent-tools or .../migration/migration-tools, install @anthropic-ai/claude-agent-sdk alongside Avocado yourself. The standalone orchestrator declares it directly and is unaffected.
The rest of that finding was “state the number, so nobody discovers it at deploy time”. The library-mode install footprint, per dependency rather than as one total, because two of the five are native and the figure differs by platform: Roughly 66 MB on darwin-arm64. If the footprint decides it for you, the standalone orchestrator is the other shape.

Breaking: internal props now appear in the field manifest

resolveManifestFieldMeta used to omit _-prefixed props. It now emits them as fields carrying internal: true.
What you have to change. If you iterate meta.fields to build your own UI, skip entries where internal === true. Everything Avocado ships already does. If you use the built-in property panel, there is nothing to do.

New: kind: "reference" for CMS references

A CMS stores an internal link as an object — a Storyblok story link, a Contentful entry link, a Sanity reference — and renders it to a different href per language. Declaring such a prop as kind: "link" flattens the object to an href, and two things go wrong. The publish diff reports the reference as changed on every page forever, because the rendered href never equals the stored object. And writing the href back replaces the pointer with a hardcoded URL: the page renders identically, and the link silently stops following renames, which is the one thing the reference was for. kind: "reference" is deliberately opaque. The panel shows where it points and offers no control. The planner is not told the prop exists, and an update_props naming one is dropped with a note saying the change belongs in the CMS. A new list row gets null rather than placeholder text. Use referenceLabelKey to name a readable key so the panel shows something better than an id. There is no reference picker. Re-pointing a reference needs your CMS’s own document ids, which only your integration has.

New: Storyblok rich text

fromStoryblok and toStoryblok join the converters in @avocadostudio-ai/richtext, which now covers four CMSes: Storyblok, Contentful, Sanity Portable Text and Strapi. Without a converter, the cheap thing to do with a rich-text field is flatten it to a string, and you lose every mark, link and list on the first publish with nothing logging it.

New: context.baseline replaces context.published

The publish diff’s baseline was always your CMS’s draft content, not the live site. The name said otherwise, and integrators either designed around a problem that did not exist or took a second read of the published perspective and created one.
What you should change. Read context.baseline in your publish handler. context.published still holds the same array and still works — it is deprecated, not removed. undefined means “no baseline available” and never “the site was empty”; publishing every field on that assumption is the overwrite a baseline exists to prevent.

Fixed in 0.6.0

  • Every successful library-mode publish was reported as a failure. The response carried no status field, and the editor reads exactly that field before its error branch. A CMS publish that had already written every document told the person who pressed the button “Failed to trigger publish.”, over HTTP 200.
  • Avocado’s own block migrations ran against sites that had re-registered a built-in name. Registering your own Hero — which the CMS adapter docs tell you to do — used to invite Avocado’s migrations to invent props on it: a placeholder image URL, an English alt string on a German page, a variant overwritten with "default". None of it showed in the preview, because your renderer ignores props it does not use. It showed at publish, on pages nobody opened. Whoever registers a name last now owns it.
  • Generated list-row ids leaked into publish diffs. Avocado stamps a stable id on every declared list row so the panel can reorder and the planner can address a row by name. It lives in props, so an adapter comparing its draft against freshly read CMS content saw every block with a list as changed, from the first load and forever — a one-field edit could produce a publish that wanted to rewrite every document with a list on it. withoutGeneratedItemIds, from site-sdk/publish, is the inverse. It strips only the ids Avocado generated, so a row carrying your CMS’s own key keeps it.
  • trailingSlashRedirect and editorPreviewRewrite are exported from both site-sdk/proxy and site-sdk/middleware. The hand-written trailing-slash redirect, built from request.nextUrl.clone() as every Next example invites, strips the slash it just added and sends every page into an infinite redirect — visible only in a browser, since curl without -L sees one ordinary 308.
  • A bare data-editable-target on a list row resolved against the enclosing block, so an edit to a headline inside a column patched a prop the section does not have. Silent and wrong, rather than silent and absent.
  • panelCoverage no longer reports a CMS’s own _uid as an orphan prop — on a CMS that stamps one, that was a finding per block per page, burying every real one.
  • notes?: string[] on CmsPublishResult, so a dry run, a queue or a review-before-write publisher can say what it did. It had one channel before, unsupported, which renders as failure.
  • Two more multilingual traps documented. A container is not localised — the fields inside it are. And on a CMS that localises per field, the component schema decides in both directions, so a document can keep a translated value your site has not rendered in years. See multilingual, rules 3 and 4.

0.5.1

Three defects found by integrating a real site. No breaking changes.
  • Alt text outranks the image filename as a list-row label, and a list field inside a list row is no longer reported as unmarked. Both were coverage findings a site could only clear by renaming its image files or by marking an element that draws nothing.
  • A relative image URL is resolved against the site’s own origin before it goes to a vision model, and a URL source is only ever https. Library-mode image selection used to fail the whole chat turn with the provider’s own 400.
  • registerBlock no longer warns that a correct polymorphic list is unbacked, and a row whose only content is rich text is labelled by its first text node rather than Item N or [object Object].

0.5.0

Minor rather than patch: this adds entry points and changes behaviour an existing integration can observe.

New entry points

The install line every guide shipped could not satisfy the imports those same guides required — @avocadostudio-ai/shared is a dependency of the SDK, not of your site, so under pnpm the documented import did not resolve at all.
What you should change. Import from @avocadostudio-ai/site-sdk/* rather than from @avocadostudio-ai/shared or @avocadostudio-ai/preview-adapter. Installing the SDK alone is then enough, which is what every guide told you.site-sdk/markers also matters for bundle size: the marker helpers used to ship from the same entry as the editor overlay, which added 66 kB of First Load JS to every public page that marked up a shared component.

New: panelCoverage

editableCoverage asks whether the rendered page offers each declared field. Nothing asked whether the property panel is intelligible — so a site could compile cleanly, serve a valid manifest, pass every marker check, and still hand a person a panel they cannot use. panelCoverage resolves metadata and rows through the same functions the panel itself uses, and compares them against your real content. No browser, no screenshot, no model call. Seven findings, ordered so the cause is read before its symptoms: colliding_type, incomplete_polymorphism, unmatched_branch, unlabelled_row, orphan_prop, filename_row_label, phantom_field. Three surfaces, because an adopter and an agent need different things: site-sdk/coverage for your own check script, avocado-check-editing-surface over MCP for an agent to call before reporting an integration done, and a formatted report for a human. See coverage checks. FieldMeta also gained panelOnly, which takes fields nothing can draw out of the coverage denominator — so 100% is reachable and anything less is actionable.

Behaviour an existing integration will notice

  • The property panel now prefers your field metadata over Avocado’s built-in registry on a colliding type name. If you register your own Hero, you were being shown Avocado’s labels for it — one integration collides on seven of its eight types and read Left column items where it had declared Left column. A field your manifest declares explicitly now wins; a field it only derives from its JSON schema still takes the registry’s richer entry. The merge also stopped silently dropping discriminator and itemFieldsByType, which meant polymorphic lists were measured against the union of all branches and rows fell back to Item 4, Item 5.
  • A mounted createOrchestrator publishes its own address. The draft fetch used to default to a standalone orchestrator on :4200 that a library-mode site does not run — silent when nothing is listening there, and worse when something is, because the preview then renders a foreign process’s content and answers 200.
  • withAvocado writes the framing CSP pair from the same allowlist the editor API uses, so you no longer hand-write frame-ancestors next to your CORS origin. Getting it wrong failed in two unrelated-looking ways: the Sites page reporting your site offline (CORS), and a blank rectangle where the preview should be (CSP). Your own headers() come first and win; framing: false opts out.
  • The legacy ../../.data database path now requires proof of workspace membership. It used to ask only whether that file exists — and since the original bug is what created it, one mistaken write re-targeted every project under the same parent directory, forever. One integration’s drafts landed in a database already holding twelve sessions from four unrelated projects, while reporting persistence: { ok: true } throughout.
  • A successful next build no longer prints a red error about production auth. It was describing a request that was not being served, on a machine that was not the deployment.

Earlier releases

0.4.0 and 0.3.3 are recorded in CHANGELOG.md at the root of the repository. Highlights: files as a content type alongside images, editableCoverage, publishing a subset of a draft, discarding changes from the version log (0.4.0); and site health checks, a durable store, and link as a field kind (0.3.3).

Upgrading

1

Move every package at once

Set all @avocadostudio-ai/* dependencies to the same version. A mixed set will resolve, then fail at runtime in ways that point nowhere useful.
2

Read the breaking notes between your version and the target

Every breaking change on this page carries a “what you have to change” note. Minor bumps are where they live.
3

Re-run your coverage checks

editableCoverage and panelCoverage are the fastest way to see whether an upgrade moved something in your integration. See coverage checks.
4

Round-trip your projection, if you have a CMS

If you use a lens or a hand-written projection, run roundTrip over real content before you publish. A codec that is not its own inverse is invisible in the editor and shows up as a publish rewriting pages nobody opened.