n8n-decanter 0.8.0 → 0.10.1

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,480 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.1] - 2026-08-16
11
+
12
+ ### Added
13
+
14
+ - **The live mirror now tells the agent when it overwrote your work.** It runs a
15
+ full `pull` after a structure edit, so it can replace an unpushed local code
16
+ edit with what is on the instance. It always warned about that — on stderr,
17
+ which is the one stream an MCP agent structurally cannot read, so the party
18
+ able to react never heard it. The warning now rides the **result of the
19
+ agent's next tool call**, naming the files and how to recover them from the
20
+ safety commit. Delivered once, never repeated.
21
+
22
+ Only on `mcp connect` (the transport `init` scaffolds). `mcp serve` pipes
23
+ upstream responses through untouched — including SSE — and buffering them to
24
+ inject an advisory line would break streaming for every response to deliver it
25
+ on some. On that transport the stderr warning stays the only signal.
26
+
27
+ - **`init` now says that its permission rules only bind the next session.** It
28
+ writes `.claude/settings.json` with the deny rules that keep an agent off
29
+ `.decanter.json`, `.env` and `push --force` — but agents read permission config
30
+ at startup, not on change, and `init` is normally run *from inside* the session
31
+ those rules are meant to constrain. They were silently inert until a restart,
32
+ and the docs mentioned a restart only for the skills plugin — so the rules that
33
+ actually gate the agent went unmentioned. Printed once, when the file is first
34
+ written; a re-init in a set-up directory stays quiet.
35
+
36
+ - **`node run` fixtures can pin a node's other outputs, so `$('Node').all(1)`
37
+ finally answers.** Give a node one items array **per output** and the branch
38
+ is readable offline:
39
+
40
+ ```json
41
+ "nodes": { "Decide": [[{ "json": { "side": "true" } }], [{ "json": { "side": "false" } }]] }
42
+ ```
43
+
44
+ `all(1)` / `first(1)` / `last(1)` and `$items('Decide', 1)` read output 1, and
45
+ an **empty** array is a real answer — that branch took no items. `input` takes
46
+ the same shape, indexed by the node's *input* (a Merge node's second input).
47
+ A plain items array still means a single output, so existing fixtures are
48
+ unchanged; asking for an output the fixture doesn't supply still refuses,
49
+ now saying how many it has. Until now every such call was refused outright,
50
+ because a fixture could only express one array per node.
51
+
52
+ - **`preflight --simulate` now replays a pinned node's other outputs too.** The
53
+ stand-in decanter substitutes for a network node is a Code node, which has one
54
+ output — so an error output (or any second branch) captured in your execution
55
+ was replayed nowhere, and everything behind it sat with no input, emitted
56
+ nothing, and let the run pass. Each populated output now gets **its own
57
+ stand-in**, wired to the same input as the original and feeding exactly that
58
+ output's targets, so the branch really runs. Deliberately not wired to the
59
+ synthetic trigger: a stand-in fires only when the original would, so a replay
60
+ whose real nodes take a different path can't have the old branch's items
61
+ injected into it anyway. The `simulate` check names the splits in its details
62
+ (terminal and `--json`). **`test` still replays `main[0]` only** — n8n's
63
+ `pinData` is one flat items array per node, with no output dimension — and
64
+ `scenario check` now spells out which of the two you are looking at.
65
+
66
+ - **`test` now reports what the run actually moved, not just that it
67
+ finished** — a coverage line over the nodes that executed (enabled and
68
+ unpinned; a pinned node's items are the input you supplied):
69
+ `coverage: 7/9 unpinned node(s) emitted items — 2 emitted none: Group
70
+ products, Write rows`. A node counts as emitting if it put an item on **any**
71
+ output. Some empty nodes are normal — a filter that dropped everything — so
72
+ the line warns and nothing more.
73
+
74
+ **But a run in which not one unpinned node emitted an item now fails
75
+ (exit 1), even with synthetic pins.** n8n calls such a run `success` and
76
+ it is: nothing errored. No data moved either, so nothing was demonstrated,
77
+ and reporting it as a pass was the check lying. The message names the usual
78
+ cause — a pin replays a node's first output only.
79
+
80
+ - **`scenario check` warns about what the replay will throw away.** Both replay
81
+ paths (`test`'s `pinData`, `preflight --simulate`'s stand-in node) read
82
+ `main[0]` only, while the validator happily accepts — and `✓ valid`s — a
83
+ scenario carrying items on several outputs. The check now says so offline:
84
+ once for a node whose data populates more than one output, naming the indices
85
+ that get dropped, and once for a node source that reads a pinned node's
86
+ non-first output (`$('Enrich').all(1)`, `$items('Enrich', 1)`) — the call that
87
+ returns nothing and leaves the node emitting nothing. Warnings only; the
88
+ scenario stays valid for the outputs that do replay.
89
+
90
+ ### Changed
91
+
92
+ - **An explicit `.ts` extension in a node file's import no longer fails the
93
+ typecheck.** The scaffolded `tsconfig.json` now sets
94
+ `allowImportingTsExtensions`, so `import { total } from
95
+ "../../../shared/money.ts"` type-checks — until now it was rejected
96
+ (TS5097) even though `push` bundles it without complaint, which made the
97
+ gate and the bundler disagree over a pure spelling choice. Extensionless
98
+ stays the recommended form (it survives a helper later becoming `.js`);
99
+ both spellings resolve everywhere. Existing sync dirs are offered the
100
+ updated `tsconfig.json` on the next `init` — a `tsconfig.json` you edited
101
+ yourself is reported as drift and left alone, so add the option by hand
102
+ there.
103
+ - **Two of the four import rules for `.ts` nodes now warn instead of
104
+ blocking a push**: a relative import resolving outside the sync dir, and an
105
+ absolute-path import. Both only endanger the author's own portability — the
106
+ bundle still builds locally and fails loudly (`Could not resolve`) wherever
107
+ the target is genuinely absent — so blocking them was decanter making the
108
+ user's call. The advisory prints on every surface (`preflight`'s `layout`
109
+ details, `push`, `node run`) and exactly once per push;
110
+ `preflight --fail-on=warn` is the strict variant for CI. **Node builtins
111
+ and npm packages not opted into `bundleDependencies` still block** —
112
+ esbuild is silent about both, so without the block the failure would
113
+ surface at runtime on the n8n instance.
114
+ - **The scaffolded `mcp-route-check.mjs` session hook now also inspects
115
+ user-level agent config for direct n8n MCP routes** — Claude Code's
116
+ `~/.claude.json` (including its entry for the current project), Cursor's
117
+ `~/.cursor/mcp.json`, the VS Code user profile, and opencode's global
118
+ config. Previously it read only project files, so an `n8n` server added
119
+ with `claude mcp add -s user` (or any other user-scoped config) bypassed
120
+ the decanter guard without a word — exactly the "second door" the hook
121
+ exists to catch. Still a warning, never a gate. Re-run `init` in an
122
+ existing sync dir (or re-copy the hook from the template) to pick it up.
123
+ - **The scaffolded `tsconfig.json` now covers the whole sync dir**, not just
124
+ `shared/` and `workflows/` — helper code may live in any folder inside the
125
+ sync dir (`shared/` is only the scaffolded default), so the typecheck and
126
+ the editor's tsserver now own every root without a config edit. Existing
127
+ sync dirs keep their scaffolded file; to match, widen `include` to
128
+ `["n8n-globals.d.ts", "**/*.ts", "**/*.js"]` and add
129
+ `"**/backups/**", "**/executions/**", "decanter-ts-plugin", "dist"` to
130
+ `exclude`. Two consequences worth knowing: a loose node-shaped scratch file
131
+ (top-level `return` outside any workflow's `code/`) is now part of the
132
+ program and reports TS1108 — move it into a workflow or add its folder to
133
+ `exclude`; and when `init` scaffolds into an **existing project** that had
134
+ no `tsconfig.json`, the new config sweeps that project's own `.ts`/`.js`
135
+ into the node-file typecheck — add your app dirs to `exclude` if they
136
+ shouldn't gate pushes.
137
+
138
+ ### Fixed
139
+
140
+ - **`diff` and `preflight` no longer report a `CONFLICT` for a node with no
141
+ recorded sync hash.** "Changed both locally and remotely" is measured against
142
+ the last-sync baseline in `.decanter.json`; with no baseline nothing is
143
+ *known* to have moved on the instance, and `push` has always treated that as
144
+ pushable. The two disagreed, so the report described a dead end the CLI did
145
+ not have — worse, its documented exit (`push --force`) is denied to agents by
146
+ the permission rules `init` scaffolds. Such a node now reads as
147
+ `push pending`, which is what `push` does with it. `pull` no longer warns
148
+ `CONFLICT` for the same case on `.ts` nodes.
149
+ - **A `.js` → `.ts` conversion no longer reads as data loss.** Re-pointing a
150
+ node's `//@file:` placeholder is the sanctioned way to convert, and `push`
151
+ and `pull` both adopt it before doing anything — but `diff` and `preflight`
152
+ looked the file up in `.decanter.json` alone and announced `local file
153
+ code/<node>.js missing` for the file you had just replaced. They now read the
154
+ placeholder too, so a converted node reports `local changes in
155
+ code/<node>.ts — push pending`, identically for every converted node.
156
+ - **`preflight` no longer prints `✓ parity local code matches the draft`
157
+ directly above `✗ drift CONFLICT`.** Both checks read the same facts, so
158
+ `parity` may claim a match only when every node is in sync; divergence that
159
+ `drift` owns is reported as an `info` line pointing at it.
160
+ - **`mcp connect` survives an unreachable n8n instead of dying at the
161
+ handshake.** `initialize` was forwarded like any other message, so a
162
+ connection failure answered the *handshake* with an error: the agent's MCP
163
+ client got no `serverInfo` and tore the session down before a single tool
164
+ call could report what was wrong. The guard now completes the handshake
165
+ itself when n8n does not answer, and the failure surfaces on the tool call
166
+ that needed the instance. Once n8n is reachable, the handshake is replayed
167
+ upstream so the session it uses is a real one. The startup line now reads
168
+ `guard: ready — forwarding all n8n MCP tools to <host>` instead of
169
+ `connected to <host>`, which claimed a connection nothing had made yet.
170
+ - **The scaffolded `AGENTS.md` now names the n8n-side prerequisites.** MCP
171
+ access is a switch per *instance* **and** one per *workflow* ("Available in
172
+ MCP"), and until a workflow's is on, `pull`/`push`/`diff`/`preflight`/`watch`
173
+ all fail for it. The CLI's error says so; the file an agent reads *before*
174
+ running anything did not.
175
+ - **`preflight`'s `types` check now reports type errors in shared helper
176
+ files** instead of passing green while `push` fails on them. The scoped
177
+ typecheck dropped every diagnostic outside the graded workflow's own dir —
178
+ and helper code lives outside every workflow dir by definition — so a
179
+ workflow could grade `ready` on code `push` then rejected. Helper
180
+ diagnostics (under `shared/` or any other folder) now surface in every
181
+ workflow's `types` line, while another workflow's *node* errors stay out of
182
+ scope as before.
183
+ - **The documented shared-import path was one level short** for the `code/`
184
+ layout: `../../shared/money` in the TypeScript-nodes docs and the scaffolded
185
+ `AGENTS.md` resolved to `workflows/shared/money` and failed on copy-paste.
186
+ The correct depth from `workflows/<folder>/code/` is `../../../shared/money`.
187
+ - **Compiled module labels — and therefore sync hashes — are now stable when
188
+ the sync dir is reached through a symlinked path** (macOS `/tmp`, a
189
+ symlinked checkout). esbuild resolves bundled files to their realpaths, so
190
+ an un-realpathed label base produced machine-specific `../…`-climbing
191
+ labels inside the hashed bytes — nodes reading "push pending" across
192
+ machines with nobody touching code. The same spelling mismatch made a
193
+ *scoped* typecheck silently drop every node diagnostic under a symlinked
194
+ path; both now realpath. If a sync dir lives behind a symlink, the first
195
+ push after this release re-baselines the affected nodes' hashes once.
196
+ - **The route-check hook now reads opencode's real config shape**
197
+ (`mcp.<name>`). It previously looked only for `mcpServers` / `mcp.servers` /
198
+ `servers` bags, so an `opencode.json` routing straight at the instance was
199
+ listed as checked but never actually flagged.
200
+ - **A `watch` save now runs the same folder-wide compliance guard as `push`.** It
201
+ used to check only the saved file, which cannot see the workflow's node names —
202
+ so it could not catch a dangling `$('Renamed Node')` and pushed straight to the
203
+ draft what a manual `push` refuses outright. The break then surfaced at run time
204
+ in n8n instead of at save time. Four surfaces already claimed the guards were
205
+ the same; now they are.
206
+
207
+ **The abort is scoped, deliberately:** a save is refused when the violation is
208
+ in the file you just saved, and violations elsewhere in the folder are printed
209
+ on every save without blocking it. Repairing a rename means fixing several
210
+ files, and a folder-wide abort would stop every save until the last fix — which
211
+ would disable `watch` during exactly the job it is for.
212
+
213
+ - **`liveMirror` is documented as what it is: a full `pull`.** The shipped agent
214
+ contract and the config reference both described it as refreshing the
215
+ `workflow.json` snapshot. It also rewrites the `code/` files and
216
+ `.decanter.json` and moves files on a rename, so it can overwrite an unpushed
217
+ local edit — which is worth knowing before you restructure. No behaviour change;
218
+ the docs now say what the code has always done.
219
+
220
+ ## [0.10.0] - 2026-08-07
221
+
222
+ ### Added
223
+
224
+ - **`scenario create <workflow> "<slug>" --extend`** — top an **existing**
225
+ scenario up with the pinnable nodes it is missing, keeping every value already
226
+ authored. Previously `scenario create` refused an existing file outright, so a
227
+ scenario `test` rejected could only be fixed by hand-editing raw JSON for nodes
228
+ the tool had never named. Also covers the ordinary case of a workflow that
229
+ gained a node after its scenario was written.
230
+
231
+ - **`scenario create --scaffold` now works with no instance.** The fill entries
232
+ were always built from your local `workflow.json`; the instance only supplied
233
+ the per-node output **JSON Schemas**, which annotate the fill rather than
234
+ enable it. With no `N8N_HOST` configured it now says the annotations are
235
+ missing and scaffolds anyway — each node lands as provenance `authored`
236
+ instead of `scaffolded`, so the difference stays visible in the file. This is
237
+ what makes `preflight --offline --simulate` reachable on a plane: of the four
238
+ pin sources, only *fetching a fresh capture* actually needs n8n. The messages
239
+ that route you to a pin source now lead with the offline-viable ones, and
240
+ `docs/cli/preflight.md` states which of the four need the instance.
241
+
242
+ ### Changed
243
+
244
+ - **Breaking:** **a slug-less `scenario create --scaffold` now writes
245
+ `scenarios/scaffold.json`, not `scenarios/scenario.json`.** The old default collided with the `scenario`
246
+ verb, and the flag parser refuses to read a verb name as a flag value — so
247
+ `preflight --simulate --scenario scenario` failed with `--scenario needs a
248
+ value`, leaving the default file referenceable only as `--scenario=scenario`.
249
+ If you have a script that names the old default file, point it at the new one
250
+ (an explicit `<slug>` argument was, and stays, unaffected).
251
+
252
+ - **`scenario check` now reports the `test` gate too, not just the
253
+ `preflight --simulate` one.** The two demand different node sets on purpose —
254
+ `--simulate` asks only for nodes the capture **reached**, `test` asks for
255
+ **every** enabled non-pure node because it runs on the live instance with real
256
+ credentials. Reporting only the looser one meant a scenario could be green and
257
+ still be refused by `test`. `check` now says which gate you have passed, and
258
+ names `--extend` as the way to close the difference. The docs that asserted the
259
+ two rules were the same are corrected.
260
+
261
+ - **`scenario create --execution` pins unreached nodes to an empty run instead of
262
+ asking you to invent output for them.** A pinnable node the capture never
263
+ reached is written as `[{"data":{"main":[[]]}}]` — "this branch isn't
264
+ exercised" — and listed under `_decanterScenario.notExercised`. That makes a
265
+ capture-seeded scenario usable with `test` straight away, keeps the node pinned
266
+ to zero items (so it can never touch the real world), and leaves the claim
267
+ visible for review: if a branch *should* have run, give it real data.
268
+
269
+ - **The cold-start errors now name the non-interactive `init`, and `--mcp-token`
270
+ is an accepted alias for `--token`.** A fresh clone has no `.env` (it is
271
+ gitignored), so `N8N_HOST must be set …` is the first thing you — or a coding
272
+ agent — read. It said what was wrong and pointed only at a path that needs
273
+ someone at a prompt; a blind session diagnosed the problem in one command and
274
+ then had to hand the job back to a human. Both messages now spell out
275
+ `n8n-decanter init . --host <host-url> --token <mcp-token>`, `init --host` on
276
+ its own names `--token` when it warns about missing credentials, and the token
277
+ flag accepts either spelling.
278
+
279
+ ### Fixed
280
+
281
+ - **The "install typescript" advice now pins `@^5`.** A bare
282
+ `npm i -D typescript` installs **7.x**, whose compiler is the native rewrite
283
+ and no longer exposes the programmatic API decanter's node-file typecheck
284
+ drives — so following the old advice replaced a *skipped* check with a broken
285
+ one. The skip message and `preflight`'s unlock now both say
286
+ `npm i -D typescript@^5` and name the reason. `init`'s scaffold already pinned
287
+ `^5`; this only ever bit projects `init` deliberately left alone.
288
+
289
+ - **A 403 from the public API now names the scope you are missing.** n8n answers
290
+ a valid-but-under-scoped `N8N_API_KEY` with a bare 403 and says nothing about
291
+ which of eight scopes is absent. Every REST surface — executions, data tables,
292
+ backup — now gets a per-endpoint hint, including the trap that catches people
293
+ out: `dataTable:read` does **not** cover `/columns` or `/rows`, which need
294
+ `dataTableColumn:read` and `dataTableRow:read`. The data-table hints also say
295
+ outright that decanter only ever **reads** them, so no write scope is needed.
296
+
297
+ - **`scenario create` prints the file's size, and warns before it lands in git.**
298
+ A capture-seeded scenario is a verbatim copy of every item of every node; one
299
+ from a busy production run can be tens of megabytes. Nothing measured it, and
300
+ `scenarios/` is tracked — so the folder-wide auto-commit on the next `pull` or
301
+ `push` swept it into history unasked. The success line now carries the size,
302
+ and anything above 1 MB warns explicitly that it is about to be committed.
303
+
304
+ - **Three scenario messages pointed somewhere the thing you needed was not.**
305
+ A pre-rename scenario (`_decanterMock`) was told to look in
306
+ `_decanterScenario.fill` — a key not in the file; the message now names the key
307
+ it actually found. A replay gap derives its node list from the **workflow
308
+ graph**, so those nodes are by definition *not* in `fill`, yet it said "see the
309
+ `_decanterScenario` block"; it now says they are **not** listed, and points at
310
+ `--extend`. And a node deliberately written as `"Node": []` was reported as
311
+ unfilled — it is now told apart from a node with no entry at all, with the
312
+ spelling for "emits nothing".
313
+
314
+ - **The agent guard's 401 no longer reads as "this project was never set up".**
315
+ It led with *"run `n8n-decanter init`"*, and a blind field-test round watched an
316
+ agent conclude from it that there was no `.env` and no token at all — then send
317
+ its user through a pointless `init`. The `.env` existed; the token had simply
318
+ been rotated. The guard now leads with the cause (*"n8n rejected decanter's
319
+ existing MCP credentials … they are configured but no longer valid"*), matching
320
+ what the CLI already said, and offers `init` only as the OAuth alternative. It
321
+ also maps **403** now, pointing at n8n → Settings → MCP.
322
+
323
+ - **A missing `typescript` is reported as a skipped check, not a failed one.**
324
+ `preflight`'s node-file typecheck needs `typescript` in your project. A
325
+ globally installed decanter ships none (it is a devDependency), and `init`
326
+ leaves an existing `package.json` alone — so scaffolding into a project you
327
+ already had produced a module-resolution stack trace surfacing as a *typecheck
328
+ failure*, which reads like a type error in your own code. It is now an honest
329
+ skip, named in the coverage block with the one-command fix
330
+ (`npm i -D typescript`).
331
+
332
+ - **The scaffolded `AGENTS.md` tells agents that `.env` is unreadable by policy
333
+ and that this is not evidence it is missing** — the reasoning trap behind the
334
+ 401 finding above.
335
+
336
+ - **`node run` no longer answers a branch index with the wrong branch's data.**
337
+ `$('Node').all(1)`, `$items('Node', 1)` and `$input.all(1)` ask for a node's
338
+ *second* output — an `IF`'s false branch, a `Switch`'s other case. A fixture
339
+ pins **one** items array per node, so there is no honest answer, but the
340
+ argument was accepted and **ignored**: you got output 0's items and the node
341
+ looked like it worked, graded against a shape it will never see live. Worse
342
+ than empty data, because nothing fails. These calls now refuse with a message
343
+ naming the call and the two ways forward (pin that branch as its own fixture
344
+ node, or run it for real with `test`) — the same signpost pattern `$vars` and
345
+ `$secrets` already use. `n8n-globals.d.ts` declared the parameter and
346
+ `docs/cli/node-run.md` listed the calls as fully covered, so both surfaces had
347
+ promised something the emulation never did.
348
+
349
+ - **`pull` no longer destroys uncommitted local edits.** It committed the folder
350
+ *after* overwriting it, so an uncommitted `.js` edit was gone and had never
351
+ entered git — while the warning printed on that exact path told you to
352
+ "recover via git". Pull now takes a **snapshot commit before it writes
353
+ anything** (`watch` and the live mirror already did). If the snapshot cannot be
354
+ made — no git repo, `commitOnPull: false`, a git error — the pull still runs,
355
+ but the warning says the overwrite is **not** recoverable instead of promising
356
+ a safety net that isn't there.
357
+
358
+ - **`pull`'s clobber warning now fires for a node it has never synced.** It was
359
+ gated on the node already having a sync baseline, which is backwards on the
360
+ read side: no baseline means the node isn't in `.decanter.json` yet, so the
361
+ local file is precisely the one with no protection. The loss path this opened
362
+ matches the scaffolded agent workflow exactly — an agent adds a Code node over
363
+ the guard (the guard blocks `jsCode`, so the remote body is empty), writes the
364
+ source into `code/<node>.js`, and a background mirror pull lands before the
365
+ first push: fresh file replaced by the empty remote body, silently.
366
+
367
+ - **The live mirror stops refreshing when its safety commit fails.** It awaited
368
+ the commit and discarded the result, but that call returns a failure (it never
369
+ throws) for any git error — unset identity, a mid-merge tree, `index.lock`, a
370
+ rejecting hook. The documented "a dirty tree is safety-committed before the
371
+ pull" rail therefore degraded silently into an unrecoverable overwrite. It now
372
+ skips the refresh and says why, matching `watch`.
373
+
374
+ - **Scenario gaps are now judged per branch, not per node.** A branching node
375
+ (an `IF`, a `Switch`) emits on **one** output per run, but `preflight
376
+ --simulate` and `scenario create --execution` read its *first* output for every
377
+ outgoing edge. Two consequences, both wrong: a node on the branch the capture
378
+ **never took** was reported as a gap you had to write pin data for, and a
379
+ genuine gap's `inputSample` was filled from the **other** branch's items — so
380
+ you coded against the wrong upstream shape. Untaken branches are exempt and
381
+ neutralized, as documented.
382
+
383
+ - **An n8n whose MCP access is switched off now says so.** Turning MCP off is a
384
+ one-click state in n8n (Settings → MCP) and it makes the server answer a valid
385
+ token with `403 MCP access is disabled` — it never 404s. decanter recognised
386
+ 401 and 404 but not 403, so every command failed with a bare
387
+ `403 Forbidden` and no next step. It now surfaces n8n's own reason plus the
388
+ fix. The 404 message was reworded to match what 404 actually means (no MCP
389
+ server at that address — wrong `N8N_HOST`, or an n8n too old to have one).
390
+
391
+ Note the ordering trap, now documented in the troubleshooting FAQ: a missing
392
+ or stale token still returns 401 while MCP is off, so the 401 can hide the 403.
393
+
394
+ ## [0.9.0] - 2026-08-04
395
+
396
+ ### Changed
397
+
398
+ - **The agent guard now refuses a `publish_workflow` that would take a broken
399
+ draft live.** `publish` already checked, but the raw MCP tool went straight
400
+ through the guard — so an agent could go live around the verb and ship exactly
401
+ the breakage the check exists to catch. Both transports (`mcp connect` and
402
+ `mcp serve`) run the same check on the same shared code.
403
+
404
+ **Fail-closed:** if the check itself cannot run — n8n unreachable — the publish
405
+ is refused too, and the message says the *check* failed rather than claiming
406
+ the workflow is broken. A read that fails almost certainly means the publish
407
+ would have failed anyway, and "couldn't verify, so we shipped it" is not a gate.
408
+
409
+ - **Dangling-reference checks now cover all four forms n8n rewrites on a rename**
410
+ — `` $('X') `` (as before) plus `$node["X"]`, `$node.X` and `$items('X')`.
411
+ Previously only the first was detected, so a rename could strand a `$node[…]`
412
+ call site that nothing reported: `preflight`, `push`, `test` and `publish` all
413
+ passed it, and it failed at run time instead. The rule is n8n's own — its
414
+ rewriter handles exactly these four — so if n8n treats it as a reference, the
415
+ guard now does too.
416
+
417
+ **This can surface errors in workflows that passed before.** A `$node["Old"]`
418
+ reference to a node that no longer exists is a hard compliance error, which
419
+ `--force` does not bypass. The message quotes the reference **as written**, so
420
+ it is clear which form triggered it. Computed references (`$(someVar)`, a
421
+ template literal with `${…}`) are still left alone — a regex cannot resolve
422
+ them, and n8n has the same limit.
423
+
424
+ - **Breaking: `n8n-decanter test <workflow>` no longer executes.** It used to
425
+ fall back to the newest capture under `executions/` and run the workflow for
426
+ real on your instance — a directory that is gitignored, so the same commit
427
+ behaved differently for different people, and a bare verb had real side
428
+ effects. Bare `test` is now a **static tier**: it reads the instance's draft,
429
+ reports dangling `$('…')` references, and runs nothing. Pass
430
+ `--execution <id>` or `--scenario <slug>` for the pinned run, which is
431
+ otherwise unchanged. There is no deprecation shim — a `test` that still
432
+ executed *sometimes* would keep exactly the ambiguity this removes.
433
+
434
+ The pinned run now also does the static check first, so a draft already known
435
+ to be broken is never fired at the instance.
436
+ - **`publish` refuses a draft carrying a dangling `$('…')` reference.**
437
+ Previously nothing checked: the compliance guard runs on `push`, `preflight`
438
+ and `backup` — not `publish` — so a task that only renamed nodes never hit a
439
+ gate and the break went live. The check reads **the draft on the instance**
440
+ (the read `publish` already makes), not your local folder: `workflow.json` is
441
+ a snapshot, so grading it would pass a broken workflow on a stale mirror and
442
+ block a legitimate publish from a fresh clone.
443
+
444
+ ### Added
445
+
446
+ - **`init` now scaffolds a hook that catches stranded `$('…')` references right
447
+ after a rename.** n8n's `renameNode` MCP op rewrites the node name and the
448
+ connections only, so the references it leaves behind used to surface at the
449
+ next `push` — arbitrarily far from the rename that caused them. On Claude Code
450
+ a PostToolUse hook on `update_workflow` now reports them immediately, split
451
+ into the two halves and in the order they must be repaired: other nodes'
452
+ expression parameters in n8n first, then the code files here, then `push`.
453
+
454
+ It scans for the old name instead of running `preflight`, deliberately: the
455
+ hook fires before the background snapshot refresh, and until that lands the
456
+ snapshot still carries the old name, so every reference still resolves and
457
+ `preflight` would report clean. Silent when nothing references the renamed
458
+ node. The checklist in the scaffolded `AGENTS.md` remains the contract for
459
+ every agent — the hook is a reminder, not a replacement.
460
+
461
+ ### Fixed
462
+
463
+ - **Corrected the rename guidance: n8n's `renameNode` MCP op does NOT rewrite
464
+ `$('…')` references.** The scaffolded agent guide (and the 0.6.0 notes below)
465
+ claimed n8n rewrites connections *and* `$('…')` references server-side on a
466
+ rename. Verified against real n8n 2.30.7 and 2.33.3: the MCP op rewrites the
467
+ node name and the **connections only**, then reports success with
468
+ `validationWarnings: []` — every `$('Old Name')` ref is left dangling, both in
469
+ Code-node source and in other nodes' expression parameters. The n8n *editor*
470
+ does rewrite them, but in the browser before it saves, so "server-side" was
471
+ wrong for that path too. No amount of pulling repairs this; `pull` faithfully
472
+ mirrors what n8n stored.
473
+
474
+ `init`'s `AGENTS.md`/`CLAUDE.md` now describe the real contract, including the
475
+ repair order that matters: **fix other nodes' expression parameters over MCP
476
+ first, then local code, then `push`.** The other order loses the code fix,
477
+ because a forwarded MCP write schedules a background snapshot refresh whose
478
+ pull overwrites unpushed `.js` edits.
479
+ - **A dangling-reference error now says which half it is and where to fix it.**
480
+ The two compliance errors were near-identical and neither mentioned a rename,
481
+ which led to `workflow.json` being hand-edited — turning the check green while
482
+ n8n stayed broken.
483
+
10
484
  ## [0.8.0] - 2026-07-27
11
485
 
12
486
  ### Added
package/README.md CHANGED
@@ -30,46 +30,31 @@ clean git diffs.
30
30
 
31
31
  ![Terminal demo — the interactive picker: filter workflows, choose a verb, sync](./docs/terminal-demo.gif)
32
32
 
33
- - **Real version control** — meaningful diffs, PRs, blame; every push and
34
- pull is auto-committed.
33
+ - **Real version control** — meaningful diffs, PRs and blame per Code node;
34
+ every push and pull is auto-committed.
35
35
  - **TypeScript or typed JS** — write nodes in TS (compiled on push) or plain
36
36
  JS; n8n globals (`$input`, `$('…')`, …) are typed in both.
37
+ - **Shared code and small libraries** — `.ts` nodes import helper files from
38
+ anywhere in the sync dir (`shared/` by default) and opted-in npm packages,
39
+ bundled on push to run anywhere, n8n Cloud included.
37
40
  - **Agent-native** — `init` scaffolds Claude Code / Cursor / Codex configs;
38
- the offline `preflight --offline` / `node run` loop gives agents a
39
- credential-free feedback loop.
40
- - **One gate, two depth flags** — a compliance guard, typecheck gate, and
41
- per-node drift guard gate every push; `preflight` folds layout, types and
42
- instance sync facts into a single **read-only, CI-gateable verdict** (never
43
- writes, never runs on your instance). `--simulate` *adds* an offline engine
44
- replay (Docker), `--offline` *drops* the instance reads — and both that
45
- replay and instance-side `test` diff every node against a real captured
46
- execution and exit 1 on divergence. The flow is **`preflight` → `push` →
47
- `test` → `publish`**: verify local, make it the draft, run what you pushed,
48
- go live.
49
- - **Committed, schema-scaffolded scenarios** — `scenario create` turns a
50
- captured execution and/or the workflow's own output schemas
51
- (`--scaffold`, no LLM API) into a reviewable, git-tracked pin-data set that
52
- `test` and `preflight --simulate` replay — the durable counterpart to an
53
- agent's ephemeral
54
- in-session pin flow.
41
+ `preflight --offline` and `node run` give agents a credential-free loop.
42
+ - **Guarded agent access to n8n's MCP, wired by default** — the scaffolded
43
+ `.mcp.json` forwards n8n's full MCP surface except Code-node `jsCode` writes,
44
+ and the read-only `workflow.json` snapshot refreshes itself afterwards.
45
+ - **One gate before you push** — `preflight` folds layout, types and instance
46
+ sync facts into a single read-only, CI-gateable verdict; `--simulate` adds an
47
+ offline engine replay, `--offline` drops the instance reads.
48
+ - **Draft-first by construction** — every push lands on the **draft**;
49
+ `publish` is the deliberate go-live. Flow: **preflight → push → test → publish**.
50
+ - **Committed, schema-scaffolded scenarios** — `scenario create` turns a captured
51
+ execution and/or the workflow's own output schemas into a git-tracked pin-data
52
+ set that `test` and `preflight --simulate` replay.
55
53
  - **Live editing** — `watch` pushes on save; the open n8n editor tab reflects
56
54
  each push live (n8n's own draft-edit refresh, no proxy needed).
57
- - **Guarded agent access to n8n's MCP — wired by default** — the scaffolded
58
- `.mcp.json` spawns `mcp connect`, forwarding the full n8n MCP surface
59
- except writes to a Code node's `jsCode`; no secret to manage. The read-only
60
- `workflow.json` snapshot then **refreshes itself** after an agent
61
- restructures the workflow through the guard — no manual `pull`
62
- (`liveMirror`, on by default).
63
- - **Git-native disaster recovery** — `backup create` captures a versioned,
64
- redeployable full export into a committed `backups/` store (fidelity MCP
65
- can't give — credential refs kept, code stays a placeholder); `backup
66
- restore` redeploys it as a new workflow with **node ids preserved**, a
67
- second version history that outlives the instance.
68
- - **Shared code and small libraries** — `.ts` nodes import helpers/types from
69
- `shared/` and opted-in npm packages, bundled on push into self-contained
70
- nodes that run anywhere, n8n Cloud included.
71
- - **Draft-first by construction** — every push lands on the workflow's
72
- **draft**; `publish` (or `push --publish`) is the deliberate go-live step.
55
+ - **Git-native disaster recovery** — `backup create` captures a redeployable
56
+ full export into a committed `backups/` store; `restore` redeploys it as a new
57
+ workflow with **node ids preserved**.
73
58
 
74
59
  ![Agent demo — a coding agent edits a Code node, verifies it offline, then pushes to the draft](./docs/agent-demo.gif)
75
60
 
@@ -133,7 +118,7 @@ Full flag reference: `n8n-decanter --help`, or the
133
118
  | `diff [workflow…]` | Per-node line diff of local code vs the n8n draft — an inspection view, always exits 0 |
134
119
  | `executions [workflow…]` | Fetch recent execution data (read-only) |
135
120
  | `data-tables [table…]` | Fetch data-table schema + rows (read-only) |
136
- | `test <workflow>` | Pinned run **on the instance** (draft); diffs vs a capture |
121
+ | `test <workflow>` | Grade the **instance's draft**: static check bare, pinned run with `--execution`/`--scenario` |
137
122
  | `preflight [workflow…]` | The one local gate — layout, types, instance sync facts, optional local-engine replay — scored and read-only; run before `push` (exits 1 on *not ready*) |
138
123
  | `scenario create` / `scenario check` | Build / validate a committed scenario (captured and/or schema-scaffolded) |
139
124
  | `backup create` / `restore` / `list` | Git-native disaster recovery — capture / redeploy / list versioned full-export backups |
@@ -149,50 +134,34 @@ tools (through the guard above); the next `pull` reconciles the local mirror.
149
134
 
150
135
  ## How it compares
151
136
 
152
- n8n-decanter is **Code-node-first**: it optimizes the loop of writing, typing,
153
- verifying, and shipping the JavaScript/TypeScript *inside* your workflows. It
154
- builds **on** n8n's own MCP server and skills — they're the foundation it rides
155
- for structure and lifecycle, not a rival. So the comparison below is against the
156
- native editor and against
157
- [n8n-as-code](https://github.com/EtienneLescot/n8n-as-code) — a broader,
158
- whole-workflow authoring toolkit.
159
-
160
- > **Choose [n8n-as-code](https://github.com/EtienneLescot/n8n-as-code) if you…**
161
- > need an enterprise-ready automation framework — where AI agents assemble
162
- > complete workflows, multi-environment Dev → Prod pipelines automatically handle
163
- > credential governance, and full TypeScript GitOps ensures strict auditability
164
- > across your team.
165
- >
166
- > **Choose n8n-decanter if you…** your workflows live or die by their Code nodes
167
- > and you want them as real files — TypeScript with typed n8n globals, shared libraries,
168
- > preflights (offline or instance-side), and code-level git history, synced
169
- > draft-first between your IDE, your coding agent, and n8n (even on Cloud).
137
+ n8n-decanter is **Code-node-first**: it optimizes writing, typing, verifying and
138
+ shipping the JavaScript/TypeScript *inside* your workflows, and builds **on**
139
+ n8n's own MCP server and skills rather than rivalling them. Compared against the
140
+ native editor and [n8n-as-code](https://github.com/EtienneLescot/n8n-as-code),
141
+ a broader whole-workflow authoring toolkit:
170
142
 
171
143
  | Capability | Native n8n (browser) | n8n-as-code | n8n-decanter |
172
144
  |---|---|---|---|
173
- | **TypeScript for Code nodes** | ❌ JavaScript or Python only | ❌ TS is at workflow level, not node logic | ✅ Code nodes as `.ts`, compiled on push, typed n8n globals |
174
- | **Shared types & helpers in Code nodes** | ❌ self-host `NODE_FUNCTION_ALLOW_*` only; no libraries | ❌ not part of its model | ✅ `shared/*.ts` + npm bundled into self-contained nodes (Cloud-safe) |
175
- | **Code as individual files** | ❌ no source files (JSON blob) | 🟡 one `.workflow.ts` per workflow | ✅ folder per workflow; each Code node its own `.js`/`.ts` |
176
- | **Code-level git versioning** | 🟡 in-app history (DB snapshots, tiered retention); Git source control is Enterprise-only | ✅ GitOps sync of workflow source | ✅ real git — diffs, PRs, blame per Code node; auto-commit each sync (+ read-only structure snapshot) |
177
- | **Preflights** (`preflight`, then `test`) | 🟡 re-run past executions / pin data, but online in-editor | 🟡 inspect executions against a live env | ✅ `preflight` scores your **local** code (layout, types, instance reads, optional offline `--simulate` engine replay) into one read-only, CI-gateable verdict *before* the push — then instance-side `test` runs the pushed draft; each runtime check diffs every node vs a real capture, exits 1 on divergence |
178
- | **Draft-first code sync** | ✅ editor *Save* vs *Publish* (manual, in-browser) | 🟡 API sync republishes on push (no draft-only) | ✅ pushes land on the **draft**; `publish` is the deliberate go-live (over MCP) |
179
- | **Live editing** | ✅ the canvas (baseline) | 🟡 explicit pull/push, no auto-watch | ✅ `watch`: push on save; the open editor tab reflects each push live (n8n-native) |
180
- | **Agent-native tooling** | 🟡 n8n's own canvas AI, not your agent on the codebase | ✅ Agent Workbench, skills, MCP, Claude/editor plugins | ✅ scaffolds Claude Code / Cursor / Codex configs incl. a pre-wired `mcp connect` guard holding the credentials; offline `preflight --offline`/`node run` loop |
181
- | **Model ownership** | ❌ locked to n8n's own hosted AI; can't use your Claude subscription | 🟡 beta Claude Code plugin uses your subscription; flagship Workbench needs an Anthropic key for Claude | ✅ never calls an LLM itself — your agent/subscription does 100%, no key or model config ever |
182
- | **Agentic workflow creation** | 🟡 AI Workflow Builder (natural language), but Cloud / plan-gated — credits, self-host needs setup | ✅ 537 node schemas + 7,700+ templates + skills | ✅ your agent builds structure over n8n's MCP (through the pre-wired `mcp connect` guard); decanter owns the Code-node source (files + `push`) |
183
- | **Whole-workflow authoring** | ❌ | ✅ `.workflow.ts` decorator classes (structure + links) | 🟡 Code-node source only; your agent builds structure live over MCP (row above), mirrored to a read-only `workflow.json` |
184
- | **Multi-environment promotion** | 🟡 Enterprise source control / environments | ✅ `promote` remaps creds + refs Dev→Prod | 🟡 separate sync dir per instance, but no `promote` (IDs/creds/refs not remapped) |
145
+ | **TypeScript for Code nodes** | ❌ JavaScript or Python only | ❌ TS at workflow level, not node logic | ✅ `.ts` nodes, compiled on push, typed n8n globals |
146
+ | **Shared types & helpers in Code nodes** | ❌ self-host `NODE_FUNCTION_ALLOW_*` only | ❌ not part of its model | ✅ helper `.ts` anywhere in the sync dir + npm, bundled into self-contained nodes (Cloud-safe) |
147
+ | **Code as individual files** | ❌ no source files (JSON blob) | 🟡 one `.workflow.ts` per workflow | ✅ folder per workflow; each Code node its own file |
148
+ | **Code-level git versioning** | 🟡 in-app history; Git source control is Enterprise-only | ✅ GitOps sync of workflow source | ✅ real git — diffs, PRs, blame per Code node; auto-commit each sync |
149
+ | **Preflights** (`preflight`, then `test`) | 🟡 re-run past executions, but online in-editor | 🟡 inspect executions against a live env | ✅ one read-only, CI-gateable verdict *before* the push; then `test` runs the pushed draft — each diffs every node vs a real capture |
150
+ | **Draft-first code sync** | ✅ editor *Save* vs *Publish* (in-browser) | 🟡 API sync republishes on push | ✅ pushes land on the **draft**; `publish` is the deliberate go-live |
151
+ | **Live editing** | ✅ the canvas (baseline) | 🟡 explicit pull/push, no auto-watch | ✅ `watch`: push on save; the open editor tab reflects it live |
152
+ | **Agent-native tooling** | 🟡 n8n's canvas AI, not your agent on the codebase | ✅ Agent Workbench, skills, MCP, editor plugins | ✅ scaffolds Claude Code / Cursor / Codex + a pre-wired `mcp connect` guard; offline agent loop |
153
+ | **Model ownership** | ❌ locked to n8n's hosted AI | 🟡 Workbench needs an Anthropic key for Claude | ✅ never calls an LLM itself — your agent/subscription does 100% |
154
+ | **Agentic workflow creation** | 🟡 AI Workflow Builder, but Cloud / plan-gated | ✅ 537 node schemas + 7,700+ templates + skills | ✅ your agent builds structure over n8n's MCP; decanter owns the Code-node source |
155
+ | **Whole-workflow authoring** | ❌ | ✅ `.workflow.ts` decorator classes | 🟡 Code-node source only; structure goes over MCP, mirrored read-only |
156
+ | **Multi-environment promotion** | 🟡 Enterprise source control / environments | ✅ `promote` remaps creds + refs Dev→Prod | 🟡 separate sync dir per instance, no `promote` |
185
157
 
186
158
  Legend: ✅ first-class · 🟡 partial or indirect · ❌ not supported.
187
159
 
188
160
  **Bottom line:** reach for n8n-decanter when your workflows live or die by their
189
- Code nodes — TypeScript, shared libraries, preflights, and code-level git
190
- history, shipped draft-first. It rides n8n's own MCP and skills for everything
191
- structural, so n8n-as-code still shines for whole-workflow authoring/generation
192
- and multi-environment ops, and the native editor stays the live visual canvas
193
- everything syncs back to. And decanter makes no LLM calls of its own — you drive
194
- it with the coding agent you already run, so Claude Code on a Claude subscription
195
- needs no extra API tokens.
161
+ Code nodes. n8n-as-code still shines for whole-workflow authoring and
162
+ multi-environment ops, and the native editor stays the visual canvas everything
163
+ syncs back to. decanter makes no LLM calls of its own — you drive it with the
164
+ coding agent you already run.
196
165
 
197
166
  ## Caveats
198
167