v0.4.13: the dependencies a fetch cannot bring
What's Changed
provider update— changemodel,endpoint,parametersormaxRoundson an existing profile, with one patch policy shared by all three hosts that apply oneplugin addover http installs the dependencies a source-fetch cannot bring — into the plugin's own cache root, not your project- Three silent failures on that path — an unreachable remedy, a rethrow that discarded the error's identity, and a singleton link that depended on an unrelated dependency being declared
provider removeno longer swallows the config section that follows the last profile
Full Changelog: v0.4.12...v0.4.13
What it's for
A plugin fetched from a URL brings one package's own files and no dependency graph. That is by
design — the route crawls imports and mirrors them into .plugins/, with bare specifiers bridged to what
the host already has — but it meant any plugin with real dependencies could be fetched, verified, written
to config, and then fail to activate on its first unresolved import. The route worked for plugins that
needed nothing, which is not most of them.
plugin add now resolves what such a plugin declares, asks once with the full transitive list, installs,
and retries activation in process. Declining changes nothing.
Where those dependencies land is the substance of it. They go to the plugin's own cache root under
.plugins/, through the same plan/apply a local plugin already used. Not your project: pnpm add refuses
outright at a workspace root, and where it succeeds it writes a fetched plugin's dependencies into a
tracked manifest and lockfile that have nothing to do with them. Not the .plugins/node_modules link farm
either — that holds the host singletons and the plugin self-links, and an install there would prune them
as extraneous. The cache root sits earlier in the resolution walk-up than the farm, so what lands there
is reachable from that plugin and invisible to everything else. It is also removed with it: evicting a
plugin takes its dependencies, and rm -rf .plugins/ clears every plugin's without touching matbot's own
node_modules.
Three things that failed by saying nothing
Getting there meant fixing a chain in which each link failed silently — the output looked like a
considered answer rather than a missed branch, which is why none of it had surfaced.
The remedy that names a missing package matched Node's Cannot find package 'x'. A bare import from
inside .plugins/ never reaches Node's error: ts-hooks retries it against the host's graph first and
throws its own Cannot resolve "x". So the branch was dead for the one route it exists for, and the
generic fallback still printed the underlying text.
Behind that, loadPlugins with onLoadError: 'throw' built a bare new Error(message) — everything but
the text was lost, including the ERR_MODULE_NOT_FOUND code the remedy tests before it looks at the
wording. Fixing the wording alone could not have helped. The wrapper now carries the original as cause
and copies its code, and the reader walks the cause chain rather than trusting one layer to remember.
And applyProvision linked the host singletons only when it had something to npm ci. The simplest
plugin there is — one whose only dependency is the @matatbread/matbot-plugin-api peer — got no
node_modules and no link at all, and resolved the singleton by whatever happened to sit above it on
disk. Every plugin in this repo has dependencies, which is why nothing caught it. Whether an unrelated
third-party dependency is present cannot be what decides if the host's copy is reachable. Two things
behind that are fixed with it: the link target asked "what would a plugin here get" (an author's
devDependencies copy, if installed) rather than "what does the host have", and "a path already exists"
is now "a path leads to the host's copy", so a second physical copy of a singleton is replaced instead of
kept.
provider update
A provider renaming its models left no way to change model: on a profile, and remove + add could not
serve: provider list projects credentials down to hasCredentials, so an LLM re-adding a profile has
no ${NAME} reference to write back and must re-prompt for a key that was never lost — the vault kept it,
the reference did not survive the round trip.
update takes the fields a caller can read back and passes the credential map through verbatim. null
clears a field, absent leaves it alone. The patch semantics live in plugin-api beside
applyCreateSecret, because three hosts apply a patch — the node tool against matbot.yaml, the browser
bootstrap against localStorage, the Drive backend against its manifest — and what a patch means must not
be one of the things they differ on.
It also exposed a pre-existing provider remove bug: the block remover matched every following line that
did not begin <non-space>, which a top-level key does not. Deleting the last profile also deleted
the header of whatever came next and left that section's children indented under providers: — so
default_settings: ceased to exist and a settings namespace became a provider profile, in a file that
still parsed.