Install Honeycomb

Shared memory for your AI coding agents — one command, all local.

This page exists so you can read the installer before you run it. Nothing here pipes anything automatically — copy a line when you're ready.

1Install

macOS & Linux:

# macOS / Linux curl -fsSL https://get.theapiary.sh | sh

Windows (PowerShell):

# Windows PowerShell irm https://get.theapiary.sh/install.ps1 | iex

2Verify before you run

Prefer to check the bytes first? Download the script and its checksum, verify, then run:

# download + verify + run (macOS / Linux) curl -fsSL -O https://get.theapiary.sh/install.sh curl -fsSL -O https://get.theapiary.sh/SHA256SUMS sha256sum -c SHA256SUMS # expect: install.sh: OK sh install.sh

On macOS without sha256sum, use shasum -a 256 -c SHA256SUMS.

3Published checksums

These are the SHA-256 of the exact files this domain serves. They change only when a new release is deployed.

2d4fa51ac98b4a2e86dbc2497d056b322fd03bc07caae26f66310c8cdecae2f6 install.sh 156bcef02bcd462b8440462e2ae6d34810bb32b628890fc4ff6d4b86af709299 install.ps1

Raw files: /install.sh · /install.ps1 · /SHA256SUMS · source on GitHub

4What this actually does

Everything runs locally on your machine — the daemon and dashboard bind to loopback. The installer is idempotent: re-running it is safe and skips anything already in place.

5Read the source

The full scripts, inline. This is byte-for-byte what the checksums above cover.

install.sh (POSIX sh)
#!/bin/sh
# Honeycomb one-command bootstrap installer (POSIX) — PRD-050a.
#
# Usage (the single line a brand-new user pastes):
#   curl -fsSL https://get.theapiary.sh | sh
#
# Contract (PRD-050a a-AC-1..6): leave the user on a running dashboard, OR tell them in ONE plain
# sentence why not. It assumes the operator knows nothing — no Node, no npm, no idea what a daemon
# is. It is deliberately THIN and IDEMPOTENT: it detects what is already present, installs only what
# is missing, and re-running it is safe.
#
# This script owns ONLY the host-bootstrap half: detect/install Node+npm (via fnm + a pinned LTS),
# then `npm i -g @legioncodeinc/honeycomb`. The moment a `honeycomb` bin exists it HANDS OFF to the
# `honeycomb install` CLI verb for the daemon-ensure + health-gate + dashboard-open — so that logic
# lives ONCE in TypeScript (src/commands/install.ts), not duplicated across two shell dialects.
#
# POSIX sh ONLY (no bashisms): this runs under `sh`, which may be dash/ash, not bash.

# `set -e` would abort on the FIRST non-zero command, surfacing a raw error. We instead handle every
# failure explicitly and print a plain-language line (parent AC-7) — so `set -e` is intentionally OFF.
set -u

# ─────────────────────────────────────────────────────────────────────────────
# THE ONE PLACE TO BUMP NODE. The single pinned Node LTS the installer provisions
# via fnm. To upgrade the provisioned Node for every new user, change THIS line
# only. (Existing users with a working Node are left untouched — see step 1.)
# ─────────────────────────────────────────────────────────────────────────────
HONEYCOMB_NODE_VERSION="22"

# The published npm package the global install pulls (PRD-048 publishes it; this consumes it).
HONEYCOMB_NPM_PACKAGE="@legioncodeinc/honeycomb@latest"

# Distribution base URL: the vanity domain that serves this installer surface (PRD-050a follow-up,
# now RESOLVED). get.theapiary.sh is a Cloudflare Pages site (site/install/) that content-negotiates:
# a shell client piping `/` gets this script as text/plain; a browser gets an "inspect before piping"
# page with the PUBLISHED SHA-256 checksums. `${BASE}/install.sh` and `${BASE}/install.ps1` always
# resolve to the raw, checksummed scripts. To verify before running: see https://get.theapiary.sh
HONEYCOMB_INSTALL_BASE_URL="https://get.theapiary.sh"

# ── Friendly progress log: step lines to stdout, the single failure summary to stderr. ──
step()  { printf '→ %s\n' "$1"; }
ok()    { printf '✓ %s\n' "$1"; }
fail()  { printf 'Honeycomb install could not continue: %s\n' "$1" >&2; }

# `command -v` is the POSIX way to test for a binary (NOT `which`, which is not guaranteed present).
have()  { command -v "$1" >/dev/null 2>&1; }

# ─────────────────────────────────────────────────────────────────────────────
# Step 1 — Node + npm. If both are present, use them. Else install fnm (NO elevation)
#          + the pinned Node LTS. fnm installs entirely under $HOME, so it never needs
#          sudo; that is exactly why it is the primary path over the official installer.
# ─────────────────────────────────────────────────────────────────────────────
ensure_node() {
  if have node && have npm; then
    ok "Node $(node --version) and npm $(npm --version) found."
    return 0
  fi

  step "Node/npm not found — installing a private copy via fnm (no admin rights needed)…"

  # fnm install is a curl|sh that writes ONLY under ~/.local/share/fnm + ~/.fnm — no elevation.
  if ! have fnm; then
    if ! have curl; then
      # We cannot fetch fnm without curl, and installing curl itself needs the OS package manager
      # (which needs elevation). Print the EXACT copy-paste and exit cleanly (a-AC-3).
      elevation_required_node
      return 1
    fi
    if ! curl -fsSL https://fnm.vercel.app/install | sh >/dev/null 2>&1; then
      # fnm's own installer failed (e.g. a locked-down $HOME it cannot write). Fall back to the
      # documented manual command + clean non-zero exit (a-AC-3) — never a raw error dump.
      elevation_required_node
      return 1
    fi
  fi

  # Load fnm into THIS shell so `fnm`/`node`/`npm` resolve in-process (the install does not refresh
  # the current shell's env). fnm lives at ~/.local/share/fnm or ~/.fnm depending on the platform.
  FNM_DIR="${HOME}/.local/share/fnm"
  [ -d "$FNM_DIR" ] || FNM_DIR="${HOME}/.fnm"
  if [ -d "$FNM_DIR" ]; then
    PATH="${FNM_DIR}:${PATH}"
    export PATH
  fi
  if have fnm; then
    # `fnm env` exports the shims; evaluate them so node/npm are on PATH for the rest of this run.
    eval "$(fnm env 2>/dev/null)" || true
    if ! fnm install "$HONEYCOMB_NODE_VERSION" >/dev/null 2>&1; then
      elevation_required_node
      return 1
    fi
    fnm use "$HONEYCOMB_NODE_VERSION" >/dev/null 2>&1 || true
    eval "$(fnm env --use-on-cd 2>/dev/null)" || true
  fi

  if have node && have npm; then
    ok "Installed Node $(node --version) via fnm."
    return 0
  fi

  # fnm landed but node/npm still are not resolvable — surface the manual path, clean exit (a-AC-3).
  elevation_required_node
  return 1
}

# a-AC-3 — print the EXACT copy-paste install command + a one-line WHY, then signal a clean
# non-zero exit. NEVER a raw error dump. The caller exits with this function's surfaced intent.
elevation_required_node() {
  fail "Honeycomb needs Node ${HONEYCOMB_NODE_VERSION} and could not install it automatically (your machine blocked the no-admin install)."
  printf '\nInstall Node %s yourself with ONE of these, then re-run this installer:\n\n' "$HONEYCOMB_NODE_VERSION"
  printf '  # macOS (Homebrew):\n'
  printf '  brew install node@%s\n\n' "$HONEYCOMB_NODE_VERSION"
  printf '  # Debian/Ubuntu:\n'
  printf '  curl -fsSL https://deb.nodesource.com/setup_%s.x | sudo -E bash - && sudo apt-get install -y nodejs\n\n' "$HONEYCOMB_NODE_VERSION"
  printf '  # Then re-run:\n'
  printf '  curl -fsSL %s/install.sh | sh\n\n' "$HONEYCOMB_INSTALL_BASE_URL"
}

# ─────────────────────────────────────────────────────────────────────────────
# Step 2 — install @legioncodeinc/honeycomb globally. The embedding runtime
#          (@huggingface/transformers) is an OPTIONAL dependency of the package and
#          is pulled by npm during this install; its MODEL WEIGHTS are NOT fetched
#          here (that is the embed daemon's lazy warmup — 050b), so this stays fast.
# ─────────────────────────────────────────────────────────────────────────────
install_honeycomb() {
  # Idempotent: a re-run on a machine that already has `honeycomb` is a NO-OP — no npm mutation, no
  # network. This keeps the documented "safe to re-run" contract and lets a rerun succeed OFFLINE. Only
  # an absent install triggers the global npm install. (`resolve_honeycomb_bin` is defined below; POSIX sh
  # resolves functions at call time, so the forward reference is fine — both exist before `main` runs.)
  if existing_bin="$(resolve_honeycomb_bin 2>/dev/null)"; then
    ok "${HONEYCOMB_NPM_PACKAGE} already installed (${existing_bin})."
    return 0
  fi
  step "installing ${HONEYCOMB_NPM_PACKAGE} globally…"
  if ! npm install -g "$HONEYCOMB_NPM_PACKAGE" >/dev/null 2>&1; then
    fail "the global install of ${HONEYCOMB_NPM_PACKAGE} failed."
    printf '\nTry it directly to see the npm error, then re-run this installer:\n\n  npm install -g %s\n\n' "$HONEYCOMB_NPM_PACKAGE"
    return 1
  fi
  ok "installed ${HONEYCOMB_NPM_PACKAGE}."
  return 0
}

# Resolve the ABSOLUTE path to the freshly-installed `honeycomb` bin. `npm i -g` does NOT refresh the
# CURRENT shell's PATH, so calling `honeycomb` by bare name in the same run can fail "command not
# found" (PRD-050a impl-note). Resolve `<npm prefix -g>/bin/honeycomb` and invoke THAT.
resolve_honeycomb_bin() {
  if have honeycomb; then
    command -v honeycomb
    return 0
  fi
  prefix="$(npm prefix -g 2>/dev/null)"
  if [ -n "$prefix" ] && [ -x "${prefix}/bin/honeycomb" ]; then
    printf '%s\n' "${prefix}/bin/honeycomb"
    return 0
  fi
  return 1
}

# ─────────────────────────────────────────────────────────────────────────────
# Step 3 — hand off to the CLI verb for the daemon-ensure + health-gate + dashboard
#          open. The open logic lives ONCE in the CLI (src/commands/install.ts), not
#          here. The verb is idempotent + health-gated (a-AC-2 / a-AC-4) and opens
#          honeycomb.local → loopback (a-AC-6), writing onboarding "installed" (a-AC-5).
# ─────────────────────────────────────────────────────────────────────────────
main() {
  ensure_node      || exit 1
  install_honeycomb || exit 1

  bin="$(resolve_honeycomb_bin)"
  if [ -z "$bin" ]; then
    fail "could not locate the installed 'honeycomb' command after the global install."
    printf '\nOpen a NEW terminal (so PATH refreshes) and run:\n\n  honeycomb install\n\n'
    exit 1
  fi

  # The verb prints its own friendly step log (daemon up / onboarding marked / opening dashboard) and
  # returns a clean exit code; we forward it verbatim. A handled failure inside the verb is already a
  # plain-language line + non-zero exit — no raw stack reaches the user here. Forward the caller's args
  # ("$@") so a bootstrap `--ref <code>` (and any future install flag) reaches the CLI's install verb.
  "$bin" install "$@"
  exit $?
}

main "$@"
install.ps1 (Windows PowerShell)
# Honeycomb one-command bootstrap installer (Windows PowerShell) -- PRD-050a.
#
# Usage (the single line a brand-new Windows user pastes):
#   irm https://get.theapiary.sh/install.ps1 | iex
#
# This is the FUNCTIONAL EQUIVALENT of install.sh (PRD-050a a-AC-5): same contract -- leave the user
# on a running dashboard, or tell them in ONE plain sentence why not. It owns ONLY the host-bootstrap
# half (detect/install Node+npm via fnm + a pinned LTS, then `npm i -g @legioncodeinc/honeycomb`),
# then HANDS OFF to the `honeycomb install` CLI verb for the daemon-ensure + health-gate +
# dashboard-open -- so that logic lives ONCE in TypeScript (src/commands/install.ts).
#
# Thin + idempotent: detect what is present, install only what is missing, re-run safely.
#
# ASCII-only by design: this file is sourced via `irm | iex` and parsed by Windows PowerShell 5.1,
# which reads a non-BOM file as the system ANSI codepage -- so non-ASCII glyphs would corrupt the
# parse. The friendly progress GLYPHS the user sees come from the CLI verb's UTF-8 output; this
# script's own prefixes stay ASCII.

# Handle every failure explicitly + print a plain-language line (parent AC-7). We do NOT set
# $ErrorActionPreference='Stop' globally -- that would surface a raw PowerShell exception/trace.
$ErrorActionPreference = 'Continue'

# -----------------------------------------------------------------------------
# THE ONE PLACE TO BUMP NODE. The single pinned Node LTS the installer provisions
# via fnm. To upgrade the provisioned Node for every new user, change THIS line
# only. (Existing users with a working Node are left untouched -- see Ensure-Node.)
# -----------------------------------------------------------------------------
$HoneycombNodeVersion = '22'

# The published npm package the global install pulls (PRD-048 publishes it; this consumes it).
$HoneycombNpmPackage = '@legioncodeinc/honeycomb@latest'

# Distribution base URL: the vanity domain that serves this installer surface (PRD-050a follow-up,
# now RESOLVED). get.theapiary.sh is a Cloudflare Pages site (site/install/) that content-negotiates:
# a shell client piping `/` gets the POSIX install.sh as text/plain; a browser gets an "inspect before
# piping" page with the PUBLISHED SHA-256 checksums. `$HoneycombInstallBaseUrl/install.ps1` always
# resolves to the raw, checksummed script. To verify before running: see https://get.theapiary.sh
$HoneycombInstallBaseUrl = 'https://get.theapiary.sh'

# Friendly progress log: step lines to the host, the single failure summary to the error stream.
function Write-Step([string]$m) { Write-Host "-> $m" }
function Write-Ok([string]$m)   { Write-Host "[ok] $m" }
function Write-Fail([string]$m) { [Console]::Error.WriteLine("Honeycomb install could not continue: $m") }

function Test-Have([string]$name) { return [bool](Get-Command $name -ErrorAction SilentlyContinue) }

# a-AC-3 -- print the EXACT copy-paste install command + a one-line WHY. NEVER a raw error dump.
function Show-NodeElevationHelp {
  Write-Fail "Honeycomb needs Node $HoneycombNodeVersion and could not install it automatically (your machine blocked the no-admin install)."
  Write-Host ''
  Write-Host "Install Node $HoneycombNodeVersion yourself with ONE of these, then re-run this installer:"
  Write-Host ''
  Write-Host '  # winget (recommended on Windows 10/11):'
  Write-Host '  winget install OpenJS.NodeJS.LTS'
  Write-Host ''
  Write-Host '  # or via the official MSI:'
  Write-Host '  https://nodejs.org/en/download'
  Write-Host ''
  Write-Host '  # Then re-run:'
  Write-Host "  irm $HoneycombInstallBaseUrl/install.ps1 | iex"
  Write-Host ''
}

# -----------------------------------------------------------------------------
# Step 1 -- Node + npm. If both present, use them. Else install fnm (NO elevation)
#           + the pinned Node LTS. fnm installs under the user profile, so it never
#           needs admin; that is why it is the primary path over the official MSI.
# -----------------------------------------------------------------------------
function Ensure-Node {
  if ((Test-Have 'node') -and (Test-Have 'npm')) {
    Write-Ok "Node $(node --version) and npm $(npm --version) found."
    return $true
  }

  Write-Step 'Node/npm not found -- installing a private copy via fnm (no admin rights needed)...'

  if (-not (Test-Have 'fnm')) {
    # Prefer winget (per-user, no elevation) to install fnm; fall back to the documented manual path.
    if (Test-Have 'winget') {
      winget install Schniz.fnm --accept-source-agreements --accept-package-agreements 2>$null | Out-Null
      # winget does NOT refresh THIS session's PATH, so a bare `fnm` lookup right after the install can
      # still miss even though the binary is on disk. Rebuild $env:Path from the machine + user
      # registry so the just-installed shim resolves in-process before we judge the install failed.
      try {
        $machinePath = [System.Environment]::GetEnvironmentVariable('Path', 'Machine')
        $userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
        $env:Path = (@($machinePath, $userPath) | Where-Object { $_ }) -join ';'
      } catch {
        # Fail-soft: the `Test-Have 'fnm'` re-check below is the real gate; just surface why, don't abort.
        Write-Warning "Couldn't refresh PATH from the registry ($($_.Exception.Message)); continuing."
      }
    }
    if (-not (Test-Have 'fnm')) {
      # Could not install fnm without elevation -- surface the exact manual command + clean exit (a-AC-3).
      Show-NodeElevationHelp
      return $false
    }
  }

  # Load fnm into THIS session so node/npm resolve in-process (the install does not refresh the
  # current shell's PATH). `fnm env` emits the PowerShell shims; invoke them here.
  # Fail-soft on `fnm env`: the final `Test-Have 'node'/'npm'` gate below is the real decider; a failure
  # here must not abort the bootstrap, but the reason should be visible (not silently swallowed).
  try { fnm env --use-on-cd | Out-String | Invoke-Expression } catch {
    Write-Warning "fnm env (pre-install) didn't load into this session ($($_.Exception.Message)); continuing."
  }
  fnm install $HoneycombNodeVersion 2>$null | Out-Null
  fnm use $HoneycombNodeVersion 2>$null | Out-Null
  try { fnm env --use-on-cd | Out-String | Invoke-Expression } catch {
    Write-Warning "fnm env (post-install) didn't load into this session ($($_.Exception.Message)); continuing."
  }

  if ((Test-Have 'node') -and (Test-Have 'npm')) {
    Write-Ok "Installed Node $(node --version) via fnm."
    return $true
  }

  Show-NodeElevationHelp
  return $false
}

# -----------------------------------------------------------------------------
# Step 2 -- install @legioncodeinc/honeycomb globally. The embedding runtime is an
#           OPTIONAL dep pulled by npm here; its MODEL WEIGHTS are NOT fetched now
#           (lazy warmup -- 050b), so this stays fast.
# -----------------------------------------------------------------------------
function Install-Honeycomb {
  Write-Step "installing $HoneycombNpmPackage globally..."
  npm install -g $HoneycombNpmPackage 2>$null | Out-Null
  if ($LASTEXITCODE -ne 0) {
    Write-Fail "the global install of $HoneycombNpmPackage failed."
    Write-Host ''
    Write-Host 'Try it directly to see the npm error, then re-run this installer:'
    Write-Host ''
    Write-Host "  npm install -g $HoneycombNpmPackage"
    Write-Host ''
    return $false
  }
  Write-Ok "installed $HoneycombNpmPackage."
  return $true
}

# Resolve the ABSOLUTE path to the freshly-installed honeycomb bin. `npm i -g` does NOT refresh the
# CURRENT session's PATH, so calling `honeycomb` by bare name in the same run can fail (PRD-050a
# impl-note). Resolve `%AppData%\npm\honeycomb.cmd` (the npm global bin shim on Windows).
function Resolve-HoneycombBin {
  $cmd = Get-Command 'honeycomb' -ErrorAction SilentlyContinue
  if ($cmd) { return $cmd.Source }
  $prefix = (npm prefix -g 2>$null)
  if ($prefix) {
    $candidate = Join-Path $prefix 'honeycomb.cmd'
    if (Test-Path $candidate) { return $candidate }
  }
  $appdataCmd = Join-Path $env:AppData 'npm\honeycomb.cmd'
  if (Test-Path $appdataCmd) { return $appdataCmd }
  return $null
}

# -----------------------------------------------------------------------------
# Step 3 -- hand off to the CLI verb for the daemon-ensure + health-gate + dashboard
#           open. The verb is idempotent + health-gated (a-AC-2 / a-AC-4) and opens
#           honeycomb.local -> loopback (a-AC-6), writing onboarding "installed" (a-AC-5).
# -----------------------------------------------------------------------------
# Returns a status CODE (never calls `exit`): in the documented `irm ... | iex` bootstrap, `exit`
# terminates the CALLER's PowerShell host and can close the user's terminal. The single process-exit
# handling lives at the entrypoint below, which sets `$global:LASTEXITCODE` from this return value.
function Invoke-Main {
  if (-not (Ensure-Node))       { return 1 }
  if (-not (Install-Honeycomb)) { return 1 }

  $bin = Resolve-HoneycombBin
  if (-not $bin) {
    Write-Fail "could not locate the installed 'honeycomb' command after the global install."
    Write-Host ''
    Write-Host 'Open a NEW terminal (so PATH refreshes) and run:'
    Write-Host ''
    Write-Host '  honeycomb install'
    Write-Host ''
    return 1
  }

  # The verb prints its own friendly step log and returns a clean exit code; forward it verbatim. A
  # handled failure inside the verb is already a plain-language line + non-zero exit -- no raw trace.
  & $bin install
  return $LASTEXITCODE
}

# Entrypoint: run main, then set the exit code ONCE without tearing down the host (so `irm | iex`
# hands control back to the user's session instead of closing it).
$global:LASTEXITCODE = Invoke-Main