Bundling
May 3, 2026 · View on GitHub
The CLI (tui, run, serve, models, …) ships as a single-file
executable per target, produced via Node SEA. The embedded entry is
dist-sea/cli.mjs (esbuild bundle of src/cli/index.ts and dependencies,
see npm run bundle:sea) with mainFormat: "module" in
sea-config.json. The separate Tauri sidecar entry is still
atomic-agent-sidecar when installed from npm; it is not the SEA
release described here. llama-server is not bundled — connect over
HTTP (ATOMIC_AGENT_LLAMA_URL) or use atomic-agent models for managed
local runtimes. Neither Chrome/Edge nor Playwright browser binaries are
bundled; playwright-core attaches to the already-installed system
browser.
Target matrix
| Slug | Platform | Arch | Runner (GH) | Archive |
|---|---|---|---|---|
darwin-arm64 | darwin | arm64 | macos-14 | tar.gz |
darwin-x64 | darwin | x64 | macos-13 | tar.gz |
linux-x64 | linux | x64 | ubuntu-22.04 | tar.gz |
linux-arm64 | linux | arm64 | ubuntu-24.04-arm | tar.gz |
win32-x64 | win32 | x64 | windows-2022 | zip |
Run npm run bundle:matrix -- --json to get the JSON input for a GitHub
Actions matrix strategy.
Build-time Node requirement
SEA embeds the build-time Node binary, so its feature set is decided at
build time, not on the end user's machine. "mainFormat": "module" (the
flag that makes SEA treat dist-sea/cli.mjs as ESM) landed in
Node 25.7.0 (#61813).
Earlier Nodes (including 24.x LTS until the backport lands) run the ESM
bundle as CommonJS and crash with SyntaxError: Cannot use import statement outside a module.
npm ci,npm run build,npm test— work on Node ≥ 22.x.npm run bundle:build-binary— requires Node ≥ 25.7. The script verifiesprocess.versions.nodeand exits fast with a clear message.- CI pins
node-version: "25.x"inrelease.yml.
Local setup: nvm install 25 && nvm use 25 before npm run bundle:build-binary.
Per-target build (runs on the target host)
- Install deps for the target platform:
npm ci --omit=dev - Build the TypeScript output:
npm run build - Bundle the CLI for SEA (single ESM file with
createRequirebanner for mixed CJS dependencies;better-sqlite3stays external):npm run bundle:sea - Fetch runtime assets (downloads the pinned
ripgrepbinary for the current host; pass--allto prefetch every target):npm run bundle:fetch-assets # or, to prefetch the full matrix: npx tsx scripts/fetch-assets.ts --all - Produce the SEA binary:
npm run bundle:build-binary - Package the bundle:
npm run bundle:package
The output lands at bundle/atomic-agent-<slug>.<ext> and
bundle/atomic-agent-<slug>.<ext>.sha256 (for shasum -a 256 -c).
Troubleshooting: killed in zsh (macOS)
- #1 cause on Apple Silicon:
postjectrewrites the Mach-O, which invalidates the ad-hoc code signature Node ships with. The kernel then kills the process at launch with SIGKILL and an emptykilledline.scripts/build-binary.tsrunscodesign --sign - --forceafter injection to restore launchability; CI later replaces that ad-hoc signature with the Developer ID one. If you hitkilled, first verify the binary is signed:codesign -dv ./bundle/darwin-arm64/atomic-agent # Format=Mach-O thin (arm64) Signature=adhoc ← expected xattr -lshowingcom.apple.provenanceonly is not quarantine. Blocking downloads usecom.apple.quarantine.provenancealone does not explain a silentkilled.- Compare: if
node dist-sea/cli.mjs --helpworks but./bundle/.../atomic-agentis killed, the problem is the SEA binary path (signing, Node version, SEA config), not the JS sources. If both fail, debug the bundle first. - For jetsam / real OOM, check Console (or
log show --predicate 'eventMessage contains "Jetsam"') around the run time.
Release (CI)
The workflow .github/workflows/release.yml
builds the matrix on tag v* or on workflow_dispatch (optionally
publishing a draft GitHub Release with all artifacts). macOS jobs
run scripts/sign-mac-binary.sh and
scripts/notarize-mac-binary.sh (same
notary key pattern as openclaw / electron-desktop). Linux and Windows
archives are unsigned in this milestone; Windows signtool signing is
deferred. Tauri hosts that embed a sidecar should use a signed binary
on notarised macOS app builds.
GitHub secrets (match openclaw): MACOS_CSC_LINK (base64 .p12),
MACOS_CSC_KEY_PASSWORD, NOTARYTOOL_KEY (App Store Connect API key
.p8 contents), NOTARYTOOL_KEY_ID, NOTARYTOOL_ISSUER. Raw
Mach-O binaries are not stapled; Gatekeeper uses an online ticket on
first launch.
Install (macOS / Linux, curl)
From a published release (or latest):
curl -fsSL "https://raw.githubusercontent.com/OWNER/atomic-agent/BRANCH/scripts/install.sh" | sh
Set ATOMIC_AGENT_REPO=owner/atomic-agent if the default in
scripts/install.sh does not match your fork.
Optional: ATOMIC_AGENT_VERSION, ATOMIC_AGENT_INSTALL_DIR.
Signing / notarisation (local / manual)
On macOS you can run the same shell scripts the workflow uses, or produce
an unsigned binary with npm run bundle:build-binary and
npm run bundle:package only. Windows code signing is not automated here.
What the bundle contains
atomic-agent[.exe] # SEA binary (CLI entry)
grammars/tool-call.gbnf # GBNF for structured tool-call decoding
vendor/rg[.exe] # pinned ripgrep for os.fs.grep (sibling of binary)
prebuilds/… # better-sqlite3 native prebuilds for the target
README.txt # short runtime note
Bundled ripgrep
os.fs.grep relies on a pinned ripgrep build so the agent works zero-setup
once the archive is extracted.
- Version: pinned in
scripts/fetch-assets.tsviaRIPGREP_VERSION. Bump that constant (and re-runnpm run bundle:fetch-assets --all) to refresh. - Location: copied by
scripts/package-bundle.tsto<bundle>/vendor/rg[.exe]next to the SEA binary. The runtime resolver (src/runtime/ripgrep-resolver.ts) discovers it via<dirname(process.execPath)>/vendor/rg[.exe]. - Override: set
ATOMIC_AGENT_RG_PATH=/path/to/rgto point the agent at a different binary without repackaging. - Size impact: roughly +5 MB per target. The binary is stripped and
stored alongside the SEA rather than embedded inside it, because Node
SEA asset extraction +
chmod +xout of a temp dir is fragile across platforms. - Not committed: downloaded binaries land under
assets/ripgrep/, which is git-ignored.
Bundled document extractors
os.fs.read_document bundles several pure-JS libraries for PDF/DOCX/XLSX/
RTF/ODT/PPTX/DOC extraction. These are regular dependencies resolved by
Node SEA's builtin module resolution, not sidecar binaries:
| Library | Purpose | Approx. size |
|---|---|---|
pdfjs-dist (legacy build) | PDF text layer | ~1.8 MB |
mammoth | DOCX → markdown | ~250 KB |
exceljs | XLSX parsing | ~850 KB |
jszip | ODT/PPTX unzip | ~100 KB |
fast-xml-parser | ODT/PPTX XML parsing | ~200 KB |
word-extractor | Legacy .doc (OLE2) | ~100 KB |
Net cost to the SEA: roughly +3 MB. RTF is handled by a custom pure-JS
parser in-tree (no dep). Test fixtures live under
src/tools/os/test-fixtures/ and are regenerated via
npm run fixtures:generate (uses devDeps pdfkit, docx).
Bundled archive tools
os.fs.archive.* shares jszip with read_document and adds one
dependency:
| Library | Purpose | Approx. size |
|---|---|---|
tar-stream | Streaming tar / tar.gz read + write | ~50 KB |
gz is handled by the built-in zlib. Net incremental cost of the
archive tools: ~50 KB (plus the already-bundled jszip).
Bundled diff / watch tools
os.fs.diff / os.fs.patch / os.fs.watch add two pure-JS runtime
dependencies:
| Library | Purpose | Approx. size |
|---|---|---|
diff (jsdiff 9.x) | Unified-diff generation + patch application, ships its own types | ~100 KB |
chokidar 5.x | Cross-platform recursive fs watcher | ~120 KB |
Net incremental cost: ~220 KB. os.fs.hash uses the built-in
crypto module (zero cost). Git tools (os.git.*) shell out to the
system git, so they add no bundle weight — but the host must have
git on PATH. Process tools (os.proc.*) rely on ps/tasklist,
which are standard OS utilities and need no bundling.
Runtime requirements (documented in README.txt)
- External llama-server (or managed mode). Set
ATOMIC_AGENT_LLAMA_URL=http://host:portas needed. - Google Chrome or Microsoft Edge installed on the host. We use the
system browser via
playwright-core(channel: chrome|msedge). - macOS: Accessibility + Screen Recording permissions must be granted
to the
atomic-agentbinary for window-management and reliable keyboard automation. Users grant this the first time the tool is used. - Linux:
wmctrlneeded foros.window.*;xdg-open/pbpasteequivalents are consumed byclipboardywhere applicable. - Skills live under
$ATOMIC_AGENT_STATE_DIR/skills/and./.atomic-agent/skills/. The redistributable ships astarter-skills/tree next to the binary; each boot the runtime replaces matching names under the global skills dir so starter packs stay current (project-local skills are unchanged).
Non-goals
- No llama-server download. The agent connects to a server the user already runs.
- No Chromium download.
playwright-coreis used withoutnpx playwright install; the user supplies the browser. - No cross-compilation. Node SEA is strictly per-host; CI fan-out handles the matrix.
- No arbitrary user skill corpus in the bundle. Only the small
built-in
starter-skills/templates ship; operators still own long-lived skill edits under stateDir.