@lacneu/wix-openclaw 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,514 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-08-29
11
+
12
+ Ten more `wix_seo_*` tools, still **off by default**, close the other half of
13
+ the SEO gap: 0.3.x could audit a site's tags and never correct them. Writing
14
+ them is a different destructiveness from deleting a redirect — a tag write
15
+ replaces **the previous value of what it writes** — so the guard is different
16
+ too, and most of this entry is about that.
17
+
18
+ ### Added — SEO: writing tags, and the patterns behind them
19
+
20
+ > Same warning as the audit tools: these endpoints are in Developer Preview,
21
+ > Wix states they should not be used on live sites, and **every tool here
22
+ > replaces what it writes**. They register only with `enableSeoTools: true`.
23
+
24
+ Ten tools complete the SEO surface: you could audit tags but not correct them.
25
+
26
+ **Per-item and site-wide tags.** `wix_seo_set_item_tags`,
27
+ `wix_seo_bulk_set_item_tags`, `wix_seo_reset_item_tags` and
28
+ `wix_seo_set_site_tags`.
29
+
30
+ **Patterns.** `wix_seo_list_patterns`, `wix_seo_get_pattern`,
31
+ `wix_seo_list_pattern_variables` (read-only) plus `wix_seo_set_pattern`,
32
+ `wix_seo_create_pattern` and `wix_seo_reset_pattern`.
33
+
34
+ ### The destructiveness is a different shape, so the guard is too
35
+
36
+ A redirect delete destroys another row — it can be listed and named. A tag write
37
+ destroys **the previous value of what it writes**: `tags` replaces the item's set
38
+ in full, so a caller that sets a title wipes the description it never mentioned,
39
+ gets exactly what it asked for, and loses something it never saw. That is this
40
+ half's "redirects do not chain".
41
+
42
+ The pre-flight therefore reports what **disappears** — the tags held today whose
43
+ slot the incoming set does not fill — never an echo of the input. A caller
44
+ reading its own request back learns nothing. Nothing is lost, nothing is
45
+ refused: the ceremony exists for the unannounced loss, not for its own sake.
46
+
47
+ A pattern write earns the heaviest pre-flight in the plugin. It re-renders
48
+ **every page of its type**, it has no revision, and the Wix dashboard writes to
49
+ the same object — last write wins. So the refusal reports the page count
50
+ downstream alongside the templates, and the token is bound to the pattern read
51
+ immediately before — **and to that page count**, because the radius is part of
52
+ what was accepted: a token minted while a type held twelve pages does not
53
+ authorise the same write once it holds four hundred.
54
+
55
+ Every refusal in this family carries the **value** of what it names, not only
56
+ its slot.
57
+ Editing `{{title}}` into `{{title}} — Ataraxis` loses no tag, so a report by
58
+ slot alone read identically before and after — an operator signing a rewrite of
59
+ every page of a type without seeing either version.
60
+
61
+ ### Three contract facts that live in code, not in prose
62
+
63
+ - **`fieldMask` is a comma-separated STRING over REST.** The SDK takes an array,
64
+ which is what a model reaches for. It is built here, never accepted.
65
+ - **`language` is actively dropped.** Tags can only be written for the item's
66
+ primary language, and sending one fails `LANGUAGE_NOT_SUPPORTED` *even when
67
+ the field mask omits it* — so not declaring the field is not enough, TypeBox
68
+ passes undeclared properties through.
69
+ - **Site SEO Tags accepts only `meta`.** Wix rejects `title`, `script` and
70
+ `link` there despite the shared tag shape listing them, and there is no way to
71
+ set site-wide structured data through any Wix API — a per-page `script` tag is
72
+ the route.
73
+
74
+ `BLOG_POST` bulk writes are capped at 20 entries, not 100, and Wix rejects the
75
+ **whole call** past an item type's limit — a 40-post batch would write nothing —
76
+ so the cap is enforced before the request leaves.
77
+
78
+ ### The `publish` split, stated where a model will read it
79
+
80
+ For a static page, `publish: true` updates **only** the published revision. The
81
+ read tools always return the **saved** one, so after such a write they keep
82
+ showing the old values — that is not a failed write, and re-sending will not
83
+ change it. The method's own response is not a read of the published page either.
84
+ A caller following the description literally converges instead of retrying.
85
+
86
+ ## [0.3.1] - 2026-08-28
87
+
88
+ **The version that actually reached npm for the 0.3 line.** `v0.3.0` was
89
+ tagged, failed to compile in the release workflow and was never published, so
90
+ `0.3.1` carries the whole 0.3.0 entry below plus the fix that unblocked it.
91
+
92
+ ### Fixed
93
+
94
+ - **The build imported a type from a subpath that does not exist across the
95
+ supported range.** `openclaw/plugin-sdk/agent-core` only appeared in
96
+ 2026.5.28, while this plugin declares `>=2026.5.0`, so CI failed on the lower
97
+ bound. The shared tool-result type is declared locally instead — verified at
98
+ both ends of the supported range rather than at the version that happened to
99
+ be installed.
100
+
101
+ ## [0.3.0] - 2026-08-28
102
+
103
+ Nine `wix_seo_*` tools, **off by default**, close the gap that made this plugin
104
+ able to manage a site's *content* but not to look at its SEO. Turn them on with
105
+ `enableSeoTools: true`.
106
+
107
+ **One fix here affects every existing install, whether or not you want the SEO
108
+ tools:** approval prompts were too long for the gateway to accept, so every
109
+ approval-gated tool was blocked without the operator ever being asked. See
110
+ *approval prompts were too long for the gateway to show at all* below.
111
+
112
+ The three SEO read paths were verified against a live Wix site; the four writing
113
+ paths were deliberately left unexercised. Everything else recorded under this
114
+ version is work on code that had not shipped before — it is written down because
115
+ the reasoning is the point, not because anyone was affected by it.
116
+
117
+ ### Added — SEO: auditing a site, and managing its URL redirects
118
+
119
+ > **These tools are OFF by default and must be opted into with
120
+ > `enableSeoTools: true`.** Every Wix endpoint behind them is in Developer
121
+ > Preview: Wix may change them at any time and states they shouldn't be used on
122
+ > live sites. Four of them also write irreversibly the moment they succeed.
123
+
124
+ **Permission.** All nine sit behind a single Wix scope,
125
+ `SCOPE.PROMOTE.MANAGE-SEO` — shown untranslated in the Wix UI, under *All site
126
+ permissions*, between *Social Posts* and *Import orders*. Wix publishes no
127
+ read-only SEO scope, so a key that can audit a site can also delete its
128
+ redirects: the read/write boundary is `enableSeoTools` plus the approvals, not
129
+ the key. The three read paths were verified live against a real site; the four
130
+ writing paths were deliberately left unexercised.
131
+
132
+ Nine tools under `wix_seo_*`, closing the gap that made the plugin able to
133
+ manage a site's *content* but not to look at its SEO.
134
+
135
+ **Audit.** `wix_seo_list_item_tags` is the entry point: pass `STATIC_PAGE` and
136
+ it enumerates the site's pages, each with the tags it sets itself, whether it
137
+ sets any (`hasOverride`), and the tags it is expected to render with — every one
138
+ marked with where it came from (the item, its host page, a customised pattern,
139
+ Wix's own pattern, or the site). Other item types are `BLOG_POST`,
140
+ `STORES_PRODUCT` and `WIX_DATA_PAGE_ITEM-{pageId}`. `wix_seo_get_item_tags` and
141
+ `wix_seo_get_site_tags` read one item and the site-wide tags.
142
+
143
+ **Redirects.** List, get, create, delete, and both bulk forms.
144
+
145
+ ### Two limits stated in the tool descriptions, not just the docs
146
+
147
+ *A static page reads as its SAVED revision, never the published one*, and
148
+ `publishStatus` is always `PUBLISH_STATUS_UNSPECIFIED` for one — Wix exposes no
149
+ path to read the published revision back. The description says so, because an
150
+ agent that doesn't know it will report on a draft as though it were the live
151
+ page. `resolvedTags` likewise omits whatever site code and apps add at render
152
+ time, so it is not the page's rendered `<head>`.
153
+
154
+ ### Changed — `wix_seo_create_redirect` is approval-gated, like the deletes
155
+
156
+ Creating a redirect is destructive by construction. Redirects do not chain: when
157
+ an existing redirect **starts at** the path a new one points to, that existing
158
+ redirect is deleted so the create can proceed — no flag asked for, and no way
159
+ back. `options.forceReplace` deletes a second one. Gating by verb would have
160
+ left the most damaging call in the set ungated.
161
+
162
+ ### Fixed — approval prompts were too long for the gateway to show at all
163
+
164
+ The gateway declares an approval's `title` as 1–80 characters and its
165
+ `description` as 1–256 (`PluginApprovalRequestParamsSchema`). The prompt this
166
+ plugin built embedded a 600-character parameter preview under a paragraph of
167
+ boilerplate, so it could not fit — and an over-long request is not truncated,
168
+ it fails validation. **Every approval-gated tool was affected, not only the new
169
+ ones**: the operator was never asked, and the call was blocked.
170
+
171
+ The description is now BUILT to that budget rather than written and hoped for,
172
+ ordered by what decides a refusal: the irreversible effect first, then the site
173
+ the call actually lands on (the old preview stripped `siteId`, so on a
174
+ multi-site key the operator never saw which tenant), then the parameters that
175
+ matter, then everything else until the budget runs out.
176
+
177
+ A redirect is shown with its SCOPE — exact or group, and its language or `all`
178
+ — because the paths alone made an exact French-only rule and a global group rule
179
+ over a whole branch read identically, while the second can make far more pages
180
+ unreachable. That applies to a bulk create's members too, which had been going
181
+ through the generic JSON path: up to 500 group rules over whole branches read
182
+ the same as 500 exact ones. A batch also reports a count PER SCOPE over every
183
+ member, and its sample leads with the widest rules. Its size and fingerprint are
184
+ their own part of the prompt, so a long scope breakdown can be dropped without
185
+ taking the batch's identity with it — sampling in request order
186
+ put three exact rules on screen while the global group ones sat inside an
187
+ opaque fingerprint. A list reports its size, a few of its members and a stable
188
+ fingerprint of the whole set — reporting only a count made two bulk deletes of completely
189
+ different GUIDs read identically. Long values are shortened in the middle, so
190
+ two paths sharing a prefix stay distinguishable, and a value that still cannot
191
+ fit is skipped rather than ending the rendering, which is how `from` and `to`
192
+ could disappear from a prompt entirely. A test asserts both limits for every
193
+ gated tool, at the largest values Wix itself accepts.
194
+
195
+ ### Changed — a redirect create REFUSES rather than silently destroying
196
+
197
+ Creating a redirect deletes an existing one that starts at the path the new one
198
+ points to — redirects do not chain — and `forceReplace` deletes the one holding
199
+ the same `from` path. Neither is reported by the API, and telling the agent to
200
+ check first only made it advice: a direct call, or a model that skipped the
201
+ instruction, still lost a redirect nobody had named.
202
+
203
+ `wix_seo_create_redirect` and `wix_seo_bulk_create_redirects` now list the
204
+ site's redirects themselves and refuse, naming each redirect they would delete
205
+ and why, until the caller repeats those GUIDs back in `acknowledgeDeletions`.
206
+ Trailing-slash and case differences count as the same path, as Wix treats them.
207
+ A create that destroys nothing goes straight through.
208
+
209
+ ### Changed — the destructive preflight compares what Wix STORES, and fails closed
210
+
211
+ Three ways the check could be walked around, all closed:
212
+
213
+ - **Language prefixes.** Wix stores a language-scoped redirect without its
214
+ prefix — a `fr` redirect from `/fr/about` comes back as `/about`. Comparing
215
+ the caller's path against the stored one found nothing, so a language-scoped
216
+ deletion went through unannounced. Paths are now normalised the way Wix
217
+ stores them, against the language of BOTH sides: a global create pointing at
218
+ `/fr/blog` is aiming at the public path of a `fr` redirect stored as `/blog`,
219
+ and normalising only by the candidate's own language (none, there) left the
220
+ two looking unrelated. Scopes that cannot collide are skipped rather than
221
+ flagged.
222
+ - **An unreadable list.** Anything that wasn't a `redirects` array — a 2xx the
223
+ client couldn't parse, a schema change, a proxy's own page — read as "this
224
+ site has no redirects", and the destructive write went out on the strength of
225
+ it. The call now refuses. The one answer a safety check must never invent is
226
+ the reassuring one.
227
+ - **Concurrency.** The check and the write are two requests, so two concurrent
228
+ creates could each read a state without the other's redirect and the second to
229
+ land would delete the first's. Destructive writes are now serialised per
230
+ EFFECTIVE site — keying on the raw argument put an omitted `siteId` and the
231
+ same site named explicitly into two different queues, so the calls the lock
232
+ exists to order ran side by side. A change made in the Wix dashboard between
233
+ the read and the write is still possible — Wix offers no precondition to close
234
+ that — and the tool descriptions say so rather than promising otherwise.
235
+ - **An entry the plugin cannot read.** Validating only that `redirects` was an
236
+ array closed the check at the top and left it open underneath: a preview
237
+ rename to `source` passes an array test, every `from` reads as undefined, no
238
+ conflict is found, and the write goes out on a list nobody understood. Each
239
+ entry is now checked before anything is computed from it.
240
+
241
+ ### Changed — an acknowledgement is tied to the redirect's CURRENT state
242
+
243
+ `acknowledgeDeletions` took bare GUIDs, which were guessable: a caller could
244
+ name one on the first call and never be shown what it destroys. Worse, an id
245
+ outlives the redirect behind it — Wix lets a create supply its own GUID, so
246
+ between a refusal and the retry the same id can point somewhere else, and the
247
+ acknowledgement would authorise deleting something the user never saw.
248
+
249
+ The refusal now hands back a `token` derived from the redirect's id, paths,
250
+ language and group flag, **and the effective site**, HMAC'd with a secret minted
251
+ once per process. It cannot be produced without having listed, it stops matching
252
+ the moment the redirect changes — in which case the call refuses again with the
253
+ new one — and a token earned on one site does not unlock its twin on another.
254
+ Two allowed sites can hold an identical redirect, since Wix accepts
255
+ caller-supplied GUIDs, and a cross-site replay would have authorised a deletion
256
+ on a tenant whose state nobody was shown.
257
+
258
+ ### Changed — the two kinds of bulk loop are told apart, and only one is guarded
259
+
260
+ An intermediate version of this work refused any batch whose members looped on
261
+ each other, on the assumption that one would delete the other. The reference says
262
+ otherwise: an item closing a loop with an EARLIER item of the same request fails
263
+ on its own with `REDIRECT_LOOP` and the rest of the batch goes ahead — nothing
264
+ is deleted. That guard blocked migrations Wix would have applied in part, and
265
+ told the operator redirects would be lost when none would. It is gone.
266
+
267
+ What remains guarded is the deletion the reference does document: an item that
268
+ closes a loop with a redirect ALREADY ON THE SITE is created and that redirect is
269
+ deleted. The tool description now states both cases separately.
270
+
271
+ The declared bound also drops from 500 to **100** for bulk create, which is what
272
+ the method page and the Developer Preview index both say. Bulk delete stays 500.
273
+
274
+ ### Fixed — irreversible SEO writes were being replayed
275
+
276
+ The HTTP client retries 429/502/503/504 twice. A create that Wix applied before
277
+ answering 503 — deleting a loop-closing redirect on the way — was then applied
278
+ again, and neither answer says which happened. `WixRequestOptions` gained
279
+ `retry`, and all four writes set it to `false` — a delete Wix applied before
280
+ answering 503 comes back 404 on the replay, and the plugin would then report a
281
+ failure for a permanent deletion that did happen: the error surfaces and the caller
282
+ re-reads, instead of a second irreversible write nobody asked for.
283
+
284
+ ### Fixed — an unsupported field rode through to Wix
285
+
286
+ TypeBox objects accept properties a schema does not mention, so a per-item
287
+ `options.forceReplace` on a bulk create survived validation and reached Wix —
288
+ while the preflight had been run with `forceReplace: false` and the approval
289
+ prompt never showed the nested flag. Wix deletes the redirect holding that
290
+ `from` path. Both creates now send a redirect rebuilt from the fields the plugin
291
+ actually supports; `forceReplace` is honoured only as the single create's own
292
+ top-level parameter, where the preflight accounts for it. The stripping is in
293
+ the code rather than left to the schema, because `execute` can be called
294
+ directly.
295
+
296
+ ### Changed — a delete names a redirect, not just an id
297
+
298
+ `wix_seo_delete_redirect` sent the DELETE straight from a GUID. Wix accepts
299
+ caller-supplied GUIDs, so between a listing, the operator's approval and the
300
+ call, the same id can name a different redirect — and the prompt showed nothing
301
+ but the id. The redirect is now re-read and the call refuses, showing its paths,
302
+ language and scope, until the `token` from that refusal comes back. Same protocol
303
+ as the creates, and the same staleness: if the redirect changed, the token no
304
+ longer matches. The bulk delete follows it too — it had been sending up to 500
305
+ GUIDs straight through, with an approval prompt carrying ids and no paths.
306
+
307
+ ### Fixed — the SEO opt-in is reachable from the configurator
308
+
309
+ `enableSeoTools` defaults to false and had no `uiHints` entry, and OpenClaw's
310
+ configuration flow prompts only for the properties declared there. The switch
311
+ that registers the nine SEO tools was therefore unreachable without hand-editing
312
+ the config file — the feature shipped off, with no supported way to turn it on.
313
+ A manifest test now requires a `uiHints` entry for every configurable property.
314
+
315
+ ### Fixed — a value cannot paint its own lines in the approval prompt
316
+
317
+ The description is rendered as a line-structured message, and the redirect
318
+ rendering — which exists to be more readable than JSON — put paths, languages
319
+ and parameter names into it verbatim. A `from` of `/a\nsite: attacker-site` drew
320
+ a second field naming a site the call never touched, on the one surface whose
321
+ purpose is a human deciding about an irreversible write. The generic branch had
322
+ been safe only because `JSON.stringify` escapes controls. Controls are now made
323
+ visible before any truncation, at every insertion point, and `language` is
324
+ bounded like the paths.
325
+
326
+ ### Fixed — an `options` shape this version cannot read is refused
327
+
328
+ `options: []` is `typeof "object"` and passed the pre-flight, as did a drift
329
+ renaming the flag to `isGroupRedirect`. Either left a group redirect described
330
+ to the operator as exact and its acknowledgement token blind to the scope it
331
+ really has — the two things the token exists to bind. `options` must now be a
332
+ plain object carrying no key but `groupRedirect`.
333
+
334
+ ### Fixed — the prompt names both ways a create destroys
335
+
336
+ A redirect takes precedence over a real page at the same path, so creating one
337
+ from a path that still serves a page makes that page unreachable — no existing
338
+ redirect involved, and nothing the pre-flight can see, since it reads redirects
339
+ rather than pages. Both create prompts warned only about deleting a redirect.
340
+ They now name the hidden page too, which is the operator's only warning about it.
341
+
342
+ The `Manage SEO` scope also moved out of the README's "minimum required" list.
343
+ In the default configuration `enableSeoTools` is false and the SEO tools are not
344
+ registered, so granting a scope that permits deleting redirects buys nothing —
345
+ it is listed as conditional on turning them on.
346
+
347
+ ### Fixed — a create that points under another language's prefix
348
+
349
+ The language filter ran before any path comparison, for both checks. Two
350
+ languages are separate URL namespaces, so that is right for a create pointing
351
+ inside its own — refusing there would block a create that destroys nothing. But
352
+ a caller can write the other language's prefix explicitly: an `en` redirect
353
+ pointing at `/fr/blog` names the public entry of the `fr` redirect Wix stores as
354
+ `/blog`, and the filter dropped that pair before the comparison that would have
355
+ recognised it, so the deletion went unannounced. That case is now detected; none
356
+ is taken away. Two related questions upstream does not answer — whether a group
357
+ redirect resolves a loop for destinations merely UNDER its `from`, and how the
358
+ two languages compare in general — are recorded in the code as known bounds
359
+ rather than guessed at, because settling them needs a create and a delete
360
+ against a live site.
361
+
362
+ A global redirect — one with no `language` — applies to every language, so it is
363
+ also entered at `/de/blog` and not only at `/blog`. Against such a rule the scope
364
+ comparison always "overlaps", which is precisely what kept the foreign-prefix
365
+ check from running, and a create pointing at `/de/blog` closed a loop with it
366
+ unannounced. The site's configured languages cannot be read here (that endpoint
367
+ needs a different scope and can answer 403), so a leading segment SHAPED like a
368
+ language tag is now recognised. It errs towards announcing: a `/fr/...` that is
369
+ really a content folder costs one acknowledgement, and the operator sees exactly
370
+ which redirect is named.
371
+
372
+ ### Fixed — the create describes the protocol it implements
373
+
374
+ Two sentences of `wix_seo_create_redirect`'s description sent a model down paths
375
+ the code refuses: it named Wix's own `options.forceReplace` — the nested flag
376
+ this plugin strips, so the create would have failed as if the flag were never
377
+ set — and it asked for the doomed redirects' GUIDs, where the acknowledgement
378
+ protocol takes the `token` from the refusal and rejects a bare id. The behaviour
379
+ was right throughout; only the contract a model reads before acting was wrong,
380
+ which no behavioural test could catch. Both are corrected, the bulk create says
381
+ `token` explicitly too, and a test now holds the descriptions to it.
382
+
383
+ ### Fixed — the pre-flight trusts a listing only when it understands all of it
384
+
385
+ The destructive pre-flight refused a redirect listing whose entries had no
386
+ string `from`, but the acknowledgement token also binds `id`, `to`, `language`
387
+ and `groupRedirect`, substituting a default for whatever is absent. A drift that
388
+ renamed or dropped `to` — a stated possibility on a Developer Preview API —
389
+ therefore degraded the token into one that no longer changed when the redirect's
390
+ destination did, and a stale acknowledgement would have authorised a permanent
391
+ deletion. Every field the token binds is now checked, scope fields for shape.
392
+
393
+ ### Fixed — the write lock forgets, and the audit pages as the endpoint defines
394
+
395
+ `serializedBySite` kept one settled promise per site id it had ever been given.
396
+ The key is caller-supplied and queued BEFORE the whitelist rejects it, so a
397
+ long-lived gateway retained an entry for every string ever passed — the comment
398
+ already claimed the chain was dropped "once it is the last one"; now the code
399
+ does it, and only while it is still the tail.
400
+
401
+ `wix_seo_list_item_tags` advertised the shared GET paging envelope, `offset`
402
+ included, on an endpoint that pages by cursor alone. The schema is now
403
+ cursor-only AND the query is built from those two keys: narrowing a TypeBox
404
+ object does not strip what it omits, and `execute` can be called directly, so
405
+ an `offset` arriving at runtime would still have been forwarded.
406
+
407
+ The smoke test no longer probes the SEO endpoints unless `WIX_SMOKE_SEO=1`.
408
+ They would otherwise have demanded `SCOPE.PROMOTE.MANAGE-SEO` — a scope that
409
+ also permits deleting redirects — of every key, including the default
410
+ configuration where the SEO tools are not even registered.
411
+
412
+ ### Fixed — a refusal names the whole loss, and the protocol converges
413
+
414
+ Every refusal listed only the redirects still lacking an acknowledgement, while
415
+ `howToProceed` asked for "every token above". The phrase therefore named a
416
+ different set on each round: a caller that REPLACED its token list rather than
417
+ extending it alternated between halves and never got through, so a legitimate
418
+ multi-redirect deletion was impossible. The shrinking list also understated the
419
+ loss — the second prompt showed fewer deletions than the first. All three
420
+ multi-target refusals (single create, bulk create, bulk delete) now list the
421
+ complete set, each entry carrying `acknowledged: true | false`.
422
+
423
+ ### Fixed — an omitted parameter is always marked as omitted
424
+
425
+ The approval description reserved room for the ` · …` marker only until the
426
+ first part was dropped, and stopped reserving exactly when the marker had become
427
+ certain. Parts after that drop filled the 256-character budget to the brim and
428
+ the final truncation cut the marker off, so a prompt that had silently dropped a
429
+ parameter read as complete — on the destructive calls where it matters. Room is
430
+ now reserved from the first drop onwards, and trailing parts are surrendered if
431
+ that is what it takes for the marker to survive.
432
+
433
+ The approval fingerprint moved from a 32-bit FNV to SHA-256 truncated to 128
434
+ bits, over a key-sorted serialisation. The old one was small enough to collide
435
+ deliberately — two batches whose sampled members matched then produced identical
436
+ prompts for two different permanent deletions — and insertion-order
437
+ serialisation gave the same batch different identities depending on how its
438
+ fields happened to be written.
439
+
440
+ ### Fixed — SEO writes ignored cancellation
441
+
442
+ The tool factory hands each run an `AbortSignal` and the HTTP client honours it,
443
+ but the SEO tools never took it. A destructive write can sit behind another
444
+ site-write for as long as that one takes, so an irreversible POST could go out
445
+ after the runtime had already reported the operation cancelled. The signal is
446
+ now checked after the queue and again immediately before the write, and passed
447
+ to every request — the five READ tools had never declared it either, so a
448
+ cancelled audit went on waiting, retrying and spending quota. The retry backoff
449
+ is interruptible too: a plain timer meant an abandoned call still sat out the
450
+ whole wait, which `Retry-After` can make long, before noticing.
451
+
452
+ ### Fixed — the manifest tests were checking a list they maintained themselves
453
+
454
+ `test/manifest.test.ts` re-listed the tool domains instead of reading what the
455
+ plugin registers, so adding a whole domain left its guards passing over the new
456
+ tools without seeing them. Registration now goes through a single exported
457
+ `buildAllTools()` that the tests read, and two guards were added: `contracts.tools`
458
+ must name exactly the registered tools, and the manifest's `approvalRequired`
459
+ default must match the code's.
460
+
461
+ ### Fixed — the build no longer depends on a package nobody declared
462
+
463
+ `src/tools/_factory.ts` imported the `AgentToolResult` type from
464
+ `@mariozechner/pi-agent-core`, which was never a dependency of this plugin: it
465
+ arrived only because `openclaw` happened to pull it in. Upstream dropped it
466
+ (absent from 2026.6.x onwards), so the import resolved on a machine holding an
467
+ older tree and nowhere else — a fresh install compiled no more.
468
+
469
+ The shape is now declared in the plugin itself. `openclaw/plugin-sdk/agent-core`
470
+ re-exports the same type, but that subpath only exists from 2026.5.28 while this
471
+ plugin supports `>=2026.5.0`: importing it would have quietly dropped the bottom
472
+ of the declared range. The local declaration holds across the whole of it, and
473
+ the structural check still happens where it matters — at `registerTool`, against
474
+ whichever SDK is installed. Verified compiling at both ends of the range, 2026.5.2
475
+ and 2026.6.34.
476
+
477
+ Two related repairs came with it. The default export is now annotated
478
+ (`ReturnType<typeof definePluginEntry>`): its inferred type reached into an
479
+ openclaw build-internal chunk that TypeScript cannot name from a declaration
480
+ file (`TS2742`). And `package-lock.json`, stale at `0.1.1` since that release,
481
+ is regenerated — `npm ci` had been failing on every run since, silently falling
482
+ back to `npm install`, which is why a dependency that had quietly vanished
483
+ upstream went unnoticed until now.
484
+
485
+ ### Migration
486
+
487
+ For instance owners on `@lacneu/wix-openclaw@0.2.2`:
488
+
489
+ 1. `openclaw plugins update @lacneu/wix-openclaw` — **`update`, not
490
+ `install`**: `install` rewrites `plugins.entries.wix-openclaw` with the
491
+ manifest's defaults, discarding the API key, the site whitelist and any
492
+ `approvalRequired` you have tuned. `update` preserves them.
493
+ 2. Restart the gateway container. This release adds no environment variable, so
494
+ a `restart` is enough — no `--force-recreate` needed.
495
+ 3. Verify in the boot log:
496
+ `wix-openclaw: ready — 50 tool(s) registered`.
497
+
498
+ Fifty, not sixty-nine: the SEO tools are not registered until you opt in. Nothing
499
+ else changes, and the approval fix applies as soon as the plugin loads — a gated
500
+ tool that used to fail without asking will now put its prompt to the operator.
501
+
502
+ To add the SEO tools:
503
+
504
+ 1. Tick **Manage SEO** (`SCOPE.PROMOTE.MANAGE-SEO`) on the Wix API key used by
505
+ that instance. Its label
506
+ is untranslated in the Wix UI — it sits under *All site permissions*, between
507
+ *Social Posts* and *Import orders*. There is no read-only variant: the same
508
+ permission that allows the audit also allows deleting redirects.
509
+ 2. Set `enableSeoTools: true` — via the configurator, which prompts for it as
510
+ *Enable SEO tools (audit + URL redirects)*, or with
511
+ `openclaw config set plugins.entries.wix-openclaw.config.enableSeoTools true`.
512
+ `update` carries your existing config forward and the new field simply
513
+ defaults to `false`, so nothing turns on by surprise.
514
+ 3. Restart, and expect `69 tool(s) registered`.
515
+ 4. Read the Developer Preview warning at the top of this section before pointing
516
+ any of the four writing tools at a live site.
517
+
10
518
  ## [0.2.2] - 2026-05-03
11
519
 
12
520
  ### Fixed — OpenClaw 2026.5.x compatibility
@@ -236,5 +744,6 @@ workaround; await the upstream release.
236
744
  - Release workflow using **npm Trusted Publishing (OIDC)** — no `NPM_TOKEN`
237
745
  secret required, provenance attached automatically.
238
746
 
239
- [Unreleased]: https://github.com/OlivierNeu/wix-openclaw-plugin/compare/v0.1.0...HEAD
747
+ [Unreleased]: https://github.com/OlivierNeu/wix-openclaw-plugin/compare/v0.3.0...HEAD
748
+ [0.3.0]: https://github.com/OlivierNeu/wix-openclaw-plugin/compare/v0.2.2...v0.3.0
240
749
  [0.1.0]: https://github.com/OlivierNeu/wix-openclaw-plugin/releases/tag/v0.1.0