n8n-decanter 0.10.1 → 0.11.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 +277 -0
- package/README.md +29 -3
- package/dist/lib/api.mjs +33 -5
- package/dist/lib/config.mjs +185 -5
- package/dist/lib/git.mjs +114 -0
- package/dist/lib/init.mjs +411 -63
- package/dist/lib/mcp.mjs +156 -34
- package/dist/lib/mcpconnect.mjs +11 -4
- package/dist/lib/mcpserve.mjs +10 -4
- package/dist/lib/preflight.mjs +10 -5
- package/dist/lib/push.mjs +42 -4
- package/dist/lib/simulate.mjs +6 -1
- package/dist/lib/util.mjs +65 -9
- package/dist/lib/validate.mjs +9 -3
- package/dist/n8n-decanter.mjs +235 -74
- package/package.json +2 -2
- package/template/.claude/hooks/mcp-route-check.mjs.example +85 -11
- package/template/.claude/hooks/rename-refs.mjs.example +20 -4
- package/template/.claude/hooks/verify.mjs.example +21 -3
- package/template/.cursor/rules/n8n-decanter.mdc.example +3 -1
- package/template/.env.example +14 -0
- package/template/AGENTS.md.example +66 -7
- package/template/CLAUDE.md.example +3 -2
- package/template/opencode.json.example +2 -3
- package/dist/lib/add.mjs +0 -101
- package/dist/lib/proxy.mjs +0 -204
- package/dist/lib/rename.mjs +0 -96
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,283 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.1] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`N8N_DECANTER_AUTH=upstream` — an auth mode for instances behind a proxy
|
|
15
|
+
that attaches the n8n credentials itself.** In this mode decanter sends *no*
|
|
16
|
+
credential header at all: no `Authorization` on MCP, no `X-N8N-API-KEY` on
|
|
17
|
+
REST. That is the only thing that works — a proxy which adds its own key
|
|
18
|
+
**appends** it to whatever the client sent, and n8n answers `401` to the pair,
|
|
19
|
+
so the previous workaround (a placeholder value in `N8N_API_KEY`, to satisfy
|
|
20
|
+
the REST-verb guard) fails just like an empty one. `N8N_API_KEY` stops being
|
|
21
|
+
required, so `executions`, `data-tables` and `backup` work with no key of your
|
|
22
|
+
own. `mcp serve` drops the agent's session secret instead of swapping it for a
|
|
23
|
+
credential, so that local secret is never forwarded upstream. Set it in `.env`
|
|
24
|
+
or with the new **`init --auth upstream`**, which needs no token, no browser
|
|
25
|
+
and no TTY, and verifies both backends through the proxy. Existing credentials
|
|
26
|
+
are kept, ignored, and reported as unused. Any other value of the variable is
|
|
27
|
+
an error on every verb — a typo must not silently restore the header.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **A REST 401 now says what is wrong.** It used to fall through to a bare
|
|
32
|
+
`401 Unauthorized` status line; it now names `N8N_API_KEY` as invalid for the
|
|
33
|
+
host (or, in upstream mode, points at the proxy).
|
|
34
|
+
|
|
35
|
+
## [0.11.0] - 2026-09-03
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **Breaking: a compiled `.ts` node now identifies itself on line 1, not on
|
|
40
|
+
its last line.** Open a bundled node in n8n and you used to see
|
|
41
|
+
`var __n8n_node = {}` followed by a few hundred lines of inlined helpers,
|
|
42
|
+
with the only trace of where it came from — `// @ts-n8n sha256:…` — at the
|
|
43
|
+
bottom, where nobody looks, naming *n8n* rather than the tool that put it
|
|
44
|
+
there. Push now writes one self-describing line where the editor opens:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// n8n-decanter · workflows/orders/code/normalize-lines.ts · do not edit here · @ts-n8n sha256:39af5ea6… · v0.10.1 ca3c201 2026-08-20T09:14Z
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Nothing you already pushed has to change.** The old trailing form is read
|
|
51
|
+
forever and counts as fully in sync — `push`, `diff` and `preflight` say
|
|
52
|
+
nothing about it, and no write is queued just to relocate a marker. Nodes
|
|
53
|
+
adopt line 1 on their next real code push; there is no migration and no mass
|
|
54
|
+
re-push. The break runs the other way: **an older n8n-decanter will not
|
|
55
|
+
recognise a node pushed by this version** (its reader only looks at the last
|
|
56
|
+
line) and would pull such a node down as a plain `.js` file. Pre-1.0, that is
|
|
57
|
+
accepted rather than shimmed — upgrade the CLI everywhere that pulls the same
|
|
58
|
+
instance. The `@ts-n8n` token itself is unchanged, so one `grep -r @ts-n8n`
|
|
59
|
+
still finds both forms.
|
|
60
|
+
|
|
61
|
+
- **The layout guard now rejects a marker line in a node source file in either
|
|
62
|
+
position, and in `.ts` files too** (it was trailing-only and `.js`-only).
|
|
63
|
+
That line is written by `push`; it is never source. Surfaces as before via
|
|
64
|
+
`preflight`'s `layout` check.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **An auth failure no longer sends you in a circle, and throttling is no
|
|
69
|
+
longer called an expiry.** Every failed OAuth token refresh used to print
|
|
70
|
+
the same sentence — `MCP session expired … re-run: n8n-decanter init` — and
|
|
71
|
+
both halves of it could be wrong. `init` reuses `.decanter-auth.json`
|
|
72
|
+
whenever the host matches and never re-mints, so following the advice
|
|
73
|
+
re-probed with the same dead credentials and finished with "credentials
|
|
74
|
+
written anyway"; the only apparent way out was deleting a credential file
|
|
75
|
+
that was often perfectly fine. The three cases are now told apart:
|
|
76
|
+
- **the refresh token really is spent** (`invalid_grant`) — named as such,
|
|
77
|
+
and pointed at `init --reauth`, a command that actually re-mints;
|
|
78
|
+
- **n8n is rate-limiting** (429) — now **retried** with the same backoff the
|
|
79
|
+
MCP endpoint has always had (honouring `Retry-After`, five attempts), and
|
|
80
|
+
if it still fails it says the credentials are fine and never mentions
|
|
81
|
+
`init`. OAuth discovery got the same retry; both sat on bare `fetch` calls;
|
|
82
|
+
- **anything else** — the reason is named without a diagnosis, and nothing
|
|
83
|
+
is suggested for discarding.
|
|
84
|
+
|
|
85
|
+
- **`init`'s closing connection check acts on a spent token instead of
|
|
86
|
+
shrugging.** It had the failure in hand and printed "credentials written
|
|
87
|
+
anyway". On a terminal it now offers to re-authorize on the spot; off one it
|
|
88
|
+
names `init --reauth`. Deliberately narrow — a network error, a 401, or a
|
|
89
|
+
403 "MCP access is disabled" still report as before, because none of them
|
|
90
|
+
says anything about your credentials.
|
|
91
|
+
|
|
92
|
+
- **A successful first OAuth consent no longer warns "no MCP credentials
|
|
93
|
+
yet".** The check looked for a *pre-existing* auth file, so a browser
|
|
94
|
+
authorization that had just succeeded still ended with advice to re-run
|
|
95
|
+
`init` with a token.
|
|
96
|
+
|
|
97
|
+
- **`<verb> --help` prints help instead of running the verb.** `--help` was
|
|
98
|
+
only recognised in argument slot 0, and every `--flag` is stripped before
|
|
99
|
+
dispatch, so `n8n-decanter init --help` was indistinguishable from a bare
|
|
100
|
+
`init` — a request for help **scaffolded a sync dir** into whatever directory
|
|
101
|
+
it was asked from. `--help` (and `-h`) now wins from any position, before any
|
|
102
|
+
verb, any namespace and the picker. It prints **that verb's own block** rather
|
|
103
|
+
than the whole listing, with only the notes that apply to it; `help <verb>` is
|
|
104
|
+
the same question, and a bare `help`/`--help` still prints everything.
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
|
|
108
|
+
- **`init` refuses to scaffold on top of a sync dir that already exists below
|
|
109
|
+
the target.** Run from the root of a bigger repo whose sync dir lives in
|
|
110
|
+
`n8n/`, `init` used to drop a second `decanter.config.json`, the template,
|
|
111
|
+
`workflows/`, `shared/`, `tsconfig.json` and the agent configs into the root.
|
|
112
|
+
It now looks a few levels down first and stops, naming the sync dir it found
|
|
113
|
+
and the `--dir=`/`N8N_DECANTER_DIR` form that addresses it from where you are
|
|
114
|
+
— the same advice every read verb already gives. A terminal gets it as a
|
|
115
|
+
question (`y` scaffolds anyway); a piped or flag-driven run exits 1 having
|
|
116
|
+
written nothing, not even the target directory. Re-running `init` **inside**
|
|
117
|
+
an existing sync dir is unchanged.
|
|
118
|
+
|
|
119
|
+
- **The provenance line carries the build stamp, not just the hash**: the
|
|
120
|
+
node's source path relative to the sync dir, "do not edit here", the CLI
|
|
121
|
+
version, the git commit and the push time. The commit is HEAD at build time
|
|
122
|
+
and renders as `ca3c201+dirty` when the sync dir has uncommitted changes —
|
|
123
|
+
which, with `commitOnPush` on, is the normal case and reads correctly as
|
|
124
|
+
"built from working-tree state on top of `ca3c201`". Fields that cannot be
|
|
125
|
+
known (no git repo, unreadable `package.json`) are simply left out. **None of
|
|
126
|
+
it is hashed**, so a rename, a new commit or a CLI upgrade never makes a node
|
|
127
|
+
look changed — only the code below line 1 does.
|
|
128
|
+
|
|
129
|
+
- **Credentials now resolve in a git worktree.** Both `.env` and
|
|
130
|
+
`.decanter-auth.json` are gitignored, so a fresh linked worktree had neither
|
|
131
|
+
and every credentialed verb died on `N8N_HOST must be set` — including the
|
|
132
|
+
`mcp connect` guard, which left agents in a worktree with no `n8n-instance`
|
|
133
|
+
tools at all. A worktree without its own credentials now reads the main
|
|
134
|
+
checkout's copies (same path, resolved from git's own worktree pointer, no
|
|
135
|
+
`git` subprocess). **A local file still wins**, so a worktree deliberately
|
|
136
|
+
aimed at another instance keeps its own, and nothing else is redirected —
|
|
137
|
+
`workflows/`, `.decanter.json` and `decanter.config.json` stay worktree-local.
|
|
138
|
+
For `.decanter-auth.json` the shared file is the only correct shape: the
|
|
139
|
+
refresh token is single-use and rotates, so **copying** it into a worktree
|
|
140
|
+
(what Claude Code's `.worktreeinclude` would do) forks it into two token
|
|
141
|
+
chains and kills the loser. New [Git
|
|
142
|
+
worktrees](/docs/concepts/configuration/#git-worktrees) section covers the two
|
|
143
|
+
remaining worktree gaps decanter cannot fix — a missing `node_modules` and the
|
|
144
|
+
per-path MCP approval — and the troubleshooting entry for missing
|
|
145
|
+
`n8n-instance` tools gained the worktree case.
|
|
146
|
+
|
|
147
|
+
## [0.10.2] - 2026-08-18
|
|
148
|
+
|
|
149
|
+
### Changed
|
|
150
|
+
|
|
151
|
+
- **"Missing `n8n-instance` tools? Restart" is no longer the only answer we
|
|
152
|
+
give — in a nested sync dir it was a dead end.** Agent wiring loads at
|
|
153
|
+
startup *from the dir the agent was started in*, so tools declared in
|
|
154
|
+
`.mcp.json` can be absent for two reasons: the wiring is new (a restart fixes
|
|
155
|
+
it), or the wiring sits **below** the launch dir, where no restart will ever
|
|
156
|
+
load it. Every surface that taught only the first now teaches both, with the
|
|
157
|
+
discriminator you can apply yourself — *is that `.mcp.json` below the
|
|
158
|
+
directory you started the agent in?* — and the two working routes (start the
|
|
159
|
+
agent in the sync dir, or wire the root with `N8N_DECANTER_DIR` plus a
|
|
160
|
+
root-resolvable command): the scaffolded `AGENTS.md`, `init`'s own output
|
|
161
|
+
(which now prints the nested guidance **instead of** the restart line when it
|
|
162
|
+
scaffolds into a nested dir), [Working with coding
|
|
163
|
+
agents](/docs/agents/overview/), [init](/docs/cli/init/) and a new
|
|
164
|
+
[troubleshooting](/docs/faq/troubleshooting/) entry. The CLI never needed
|
|
165
|
+
that wiring, so `pull`/`push`/`preflight` keep working either way — the docs
|
|
166
|
+
now say so too.
|
|
167
|
+
|
|
168
|
+
- **The oversized-scenario warning now names a remedy you can actually take.**
|
|
169
|
+
Above 1 MB `scenario create` said "Trim it before that" — but there is no
|
|
170
|
+
trim flag, so the advice pointed nowhere. It now says what is true and what
|
|
171
|
+
to do: nothing is committed yet (`scenario create` never commits — only the
|
|
172
|
+
next `pull`/`push` sweeps the folder into history), so you can still cut
|
|
173
|
+
items out of `data.resultData.runData` by hand, or re-create the scenario
|
|
174
|
+
with `--scaffold` instead of `--execution` and author the pins yourself.
|
|
175
|
+
`docs/cli/scenario.md` spells out both, including which parts of a capture
|
|
176
|
+
are unsafe to delete.
|
|
177
|
+
|
|
178
|
+
- **The agent contract now says to orient *before* the first edit, not only
|
|
179
|
+
before the push.** `preflight` was always the read-only report of the
|
|
180
|
+
instance's side (`drift`, `CONFLICT`, a pending `parity`), but every surface
|
|
181
|
+
framed it as the pre-push gate — so an agent edited first and learned about a
|
|
182
|
+
colleague's UI edit afterwards. The scaffolded `AGENTS.md` and both agent doc
|
|
183
|
+
pages now open the loop with it: on drift, `pull` and carry on; on a
|
|
184
|
+
`CONFLICT`, show `diff` and ask before either side is overwritten.
|
|
185
|
+
- **The scaffold stopped advertising a deny rule it no longer has.** The
|
|
186
|
+
Claude Code `settings.json` denies `.decanter.json`, `.env` and
|
|
187
|
+
`push --force` — but the scaffolded `CLAUDE.md` and `opencode.json` still
|
|
188
|
+
claimed `*.remote.js` was blocked too. Those conflict artifacts were removed
|
|
189
|
+
in the MCP pivot; both files now describe the policy that actually ships.
|
|
190
|
+
- **Docs: `mcp connect` / `mcp serve` no longer read as if the guard obtained
|
|
191
|
+
credentials itself.** "decanter's own credentials", "the agent never holds an
|
|
192
|
+
n8n credential" and "no secret to manage" led readers (and agents) to believe
|
|
193
|
+
the guard handles the n8n login. It does not: it only *reads* what `init`
|
|
194
|
+
wrote to `.env` / `.decanter-auth.json`, and can at most refresh an OAuth
|
|
195
|
+
token. The mcp-connect / mcp-serve pages and the template `AGENTS.md` now say
|
|
196
|
+
so outright — obtaining credentials is exclusively `init`'s job, and a
|
|
197
|
+
"no MCP credentials" answer means run `init`, not retry.
|
|
198
|
+
|
|
199
|
+
- **"decanter.config.json not found" now points at `init` — with its flags.**
|
|
200
|
+
The classic half-setup is a hand-written `.env`: an agent that cannot run the
|
|
201
|
+
browser OAuth flow asks its human to paste `N8N_MCP_TOKEN` into a file and
|
|
202
|
+
stops there, leaving no config, template, `.gitignore` or agent wiring behind.
|
|
203
|
+
The error now says the dir is not a sync dir yet, that `.env` alone is not
|
|
204
|
+
enough, and prints the prompt-free command that fixes it
|
|
205
|
+
(`n8n-decanter init . --host <host-url> --token <mcp-token>`). The docs
|
|
206
|
+
(init, configuration, troubleshooting, README) say the same thing: headless is
|
|
207
|
+
not a reason to skip `init` — it takes the same token as a flag.
|
|
208
|
+
|
|
209
|
+
- **`init`'s restart reminder now covers everything it wires, not just
|
|
210
|
+
permission rules.** MCP servers (`.mcp.json` / `opencode.json` — including the
|
|
211
|
+
guarded `n8n-instance` server), permission rules and hooks are all read at
|
|
212
|
+
agent **startup**, and `init` normally runs inside the session it configures.
|
|
213
|
+
The reminder now fires when any of those files is newly scaffolded and says
|
|
214
|
+
what it means: this session is still unconfigured, restart the agent (or
|
|
215
|
+
`/reload`). README, init and the agents docs say the same, and the scaffolded
|
|
216
|
+
`AGENTS.md` tells the agent to **ask for a restart** when the `n8n-instance`
|
|
217
|
+
tools are missing instead of connecting to the instance directly.
|
|
218
|
+
|
|
219
|
+
### Fixed
|
|
220
|
+
|
|
221
|
+
- **The scaffolded hooks now work when your sync dir is not where the agent was
|
|
222
|
+
started.** All three found the sync dir by assuming it was the current
|
|
223
|
+
directory, which only holds for an agent launched inside it. With the sync dir
|
|
224
|
+
nested in a bigger repo — a layout the docs explicitly allow — the agent runs
|
|
225
|
+
at the repo root and every one of them misbehaved: the rename-reference guard
|
|
226
|
+
became a **silent no-op**, so `$('Old Name')` references left behind by a
|
|
227
|
+
`renameNode` went unreported until a later `push` refused them; the verify hook
|
|
228
|
+
spawned the CLI without a directory, so it **blocked every node-file edit** with
|
|
229
|
+
a "not a sync dir" error; and the MCP routing check scanned the wrong tree.
|
|
230
|
+
Each hook now locates the sync dir from its own installed path, so it behaves
|
|
231
|
+
the same wherever the agent starts.
|
|
232
|
+
- **The routing check no longer misses direct-route servers in your user
|
|
233
|
+
config.** Its lookup for this project's entry in `~/.claude.json` matched the
|
|
234
|
+
current directory, but that file is keyed by the repository root — so in any
|
|
235
|
+
sync dir inside a git repo the check silently found nothing. It now matches the
|
|
236
|
+
project entry for the sync dir or any parent of it.
|
|
237
|
+
- **The verify hook finds a locally installed CLI.** It only ever looked for
|
|
238
|
+
`n8n-decanter` on `PATH`, so with a local (non-global) install it stayed quiet
|
|
239
|
+
and no verification ran at all. It now prefers the sync dir's
|
|
240
|
+
`node_modules/.bin` and falls back to `PATH`.
|
|
241
|
+
- **"decanter.config.json not found" no longer sends you to `init` when the sync
|
|
242
|
+
dir is simply somewhere else.** Run from *above* a perfectly good sync dir —
|
|
243
|
+
what happens whenever an agent starts at the repo root — the error read as if
|
|
244
|
+
nothing had ever been set up, and advised scaffolding a second sync dir on top
|
|
245
|
+
of the working one. It now looks below the directory it searched from, names
|
|
246
|
+
the sync dir it finds there, and prints the `--dir` / `N8N_DECANTER_DIR` form
|
|
247
|
+
that reaches it. When there really is no sync dir, the `init` advice is
|
|
248
|
+
unchanged.
|
|
249
|
+
|
|
250
|
+
### Added
|
|
251
|
+
|
|
252
|
+
- **The routing check also looks at parent directories, up to your repository
|
|
253
|
+
root.** Agents merge `.mcp.json` from every directory above the one they start
|
|
254
|
+
in, so a server pointing straight at your n8n instance can sit in the repo
|
|
255
|
+
root's config and still route this session. The scan stops at the repository
|
|
256
|
+
boundary, so it never reaches into unrelated parent directories, and an
|
|
257
|
+
offender found above the sync dir is named by its relative path
|
|
258
|
+
(`../.mcp.json`) so you can tell which file it means.
|
|
259
|
+
|
|
260
|
+
- **`--dir <path>` (or `N8N_DECANTER_DIR`) points any verb at a sync dir that is
|
|
261
|
+
not the current directory.** The layout the docs allow but nothing supported:
|
|
262
|
+
the sync dir nested inside a bigger repo, with the agent started at the repo
|
|
263
|
+
root. The MCP entry `init` writes into the sync dir is invisible from up
|
|
264
|
+
there, and hoisting it to the repo root spawned the guard where no
|
|
265
|
+
`decanter.config.json` could be found — so the guard did not work at all. The
|
|
266
|
+
search still only walks *up*; this says where it starts. In an agent's server
|
|
267
|
+
entry the environment variable is the form to reach for
|
|
268
|
+
(`"env": { "N8N_DECANTER_DIR": "flows" }`), and relative values resolve
|
|
269
|
+
against the working directory, so a repo-relative one keeps working for
|
|
270
|
+
everyone who clones. `init` does not take `--dir` — it still takes the
|
|
271
|
+
directory to scaffold as an argument.
|
|
272
|
+
|
|
273
|
+
- **`init` now tells you how to wire an agent when your sync dir is nested in a
|
|
274
|
+
bigger project.** Agents look for `.mcp.json`, `opencode.json` and
|
|
275
|
+
`.claude/settings.json` from the directory they were *started* in and never in
|
|
276
|
+
one below it, so everything `init` scaffolds is inert for an agent started at
|
|
277
|
+
the repo root above the sync dir. When init sees a project around it (a `.git` or
|
|
278
|
+
`package.json` in a parent) it prints both shapes that work: starting the
|
|
279
|
+
agent inside the sync dir — recommended, nothing further to configure — or the
|
|
280
|
+
paste-ready MCP, opencode and hooks/permissions blocks for the project root,
|
|
281
|
+
with every path and glob already prefixed. That prefixing is the point: copied
|
|
282
|
+
up verbatim, `Read(.env)` / `Edit(.env)` guard the *root's* `.env` and quietly
|
|
283
|
+
stop protecting your credentials. `init` prints this; it never writes into a
|
|
284
|
+
parent directory. Only on the run that first scaffolds the agent files, and
|
|
285
|
+
never for a standalone sync dir.
|
|
286
|
+
|
|
10
287
|
## [0.10.1] - 2026-08-16
|
|
11
288
|
|
|
12
289
|
### Added
|
package/README.md
CHANGED
|
@@ -37,6 +37,10 @@ clean git diffs.
|
|
|
37
37
|
- **Shared code and small libraries** — `.ts` nodes import helper files from
|
|
38
38
|
anywhere in the sync dir (`shared/` by default) and opted-in npm packages,
|
|
39
39
|
bundled on push to run anywhere, n8n Cloud included.
|
|
40
|
+
- **The deployed node says where it came from** — a compiled `.ts` node opens
|
|
41
|
+
in n8n with its source path, "do not edit here", the content hash and the
|
|
42
|
+
build stamp (CLI version, git commit, push time) on line 1, so a bundle full
|
|
43
|
+
of inlined helpers is never a mystery artifact.
|
|
40
44
|
- **Agent-native** — `init` scaffolds Claude Code / Cursor / Codex configs;
|
|
41
45
|
`preflight --offline` and `node run` give agents a credential-free loop.
|
|
42
46
|
- **Guarded agent access to n8n's MCP, wired by default** — the scaffolded
|
|
@@ -73,6 +77,13 @@ n8n-decanter init [dir] # OAuth in your browser (or a pasted MCP token)
|
|
|
73
77
|
n8n-decanter pull # pick a workflow from the list → it pulls
|
|
74
78
|
```
|
|
75
79
|
|
|
80
|
+
**Restart your coding agent after `init`** — the MCP servers, permission rules
|
|
81
|
+
and hooks it scaffolds are read at agent startup, so the session that ran `init`
|
|
82
|
+
is still unconfigured. If the sync dir is nested in a bigger repo and you start
|
|
83
|
+
the agent at the repo root, a restart is *not* enough — that wiring loads only
|
|
84
|
+
from the dir the agent starts in
|
|
85
|
+
([which file loads from where](docs/agents/overview.md#where-the-agent-wiring-loads-from)).
|
|
86
|
+
|
|
76
87
|
On a terminal, `pull` with no argument lists your n8n workflows — pick one and
|
|
77
88
|
it lands in `workflows/<slug>/`. Know the id already? `n8n-decanter pull
|
|
78
89
|
<id-or-name>` pulls it directly (scriptable, no TTY needed).
|
|
@@ -80,8 +91,12 @@ it lands in `workflows/<slug>/`. Know the id already? `n8n-decanter pull
|
|
|
80
91
|
set a bare `pull`/`push`/`diff` acts on.)
|
|
81
92
|
|
|
82
93
|
**Credentials:** OAuth by default (via `init`); `N8N_MCP_TOKEN` for
|
|
83
|
-
headless/CI
|
|
84
|
-
|
|
94
|
+
headless/CI — pass it to `init --host <url> --token <mcp-token>` rather than
|
|
95
|
+
hand-writing `.env`, which leaves the config, template, `.gitignore` and agent
|
|
96
|
+
configs unscaffolded; `N8N_API_KEY` is optional, needed only for `executions`,
|
|
97
|
+
`data-tables`, and `backup`. Behind a proxy that attaches the n8n credentials
|
|
98
|
+
itself, `init --auth upstream` sets `N8N_DECANTER_AUTH=upstream` and decanter
|
|
99
|
+
sends none of its own. Details: [Installation](docs/getting-started/installation.md),
|
|
85
100
|
[init](docs/cli/init.md), [Configuration](docs/concepts/configuration.md).
|
|
86
101
|
|
|
87
102
|
## Works with n8n's official skills
|
|
@@ -109,7 +124,7 @@ Full flag reference: `n8n-decanter --help`, or the
|
|
|
109
124
|
|
|
110
125
|
| Verb | What it does |
|
|
111
126
|
|---|---|
|
|
112
|
-
| `init [dir]` | Bootstrap a sync dir — OAuth/token, template, config, agent tooling (`--host`/`--token`/`--api-key` drive it non-interactively) |
|
|
127
|
+
| `init [dir]` | Bootstrap a sync dir — OAuth/token, template, config, agent tooling (`--host`/`--token`/`--api-key` drive it non-interactively; `--reauth` re-consents after a spent token) |
|
|
113
128
|
| `completion zsh\|bash` | Print a shell completion script |
|
|
114
129
|
| `pull [workflow…]` | Code + structure snapshot → `workflows/<kebab>/` |
|
|
115
130
|
| `push [workflow…]` | Push Code-node source to the workflow's **draft** |
|
|
@@ -132,6 +147,17 @@ renaming, and archiving workflows — and adding or renaming nodes — are
|
|
|
132
147
|
**n8n's acts**: do them in the editor or let your agent do it over n8n's MCP
|
|
133
148
|
tools (through the guard above); the next `pull` reconciles the local mirror.
|
|
134
149
|
|
|
150
|
+
**Sync dir nested in a bigger repo?** A global `--dir <path>` (or
|
|
151
|
+
`N8N_DECANTER_DIR`) starts the upward `decanter.config.json` search there
|
|
152
|
+
instead of in the working directory — every verb honours it except `init`,
|
|
153
|
+
which takes the directory to scaffold as its argument. A coding agent launched
|
|
154
|
+
at the repo root needs it too — agents look for `.mcp.json` and
|
|
155
|
+
`.claude/settings.json` from where they were *started*, never in a directory
|
|
156
|
+
below it, so either start the agent in the sync dir or wire the root
|
|
157
|
+
explicitly:
|
|
158
|
+
[Working with coding agents](docs/agents/overview.md#where-the-agent-wiring-loads-from),
|
|
159
|
+
[mcp connect](docs/cli/mcp-connect.md#when-your-sync-dir-is-not-your-project-root).
|
|
160
|
+
|
|
135
161
|
## How it compares
|
|
136
162
|
|
|
137
163
|
n8n-decanter is **Code-node-first**: it optimizes writing, typing, verifying and
|
package/dist/lib/api.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AUTH_MODE_ENV } from "./config.mjs";
|
|
1
2
|
/**
|
|
2
3
|
* n8n public REST API client — since Plan 33 only the surfaces MCP cannot
|
|
3
4
|
* serve: executions (no MCP read of full run data), data-table rows (MCP is
|
|
@@ -16,7 +17,19 @@
|
|
|
16
17
|
* fold into `dataTable:read`, so a key that lists tables fine still 403s on
|
|
17
18
|
* `/columns`.
|
|
18
19
|
*/
|
|
19
|
-
function scopeHint(method, pathname) {
|
|
20
|
+
function scopeHint(method, pathname, authMode) {
|
|
21
|
+
const scope = scopeName(method, pathname);
|
|
22
|
+
// In upstream mode the under-scoped key belongs to the proxy, so the scope
|
|
23
|
+
// name alone would send the reader to an `.env` that holds no key (Plan 92).
|
|
24
|
+
// Credentials mode keeps its exact previous wording.
|
|
25
|
+
if (authMode === "upstream") {
|
|
26
|
+
const where = "The key belongs to the upstream auth proxy, not to your .env — fix it where the proxy mints it.";
|
|
27
|
+
return scope === "" ? where : `${scope} ${where}`;
|
|
28
|
+
}
|
|
29
|
+
return scope === "" ? "Check the key's scopes in n8n → Settings → n8n API." : scope;
|
|
30
|
+
}
|
|
31
|
+
/** The scope a refused path needed, or `""` when the path is not one we map. */
|
|
32
|
+
function scopeName(method, pathname) {
|
|
20
33
|
const readOnly = " decanter only ever READS data tables — no write scope is needed.";
|
|
21
34
|
if (pathname.includes("/data-tables/") && pathname.includes("/columns"))
|
|
22
35
|
return `Add \`dataTableColumn:read\` (listing tables uses \`dataTable:list\`, which does NOT cover columns).${readOnly}`;
|
|
@@ -32,16 +45,18 @@ function scopeHint(method, pathname) {
|
|
|
32
45
|
return "Add `workflow:create` (`backup restore` redeploys a backup as a new workflow).";
|
|
33
46
|
if (pathname.startsWith("/api/v1/workflows"))
|
|
34
47
|
return "Add `workflow:read` (and `workflow:list` for init's connection check).";
|
|
35
|
-
return "
|
|
48
|
+
return "";
|
|
36
49
|
}
|
|
37
50
|
export class N8nApi {
|
|
38
51
|
#host;
|
|
39
52
|
#apiKey;
|
|
40
53
|
#timeoutMs;
|
|
41
|
-
|
|
54
|
+
#authMode;
|
|
55
|
+
constructor({ host, apiKey, requestTimeoutMs = 30_000, authMode = "credentials" }) {
|
|
42
56
|
this.#host = host;
|
|
43
57
|
this.#apiKey = apiKey;
|
|
44
58
|
this.#timeoutMs = requestTimeoutMs;
|
|
59
|
+
this.#authMode = authMode;
|
|
45
60
|
}
|
|
46
61
|
/**
|
|
47
62
|
* Recent executions, newest first. Read-only by design — the executions API is
|
|
@@ -134,7 +149,10 @@ export class N8nApi {
|
|
|
134
149
|
const res = await fetch(this.#host + pathname, {
|
|
135
150
|
method,
|
|
136
151
|
headers: {
|
|
137
|
-
|
|
152
|
+
// OMITTED, not blanked, in upstream mode: the proxy in front of n8n
|
|
153
|
+
// APPENDS its key to whatever the client sent, and n8n answers 401 to
|
|
154
|
+
// the pair (measured 2026-09-09). An empty header is still a header.
|
|
155
|
+
...(this.#authMode === "credentials" && { "X-N8N-API-KEY": this.#apiKey }),
|
|
138
156
|
accept: "application/json",
|
|
139
157
|
...(body !== undefined && { "content-type": "application/json" }),
|
|
140
158
|
},
|
|
@@ -147,8 +165,18 @@ export class N8nApi {
|
|
|
147
165
|
// and n8n does not say which scope is missing. Mapped HERE rather than
|
|
148
166
|
// in each caller so every REST surface (executions, data-tables, backup)
|
|
149
167
|
// gets it: `pathname` already identifies what was refused.
|
|
168
|
+
// 401 used to fall through to the bare status line below — the least
|
|
169
|
+
// helpful place to land, and the failure upstream mode makes most
|
|
170
|
+
// likely (a proxy that forwards but attaches nothing).
|
|
171
|
+
if (res.status === 401) {
|
|
172
|
+
throw new Error(this.#authMode === "upstream"
|
|
173
|
+
? `${method} ${pathname} was rejected (401) — decanter sent no API key by design (${AUTH_MODE_ENV}=upstream); the upstream auth proxy did not attach a valid one`
|
|
174
|
+
: `${method} ${pathname} was rejected (401) — N8N_API_KEY is not valid for ${this.#host} (n8n → Settings → n8n API)`);
|
|
175
|
+
}
|
|
150
176
|
if (res.status === 403) {
|
|
151
|
-
throw new Error(
|
|
177
|
+
throw new Error(this.#authMode === "upstream"
|
|
178
|
+
? `${method} ${pathname} was refused (403) — the API key the upstream auth proxy attaches is valid but lacks a scope. ${scopeHint(method, pathname, this.#authMode)}`
|
|
179
|
+
: `${method} ${pathname} was refused (403) — N8N_API_KEY is valid but lacks a scope. ${scopeHint(method, pathname, this.#authMode)}`);
|
|
152
180
|
}
|
|
153
181
|
throw new Error(`${method} ${pathname} failed: ${res.status} ${res.statusText}\n${text.slice(0, 2000)}`);
|
|
154
182
|
}
|