Appearance
Build the CLI from source
This page is for contributors: it takes a fresh clone of the GAIA repository to a working gaia on your PATH, with no container or worktree prerequisite.
You probably don't need this
If you just want to use GAIA, install the published CLI instead — no clone, no pnpm, no build:
sh
npm install -g @gaia-ai/gaianpm resolves the whole package set for you. See Install the CLI. Building from source is only for working on the CLI.
Prerequisites
Node.js ≥ 20.19 (the
enginesfloor of every package in the workspace).pnpm — the workspace package manager. If it is missing:
shcorepack enable pnpm # ships with Node >= 16.9 (recommended) npm install -g pnpm
The bootstrap script checks for both and prints an actionable message if either is missing.
Bootstrap a fresh clone
From the repository root:
sh
gaia-cli/scripts/setup.shor, equivalently, from the gaia-cli/ directory:
sh
cd gaia-cli && pnpm run bootstrapThe script is idempotent — re-run it any time, for instance after pulling changes that touch any package or addon.
When it finishes you have a gaia on your PATH backed by your checkout's build. To point it at a control plane and log in, follow Install the CLI — connecting is the same for a source build as for a global install.
What the bootstrap does
gaia-cli/scripts/setup.sh resolves the repository root from its own path (it does not depend on git) and runs eight idempotent steps.
1. Prerequisite check — verifies pnpm is on PATH and sanity-checks Node ≥ 20.19, exiting non-zero with an actionable message if not.
2. Install — pnpm install --config.minimumReleaseAge=0 in gaia-cli/. The minimumReleaseAge=0 dodges the workspace's supply-chain deps-verify policy, which would otherwise fail a plain install on a fresh tree.
3. Build the packages and addons — in layered dependency order, each step emitting the .d.ts files the next one consumes:
core— the surface-agnostic kernel, with no@gaia-ai/*edge of its own;conductor— the run engine, which owns the conductor-surface contract (@gaia-ai/conductor/contract) that every conductor-surface addon imports, so it must precede the addons;addons/*— withworkspace-gitforced first, becauseaddon-herdrtakesloadInstructionsfrom it (the one permitted addon-to-addon edge);ui— imports@gaia-ai/addon-herdr;host— the leaf sink; imports@gaia-ai/ui/presetplus every addon preset.The script invokes
tscdirectly rather thanpnpm run build, to stay fresh-tree-safe.
4. Resolution farm — creates a repository-root node_modules/ symlink farm so a .gaia/conductor.config.js can resolve its bare imports. Node's ESM resolver walks up from .gaia/ looking for @gaia-ai/gaia, @gaia-ai/addon-* and @dropsh/*; the root farm is where it lands. Each package is linked under its real package.json name, not a directory-derived one — @gaia-ai/ui lives beside addons/, while addons/gaia-ui is @gaia-ai/addon-gaia-ui:
| Import | Symlink target |
|---|---|
@gaia-ai/core | gaia-cli/core/ |
@gaia-ai/conductor | gaia-cli/conductor/ |
@gaia-ai/gaia | gaia-cli/host/ |
@gaia-ai/ui | gaia-cli/ui/ |
@gaia-ai/addon-<name> | gaia-cli/addons/<name>/ |
@dropsh/<pkg> | gaia-cli/node_modules/@dropsh/<pkg>/ |
The farm lives under the already-gitignored node_modules/, so it never dirties the tree.
5. PATH shim — installs a test-workspace-aware gaia to ~/.local/bin/gaia (see below). If a differing gaia already sits there it is left untouched — the script warns and skips, and never clobbers; the warning prints the one command that adopts the new one (see Adopting a new shim below). If ~/.local/bin is not on your PATH it prints the export line to add; if a global gaia sits ahead of ~/.local/bin on PATH, shadowing the shim, it warns about that too.
6. Config scaffold — writes .gaia/conductor.config.local.js from gaia-cli/templates/conductor.config.local.js, leaving an existing file untouched. Interactive shells are prompted for machine_id (default: the short hostname) and oauth.client_id (default: gaia-agent); non-interactive shells take the defaults. No secret is ever written — the scaffold reads client_secret from process.env.GAIA_CONSUMER_SECRET.
7. herdr run-session plugins — links the repository-local spiceedit herdr plugin that powers the review click-to-edit overlay. Best-effort and guarded: skipped when herdr is not on PATH (it is an executor, not a CLI prerequisite), and a missing spiceedit binary only prints a note. Nothing here can fail the bootstrap.
8. Summary — prints the remaining steps to connect and authenticate.
Monorepo layout
gaia-cli/
├── core/ # @gaia-ai/core — the surface-AGNOSTIC kernel: host contract, split-config
│ # helpers, conductor registry, utils, workflow step-contract,
│ # and the generic preset/discovery machinery. No plugin
│ # implementations, no conductor-surface interfaces, no
│ # config contract. Zero @gaia-ai/* edges.
├── conductor/ # @gaia-ai/conductor — the run engine + the `conductor` command plugin + the
│ # CONDUCTOR-SURFACE CONTRACT and the config contract
│ # (`@gaia-ai/conductor/contract`)
├── ui/ # @gaia-ai/ui — the project-first `gaia ui` cockpit command app
├── host/ # @gaia-ai/gaia — the plugin HOST (`bin gaia`) + `gaia upgrade`; the leaf sink
├── addons/* # @gaia-ai/addon-* — every mountable, one package per addon
├── scripts/ # setup.sh, prepare-publish.mjs, release.mjs, verify-package.mjs, assert-acyclic.mjs
└── templates/ # conductor.config.js, gaia.config.js, conductor.config.local.js, gaia.pathshim.shgaia-cli/ itself is a private workspace root and is never published. Its pnpm workspace globs are core, host, conductor, ui, addons/*.
17 packages are published: @gaia-ai/core, @gaia-ai/conductor, @gaia-ai/ui, @gaia-ai/gaia, plus 13 @gaia-ai/addon-* packages — auth-basic, claude, codex, deployment, dropsh, essentials, gaia-ui, herdr, kimi, opencode, pi, remote-drupal, workspace-git. There is a 14th addon directory, addons/fake (@gaia-ai/addon-fake), which is workspace-only: a test fixture, never published.
Addons and plugins
Two levels, and the distinction matters when you read a config or a preset:
- An addon is a package under
addons/*. It self-declares its contributions through a fixed./presetexport, and it is what you list in anaddons: []config array. One addon may ship several plugins —@gaia-ai/addon-herdrships an executor and a workspace. - A plugin is the single typed per-surface contribution:
RemotePlugin,ExecutorPlugin,WorkspacePlugin,AgentPlugin(conductor surface),GaiaCommandPlugin(command surface),DropSHPlugin/connectionPlugins(connection surface).
The *Plugin interface names and the /plugins subpath barrels keep the word "plugins"; only the package prefix is @gaia-ai/addon-.
Writing an addon
Peer-depend on @gaia-ai/core and — if the addon contributes to the conductor surface — on @gaia-ai/conductor too, then import the interfaces from the runtime-light subpath:
ts
import type { ExecutorPlugin, GaiaExecutor } from '@gaia-ai/conductor/contract';@gaia-ai/conductor/contract carries only the surface interfaces, the config contract, the preset view and two tiny helpers — no engine, no commander, no dropsh runtime — so importing it costs nothing at boot, unlike the package main.
Export the addon's contributions from src/preset.ts behind the package's ./preset export. gaia-cli/scripts/assert-acyclic.mjs reads those accumulator exports to decide which peer dependencies the addon owes — a name-pattern-free ratchet, so a conductor-surface addon that forgets its @gaia-ai/conductor peer fails the guard. That script also enforces the layering: the kernel has zero @gaia-ai/* edges, nothing depends on the @gaia-ai/gaia sink, and an addon's edge set stays within {core, conductor, addon-dropsh} plus the single permitted sibling edge.
The gaia on your PATH
There is a single gaia on your PATH — no separate gaia-dev. That one gaia is a test-workspace-aware shim, backed by two thin launchers with different jobs:
~/.local/bin/gaia— the shim installed by step 5 (canonical source:gaia-cli/templates/gaia.pathshim.sh), placed ahead of the npm global onPATH. It resolves in two tiers (GAIA-390): inside a skill-test workspace — recognised by a.gaia-testbuildmarker file at or above the physical working directory, written byscripts/setup-test-project.shand naming the checkout that created the workspace — it runs that checkout's builtgaia-cli/bin/gaia, and a marker naming an unbuilt checkout fails loudly with a build hint rather than falling through to a differentgaia; everywhere else — inside a plain gaia checkout included — it runs the globally-installed@gaia-ai/gaia. So a baregaia <cmd>is the released CLI unless you stand in the one place a branch build is under test.The lookup walks the filesystem, not git history: the conductor's per-ticket worktrees live inside the workspace, where a git-based lookup would stop at the worktree's own toplevel and never see the marker.
gaia-cli/bin/gaia— the dev launcher. It runs the built plugin host (host/dist/src/host.js), which lazily mounts only the invoked command plugin (conductor,ui,dropshordeployment) — so it needs the packages built by step 3. The shim execs it inside a test workspace; anywhere else, call./gaia-cli/bin/gaiaby explicit path to exercise a checkout's own build. Tier 1 used to key on the git toplevel, which meant an unbuilt or broken branch build broke every unrelatedgaiacommand in that worktree.
Adopting a new shim
Step 5 never overwrites a differing ~/.local/bin/gaia, so a changed template does not reach an existing install by re-running setup.sh. Adopt it explicitly — the WARN prints this line for you:
bash
cp gaia-cli/templates/gaia.pathshim.sh ~/.local/bin/gaia && chmod 0755 ~/.local/bin/gaiaDiff first if you have edited yours: diff ~/.local/bin/gaia gaia-cli/templates/gaia.pathshim.sh.
PATH-order hazard
The shim only wins if ~/.local/bin precedes the active Node's bin directory — where npm install -g @gaia-ai/gaia lands — on PATH. If the global bin comes first, the released CLI shadows the shim and a bare gaia in a test workspace never sees the build under test. Step 5 detects and warns about this ordering; fix it with:
sh
export PATH="$HOME/.local/bin:$PATH".gaia/conductor.config.local.js
A checkout's .gaia/conductor.config.js is committed. It loads an optional per-developer override, .gaia/conductor.config.local.js, which is gitignored and read only if present. The scaffold carries only non-secret fields:
| Field | Purpose |
|---|---|
machine_id | this node's identifier |
oauth.client_id | the OAuth consumer (default gaia-agent) |
oauth.client_secret | read from process.env.GAIA_CONSUMER_SECRET — never committed |
Export the secret before running the conductor:
sh
export GAIA_CONSUMER_SECRET=<your consumer secret>The config model itself — the connection config gaia.config.js versus the engine config conductor.config.js — is documented in Connection config and Engine config.
Rebuild after editing sources
The CLI runs the compiled dist/, so an edit is invisible until you rebuild:
sh
cd gaia-cli && pnpm install && pnpm -r run buildpnpm run build:all is an alias for the same thing. Or just re-run gaia-cli/scripts/setup.sh — it is idempotent, refreshes the resolution farm and the shim too, and builds in the layered order core → conductor → addons → ui → host, which a bare pnpm -r does not guarantee on a fresh tree.
Check the publish shape locally
Before a release, reproduce what npm consumers will actually get:
sh
cd gaia-cli && pnpm run verify:packageIt builds every package, stages a fresh publish manifest for all 17, npm packs each one, and installs the whole set into a throwaway project with plain npm install — no pnpm, no workspace links. Then it asserts that:
- no published manifest ships a
scriptsblock; gaia --helpworks and lists all four command plugins (conductor,ui,dropsh,deployment) as lazy stubs, importing none of them;gaia deployment tickets --helpreally mounts from@gaia-ai/addon-deployment;- an engine
conductor.config.jsresolves itsremote,executor,agentandworkspaceslots throughloadConductorConfig, with the Drupal remote resolving from@gaia-ai/addon-remote-drupalin theaddons: []form; - a connection
gaia.config.jsresolves itssiteandplugins[]throughloadGaiaConfig.
Publish manifests are generated, never hand-maintained: gaia-cli/scripts/prepare-publish.mjs writes a fresh manifest per package into <package>/publish/, rewriting every workspace:^ dependency to the release version. The monorepo's own workspace: wiring is untouched. Its target list is the publish order, and the release script iterates the same list.
Troubleshooting the build
ERR_MODULE_NOT_FOUND: Cannot find package '@gaia-ai/gaia'(or@gaia-ai/addon-*, or@dropsh/*) when a command loads its config → the resolution farm or an addon build is missing or stale. Re-rungaia-cli/scripts/setup.sh.gaia: command not found→~/.local/binis not on yourPATH. Add it:export PATH="$HOME/.local/bin:$PATH".gaia: test workspace … names checkout …, which is not built.→ exactly what it says, and deliberately fatal rather than a silent fallback to the global CLI: the workspace is the one place whose purpose is to run that build. Run the rebuild the message prints.gaia: no global gaia on PATH, and no .gaia-testbuild workspace above …→ you are outside every test workspace and have no globalgaia. Install the published CLI, or call a checkout's./gaia-cli/bin/gaiaby explicit path.- The shim runs the global CLI inside a gaia checkout → that is the rule since GAIA-390, not a fault. A checkout's own build is selected only inside a
.gaia-testbuildworkspace; use./gaia-cli/bin/gaiato exercise it from the checkout. If a baregaiain a test workspace is the global CLI, your installed shim predates GAIA-390 — adopt the current one (see Adopting a new shim above).
For anything that is not a build problem — connecting, authenticating, migrating configs — see Install the CLI and Set up a project.