Command-line interface for MeshWeaver / Memex. Operates a portal's mesh over the REST API — read, search, mutate, compile, and mirror mesh nodes from the shell or from scripts.
Install it as a dotnet tool. Like gh, it is a thin client with its own version, released when the
CLI changes rather than with every platform build
(NuGet Package Retirement):
dotnet tool install -g MeshWeaver.Cli
dotnet tool update -g MeshWeaver.CliOr run it from a checkout: dotnet run --project src/MeshWeaver.Cli -- <command>.
In CI the same verbs come from the mw-plugin-test container image: every lane in this repository
and in every satellite invokes the image's entrypoint directly, so no lane installs the CLI.
Create an API token in your portal (Profile → API Tokens), then:
memex login mw_yourtoken --base-url https://portal.example.comThe token and base URL are stored in ~/.memex/config.json; $MEMEX_TOKEN / $MEMEX_BASE_URL or --token / --base-url override per call.
The whole CI contract for a plugin repo, in three lines:
- uses: actions/checkout@v7
- run: |
dotnet run --project "$MESHWEAVER/src/MeshWeaver.Cli" -- build plugin . \
--image meshweaver.azurecr.io/mw-plugin-test@${{ vars.MW_IMAGE_DIGEST }} \
--platform-image meshweaver.azurecr.io/memex-portal-ai@${{ vars.MW_PORTAL_IMAGE_DIGEST }}The tool pulls both images, pins them by digest, and runs the platform's own builder and tester inside them. Nothing in the job needs to know about docker, mounts, seeds or allow-files.
--image EXECUTES; --platform-image supplies the reference set. The tester image's /app is a
strict SUBSET of the portal's — 88 vs 219 assemblies on 3.0.0-rc9.ci.7534; MeshWeaver.Maps,
.AI, .ContentCollections.Indexing, the Blazor and hosting halves exist only in the portal — so a
build that compiled against the tester's could not see what every portal compiles against. Content
binding a portal-shipped assembly then fails with CS0234 … does not exist in the namespace 'MeshWeaver': a CONTENT-shaped failure with an INFRASTRUCTURE cause, on source nobody changed.
Both stages therefore run from a composed gate host — the portal's /app with the tester CLI
laid beside it, started by the portal image's own dotnet — composed by
.github/scripts/compose-gate-host.sh, the same file the reusable node-repo lanes fetch. The CLI
embeds that script and runs it; it does not carry a second copy of the rules.
🚨 There is no fallback to the tester's /app. Omit --platform-image and the build is REFUSED
before anything is pulled, and passing the tester image as the platform is refused by name. Pin the
two digests from ONE CD wave: the run asserts, with the tester's own framework-identity verb, that
the two images resolve ONE framework identity, and that the bake it produces is keyed to the identity
the platform host resolves.
It runs the two stages the plugin build contract defines, in order:
- produce —
compile /repo … --output /bake, then assert the bake identity (framework-mvid.txt) is non-empty. A compile that emitted no bundles still exits 0, so "the command returned" is not evidence. - consume — the gate with
--seed /bake, standing a mesh up on those bytes. Testing the baked bytes is a stronger claim than a fused pass: the bytes judged are the bytes that ship.
| option | meaning |
|---|---|
--image (required) |
the TESTER image (mw-plugin-test); pulled and pinned by digest. It executes the build |
--platform-image (required) |
the PORTAL image (memex-portal-ai); pulled and pinned by digest. It supplies the reference set, the framework identity and the runtime. No default, no fallback |
--bake-output <dir> |
where the bundles land (default: a temp dir) |
--external-modules <dir> |
module DLLs to mount at /ext |
--source-sha <sha> |
commit stamped into the bake |
--allow <file> |
allow-file relative to the plugin path (default plugin-gate.allow, used only if present) |
The images are arguments, not ambients: which images a plugin is built against decide the result, so they belong where a reader can see them. A gate that resolves its framework from an environment variable while advertising a branch name will eventually report a missing type against the wrong repository — which is exactly what happened on 2026-08-30.
Compile a .csproj against a MeshWeaver image's own assemblies — no dotnet SDK, no NuGet
restore, no platform source checkout:
memex build project ../MeshWeaver.Plugins/src/MeshWeaver.Import \
--image meshweaver.azurecr.io/memex-portal-ai:<tag> \
--extra-refs ./additional-libs --output ./outThe .csproj is evaluated without MSBuild, and every reference comes from the image's /app, its
.deps.json and the shared frameworks installed in it. ProjectReferences inside the source root
are built first in dependency order; a ProjectReference pointing outside it resolves to the
assembly the image already carries. Each dependent compiles against the full transitive output
closure, matching the SDK rather than only exposing its direct project's assembly. See
In-Mesh Build and Test.
| option | meaning |
|---|---|
--image <image> |
image to build against; pulled and pinned by digest. Omit ONLY when this command is itself running inside a MeshWeaver image — there is deliberately no local-SDK fallback |
--output <dir> |
where the emitted assemblies land (default: a temp dir) |
--root <dir> |
directory mounted as /repo (default: the nearest Directory.Build.props ancestor) |
--extra-refs <dir> |
libraries ADDITIONAL to the platform — the only way to satisfy a PackageReference the image does not supply. Repeatable |
--accept <construct> |
acknowledge one construct the evaluator cannot reproduce (target:<Name>, embedded-resource, conditions, razor-css-scope, razor-not-compiled). Repeatable |
--accept <construct> |
acknowledge one construct the evaluator cannot reproduce (target:<Name>, conditions, embedded-resource, embedded-resource:{resx,culture,dependent-upon,manifest-resource-name,build-output}). Repeatable |
--no-warn / --allow-warnings |
warnings fail the build (default); --no-warn=false or --allow-warnings opts out |
--no-pull |
use the image the docker daemon already has, for one built locally |
Razor/Blazor compiles (2026-08-31): the image ships the SDK's Razor source generator beside the
builder, per RID, and build-project runs it for any project whose Sdk processes Razor items —
MeshWeaver.Blazor (31 .cs + 42 .razor) builds green against memex-portal-ai. What it will
not do quietly is skip a .razor file: a project with Razor input and no generator fails by name,
and CSS isolation (*.razor.css) needs --accept razor-css-scope because the b-… scope comes
from an MSBuild task this builder does not run.
🚨 Nothing is dropped in silence. A project construct this builder cannot reproduce — an unknown
element, an unevaluatable Condition, a <Target>, a resource whose SDK manifest name cannot be
matched exactly — FAILS the run naming
the construct and the file, and a PackageReference the container does not supply is reported by
name rather than skipped. A silently dropped Nullable or NoWarn produces a build that looks green
and is not the build the SDK would have produced.
The verb ships in MeshWeaver.Cli, so dotnet tool install -g MeshWeaver.Cli needs a release that
contains it. From a platform checkout it runs directly:
dotnet run --project src/MeshWeaver.Cli -c Release -- \
build plugin ../MeshWeaver.Plugins \
--image meshweaver.azurecr.io/mw-plugin-test:<tag> \
--platform-image meshweaver.azurecr.io/memex-portal-ai:<same-wave-tag>Same behaviour, no install — useful for trying it on a repo before the tool version is out.
| Command | Purpose |
|---|---|
get <path> |
Read a node or resource by path |
search <query> |
Search the mesh (GitHub-style query, e.g. nodeType:Agent) |
create -f node.json |
Create a node from a JSON file |
update -f nodes.json |
Full-replace update from a JSON array file — every field the file omits is still written, so a concurrent writer's field is reverted |
patch <path> … |
Partial update of a node's top-level fields (content deep-merged per RFC 7396) |
delete <paths…> |
Delete nodes (recursive) |
move <src> <dst> / copy <src> <ns> |
Move / copy a node and its descendants |
upload <path> <file> |
Upload a file into a node's content collection |
compile <path> / diagnostics <path> |
Compile a NodeType and inspect diagnostics |
execute-script <path> |
Run an executable Code node through the kernel |
mirror push|pull <remoteUrl> <source> |
Mirror a subtree between two portals |
recycle <path> |
Force a fresh hub initialisation |
navigate-to <path> / base-url |
Print the browser URL for a path / the portal base URL |
All commands print the server's JSON verbatim, so output pipes cleanly into jq. Errors go to stderr with a non-zero exit code.
update is the full-entity write and is only correct when you are the sole authority for that
node's content — a one-way sync source, or a buffer that is the new content. patch is the right
default for "change these fields, leave the rest alone".
Two things neither can do, both by construction:
- Neither can express a fold.
count + 1has to be computed by you from a read, which a second writer can invalidate before your write lands. The agent/MCP surface has the same gap; only in-process C# (GetMeshNodeStream(path).Update(lambda)) folds against the live node. - Neither splices text. Both replace a text field wholesale, so editing one line of a long
markdown body or code source means re-emitting the document. The agent/MCP surface has an anchored
edit_contentfor this; the CLI has no equivalent verb yet.
The full design — the four shapes a write can take and which context may use which — is
Doc/Architecture/ExpressingAWrite.
MeshWeaver source and documentation: https://github.com/Systemorph/MeshWeaver — or browse the live docs at https://memex.meshweaver.cloud.