@m6d/cortex-cli
v0.21.0
Published
Scaffold Cortex projects and manage a Cortex deployment
Readme
@m6d/cortex-cli
The cortex command line: scaffold a Cortex project, and manage a Cortex deployment from the
terminal.
bunx @m6d/cortex-cli new my-app # from nowhere — scaffolds a project
bunx cortex agents list # against the Cortex you logged in to
bunx cortex apply # make it match ./cortex.manifest.tsCommands are noun-first, cortex <resource> <procedure>, except new, which acts on no existing
resource, setup and apply, which act on a whole deployment, and login, logout and status,
which act on your profiles. Run cortex --help, then cortex <resource> --help.
bun add -d @m6d/cortex-cliRequires Bun. The binary re-execs itself under Bun when Node started it, and tells you how to install Bun when it is genuinely absent.
cortex new
Scaffolds ./<name>: a backend for Cortex agents, and Cortex itself to run them on.
server.tsis a Cortex server built withdefineServerfrom@m6d/cortex: one variable (userName) and one native tool (rollDice), mounted at/cortexbyindex.ts.cortex.manifest.tsdeclares ahelperagent on that server, forcortex apply.docker-compose.ymlruns Cortex's app, worker and engine on the onesyscodeia/corteximage, with Postgres, Redis and MinIO. Only the app and the engine publish host ports. They and the backend's port are picked at scaffold time from what was free..envholds every setting: Cortex's secrets, the backend's key and name, the model provider, and theCORTEX_URLandCORTEX_KEYthe CLI reaches this checkout's Cortex with. It is not committed:setup.tswrites it from.env.exampleon everybun install, generating each secret left blank, so a fresh clone gets its own secrets and a stack that never shares the original's volumes..dockerignorekeeps both out of images.bun run devstarts Cortex (docker compose up), the backend andbun cortex setup --wait --yesin an mprocs dashboard.bun run dev:agentruns the same throughconcurrently, one labelled log for coding agents and CI.- oxlint, prettier, cspell, fallow and tsconfig are inlined, so
bun run verifyworks standalone.
It then runs bun install, and git init unless the target is already inside a work tree. An
existing non-empty directory is an error. So is a name like cortex or cortex., since the
manifest takes its id from the name and Cortex keeps the id cortex for its own.
bunx @m6d/cortex-cli new shop --datasetsOn a terminal it asks only what the flags left open: the name, whether to set up the helper agent
on the first bun run dev, and if so the model provider (name, base URL, API key, model). It checks
the provider with one chat message to the model, and reuses it for embeddings when it serves the
stack's embedding model. With no terminal (stdin and stderr) to ask on, it never prompts and behaves
as --yes, so a missing name is a hard error rather than a hang.
| Flag | Default | Writes |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------- |
| --datasets | off | datasets.ts, an in-memory orders table served as a dataset host, and its manifest entry |
| --setup / --no-setup | on | cortex setup among the processes bun run dev starts |
| --yes | | Accept the defaults instead of asking; setup then runs with no model provider |
The image runs at the Cortex release that was current when this CLI was published
(CORTEX_VERSION in the shell picks another); the backend depends on the @m6d/cortex release of
that time. The server is registered under the project name as the manifest id takes it:
lowercase words joined by hyphens, cut to Cortex's 64 characters.
cortex setup
Registers a cortex new project's backend in its Cortex, then applies its manifest. It reads the
backend's name, URL and key (CORTEX_SERVER_*) and the model provider (MODEL_PROVIDER_*) from
.env, and goes in the one order Cortex accepts: the provider's key and the provider, the server
key's secret and the server on the provider's model, a sync of the server, then the apply. Each step
creates what is missing and keeps what exists, so a second run changes nothing. A key's secret is
named for its value (openai-api-key-1a2b3c4d), so a changed key gets a new secret the provider or
server moves to, and whatever else presents the old one keeps it. A provider or server under
.env's name that calls another URL belongs to something else: setup leaves it alone and stops. So
does a MODEL_PROVIDER_* set only in part, or a provider URL on this machine or a private network
that EGRESS_ALLOWED_HOSTS in .env does not admit. A server with no model yet gets the provider's.
bun cortex setup # asks for a model provider when .env has none
bun cortex setup --wait --yes # what `bun run dev` runs--wait waits up to 10 minutes for Cortex to answer, long enough for a first start to pull the
image. Without -p, setup sets up the Cortex .env names: a different CORTEX_URL exported in the
shell, which Bun lets win over .env, ends the run. The retried sync, a minute while Cortex cannot
reach the backend, gives up at once on any other refusal. With no model provider and --yes, it
registers the server without a model and still applies, since an agent may name its own; when
Cortex refuses an agent for want of one (chat_model_required), nothing is applied and setup says
how to add a provider. The apply
deletes what the manifest created and no longer lists, so like cortex apply it asks on a terminal
before it writes anything, and needs --yes without one. A field the manifest changes that was
also edited live is asked about on a terminal. Under --yes, as bun run dev runs it, such a
conflict stops every run until bun cortex apply --yes --prefer live (or manifest) settles it.
The run that first creates something says how to sign in, with the password only on a terminal.
Management commands
Manage a Cortex deployment from the terminal. The commands come from
Cortex's own management API (/api/management/openapi.json), fetched
on each run and cached, so the tree always matches the deployment you point
at and a new procedure needs no CLI release.
cortex login # asks for the URL and a management key
cortex status
cortex agents list
cortex agents create --name support --displayName '{"en":"Support"}' --serverId <uuid>
cortex tools create --json tool.json --publish
cortex tools list -o json | jq '.items[].name'
cortex logoutProfiles live in ~/.cortex/profiles.json (owner-only). -p, --profile <name>,
a global option, picks one for any command, and names the one login saves
and logout forgets (default otherwise). CORTEX_URL and CORTEX_KEY, both
set, win over the current profile, for CI and for a project's .env; a
profile -p names wins over them. CORTEX_HOME relocates the directory.
The grammar is cortex <router> <procedure> [id] [flags], with routers as
the API names them (datasetHosts, modelProviders). Each top-level input
field is a flag typed from the API's schema; nested values take JSON, and
--json <file|-> supplies a whole body that flags then override. A required
field left out, id included, is asked for on a terminal and named as the
missing flag or positional otherwise. Listings take --pageIndex, --pageSize, --q, and --all to
walk every page. -o json prints the raw result for pipes.
Writes to agents, tools, services, skills, triggers, MCP servers, and dataset
hosts save drafts.
--publish on a write the API marks publishable (update, and a create
whose result is complete enough) runs that entity's publish procedure right
after; without it the command says so and names the command. Fields the API
marks live (x-live-fields) take effect without a publish, from agents'
next turn, and the command names them: a trigger's
credentialName, fireCredentialName and model, say, or a dataset host's
url, headers and timeoutMs. cortex datasetHosts sync <id> reads a host's manifest, and a
draft host publishes only after datasetHosts test reached it once.
A procedure the API marks destructive (x-destructive: delete, publish, rotate, toggle and the
like) asks before anything is sent, and so does a --publish that runs one. --yes skips the
question. With no terminal to ask on, the command fails unless --yes is given, so scripts and CI
pass it. Questions render on stderr, so -o json output stays parseable.
cortex apply
Makes Cortex match a manifest, in one transaction: what is new is created, what changed is updated and republished, and what the manifest created and does not list is deleted. Rows it did not create are never touched, and a name one of them holds fails the apply.
cortex apply # ./cortex.manifest.ts
cortex apply deploy/manifest.json --dry-run
cortex apply -p prod --yes # no question, as CI needs
cortex apply --prefer live # keep every live edit the manifest also changedAn entry's overridable lists the fields people may change in Cortex, such
as an agent's prompt. An apply keeps those edits. When the manifest changes
a field that was also edited live, cortex apply asks which to keep for
each one. --prefer live or --prefer manifest answers them all. --yes
never asks: under it, as without a terminal, an apply with conflicts and no
--prefer changes nothing and lists them. --dry-run marks each with !.
The path defaults to exactly ./cortex.manifest.ts in the directory you run
in. TypeScript and JSON load the same way: the file's default export is the
manifest. defineManifest from @m6d/cortex types one as you write it:
import { defineManifest } from "@m6d/cortex";
export default defineManifest({
id: "shop",
tools: [
{
type: "http",
name: "findOrder",
description: "Find one order by id.",
url: "https://shop.example.com/orders/{orderId}",
method: "GET",
readOnly: true,
inputSchema: {
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"],
},
audience: { agents: ["support"] },
},
],
});An openapi source makes every operation of an OpenAPI document a tool the
manifest owns, so a backend with many endpoints needs no entry per tool:
openapi: [
{
name: "shop",
url: "https://shop.example.com/openapi.json",
headers: [{ header: "Authorization", prefix: "Bearer ", from: { kind: "runToken" } }],
// For an API that wraps every answer: where each one keeps its result.
// resultPath: "/data",
pinned: ["findOrder"],
// A GET runs at once; any other method waits for the end user's approval.
approval: { never: ["addOrderNote"], always: ["exportOrders"] },
audience: { agents: ["support"] },
},
],Every apply reads the document first, without the source's headers. An
operation it adds is created, a changed one updated, and a dropped one
deleted. A source whose headers carry a credential or the run token is read
only over https, or from a host the deployment allows, since the document
says where they go. A source needs the tools write permission even when nothing
changes. Each tool is named after its operationId, made an identifier of at
most 64 characters; an operation Cortex cannot import is left
out. Two operations that come out with one name are told apart by a number
(getOrders, getOrders2) in the order of their paths. The apply fails
when pinned or approval names a tool no operation has (unknown_tool)
or any name such a collision decided (ambiguous_tool), since a path the
document adds or drops could move it to another endpoint. It also fails when an
operation's name is already a listed tool's or another source's.
A collection lists its documents. Each is inline text, a file path
relative to the manifest file, or a url Cortex reads on every apply, with no
credentials:
collections: [
{
name: "Policies",
audience: { agents: ["support"] },
documents: [
{ name: "faq.md", text: "# FAQ\n\nReturns take 30 days." },
{ path: "./docs/returns.pdf" },
{ url: "https://shop.example.com/policies/shipping.html" },
],
// overridable: ["documents"],
},
],A document's name sets its type: .md, .txt, .html, .htm, .pdf or
.docx. A path or URL document is named after its file unless name says
otherwise. Each is at most 10 MB. cortex apply reads paths and sends their
bytes, so a request carries every inline and path document again; Cortex
stores only what changed. A document with new bytes replaces the old one,
which keeps answering until the new one is processed, and one the manifest
drops is retired.
Listing documents makes the manifest own them: an apply retires a document
uploaded in the console, the first apply after upgrading included, unless
the collection offers documents in overridable. Offered, a document
uploaded or removed in Cortex stays so until the manifest changes that same
document, which cortex apply then asks about.
The manifest is checked against the contract before anything is sent, then
sent as written, so Cortex fills its own defaults. It goes to the
management API (manifests/sync, or manifests/preview with --dry-run,
which changes nothing) under the same profile the management commands use, so
the key needs manifests:apply and the write permission of every kind the
apply changes. The output lists each changed entry under its kind: +
created, ~ updated, - deleted. A trigger the apply creates, or whose
fireCredential it drops, gets a fire secret, printed once below the summary
and never again.
Since a sync deletes, cortex apply asks before sending it. --yes skips the
question, and is required with no terminal to ask on. --dry-run never asks.
References between entries (mentions, service scopes, which role a credential or secret plays) are judged on the state the apply leaves, so the order the manifest lists them in does not matter. A dataset host's headers are the exception: its catalog is read with them before the apply, so they are judged on the deployment as it stands. A credential a trigger fires with cannot go to a host in the same apply that drops it; drop it first, then apply again.
Global options
| Option | Description |
| ---------------------- | -------------------------------------------- |
| -v, --verbose | Print full errors and the commands being run |
| -p, --profile <name> | Which saved profile to use |
| -V, --version | Output the version number |
| -h, --help | Display help for command |
Exit codes
| Command | 0 | 1 |
| ------- | ---------------------------------------------------------- | --------------------------------------------------------- |
| new | Files written — even if bun install failed | Bad target, missing name, cancelled |
| setup | Set up, or the server registered with no model to apply on | Not a project, Cortex refused or unreachable, or declined |
| apply | Applied, or previewed with --dry-run | Invalid manifest, refused, or declined |
| others | The procedure ran | It failed, or couldn't run |
