The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64 and arm64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
355 files changed
+47028
No files matched your search
Executable
+340
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install LLeMbas CLI (the `lembas` command) for the current user.
|
||||
#
|
||||
# ./install.sh [--binary PATH] [--aliases | --no-aliases] [--no-config] [--yes]
|
||||
# ./install.sh --uninstall [--purge] [--yes]
|
||||
#
|
||||
# - From a checkout, builds dist/lembas first, every time (a binary left from an older
|
||||
# checkout is never installed). Bun builds it: when Bun is missing, offers to fetch it into
|
||||
# ~/.bun (the release LLeMbas CLI is tested with, checked against its published SHA-256).
|
||||
# - Copies the binary to ~/.local/bin/lembas.
|
||||
# - Writes starter ~/.config/lembas/config.yaml and connections.yaml (0600), commented, where
|
||||
# there are none (--no-config: none at all, for a deploy that writes its own). Existing files
|
||||
# are never touched. The JSON Schemas editors check them with (~/.config/lembas/schema/)
|
||||
# are written every time, so they match the binary.
|
||||
# - For bash (~/.bashrc) and zsh (${ZDOTDIR:-~}/.zshrc) — whichever exist, or the one for $SHELL
|
||||
# if neither does — writes one marked block that puts ~/.local/bin on PATH and, if wanted,
|
||||
# the alias `agent`. Re-running replaces the block; nothing is appended twice.
|
||||
# - --uninstall says what it will remove and asks: the service (stopped first), the binary (and
|
||||
# the one an update kept), the editor schemas and the rc block.
|
||||
# Settings, sessions and memory stay, for a later reinstall; --purge removes them too
|
||||
# (~/.config, ~/.local/share and ~/.local/state /lembas). A project's own .agent/ is never
|
||||
# touched. `lembas uninstall` does the same.
|
||||
set -euo pipefail
|
||||
|
||||
BIN_DIR="$HOME/.local/bin"
|
||||
TARGET="$BIN_DIR/lembas"
|
||||
BEGIN="# >>> lembas >>>"
|
||||
END="# <<< lembas <<<"
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# A relative XDG_* is ignored, as the XDG spec says (it would mean wherever this was started).
|
||||
xdg() { case "${1:-}" in /*) printf '%s' "$1" ;; *) printf '%s' "$2" ;; esac; }
|
||||
CONFIG="$(xdg "${XDG_CONFIG_HOME:-}" "$HOME/.config")/lembas"
|
||||
# The Bun release LLeMbas CLI is built and tested with.
|
||||
BUN_VERSION=1.4.2
|
||||
BUN_SHA256_X64=36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913
|
||||
BUN_SHA256_X64_BASELINE=c678040f14fe0440eb839d37cbd0ce4c051a32da72806ac97de6a6aab6bf728f
|
||||
BUN_SHA256_AARCH64=54328bbc2d9c8e0c9f892c544d66c57a83b84139e34909e5ee81758f1ac8fda7
|
||||
|
||||
binary="" aliases="ask" yes=0 uninstall=0 starter=1 purge=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--binary) binary="$2"; shift 2 ;;
|
||||
--aliases) aliases=yes; shift ;;
|
||||
--no-aliases) aliases=no; shift ;;
|
||||
--no-config) starter=0; shift ;;
|
||||
--yes|-y) yes=1; shift ;;
|
||||
--uninstall) uninstall=1; shift ;;
|
||||
--purge) purge=1; shift ;;
|
||||
-h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "install.sh: unknown option $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
|
||||
rc_files() {
|
||||
local files=() zrc="${ZDOTDIR:-$HOME}/.zshrc"
|
||||
[ -f "$HOME/.bashrc" ] && files+=("$HOME/.bashrc")
|
||||
[ -f "$zrc" ] && files+=("$zrc")
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
case "${SHELL##*/}" in
|
||||
zsh) files+=("$zrc") ;;
|
||||
*) files+=("$HOME/.bashrc") ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s\n' "${files[@]}"
|
||||
}
|
||||
|
||||
# Remove our block from a file, leaving everything else byte-for-byte.
|
||||
strip_block() {
|
||||
local f="$1"
|
||||
[ -f "$f" ] || return 0
|
||||
grep -qF "$BEGIN" "$f" || return 0
|
||||
# Only a whole block: with its end marker gone, everything after the start would go with it.
|
||||
if ! awk -v b="$BEGIN" -v e="$END" '$0==b{open=1;next} $0==e&&open{open=0;ok=1} END{exit !(ok && !open)}' "$f"; then
|
||||
echo "install.sh: $f has \"$BEGIN\" without its \"$END\" — left as it is; remove the block by hand" >&2
|
||||
return 1
|
||||
fi
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk -v b="$BEGIN" -v e="$END" '$0==b{skip=1;next} $0==e{skip=0;next} !skip' "$f" >"$tmp"
|
||||
cat "$tmp" >"$f"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
confirm() {
|
||||
[ "$yes" -eq 1 ] && return 0
|
||||
[ -t 0 ] || return 1
|
||||
local answer
|
||||
read -r -p " $1 [Y/n] " answer
|
||||
[[ ! "$answer" =~ ^[Nn] ]]
|
||||
}
|
||||
|
||||
if [ "$uninstall" -eq 1 ]; then
|
||||
DATA="$(xdg "${XDG_DATA_HOME:-}" "$HOME/.local/share")/lembas"
|
||||
STATE="$(xdg "${XDG_STATE_HOME:-}" "$HOME/.local/state")/lembas"
|
||||
# `lembas service` as a systemd user unit: left running with its binary gone,
|
||||
# it restart-loops on 203/EXEC every 10 s, so it is stopped, disabled and removed first.
|
||||
UNITS="$(xdg "${XDG_CONFIG_HOME:-}" "$HOME/.config")/systemd/user"
|
||||
gone=() kept=() units=()
|
||||
u=lembas.service
|
||||
if [ -e "$UNITS/$u" ]; then units+=("$u"); gone+=("$UNITS/$u (stopped and disabled first)"); fi
|
||||
for f in "$TARGET" "$TARGET.prev" "$CONFIG/schema"; do [ -e "$f" ] || [ -L "$f" ] && gone+=("$f"); done
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] && grep -qF "$BEGIN" "$f" && gone+=("the LLeMbas CLI block in $f")
|
||||
done < <(rc_files)
|
||||
for d in "$CONFIG" "$DATA" "$STATE"; do
|
||||
[ -e "$d" ] || continue
|
||||
if [ "$purge" -eq 1 ]; then gone+=("$d"); else kept+=("$d"); fi
|
||||
done
|
||||
if [ ${#gone[@]} -eq 0 ]; then
|
||||
say "Nothing of LLeMbas CLI's to remove here."
|
||||
exit 0
|
||||
fi
|
||||
say "This removes:"
|
||||
printf ' %s\n' "${gone[@]}"
|
||||
if [ ${#kept[@]} -gt 0 ]; then
|
||||
say "and keeps your settings, sessions and memory (--purge removes them too):"
|
||||
printf ' %s\n' "${kept[@]}"
|
||||
fi
|
||||
[ "$purge" -eq 1 ] && say "(--purge: settings, connections, memory, skills, every session and snapshot.)"
|
||||
confirm "Uninstall?" || { say "Nothing removed."; exit 1; }
|
||||
if [ ${#units[@]} -gt 0 ]; then
|
||||
for u in "${units[@]}"; do
|
||||
systemctl --user disable --now "$u" >/dev/null 2>&1 || true
|
||||
rm -f "$UNITS/$u"
|
||||
done
|
||||
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
while IFS= read -r f; do strip_block "$f"; done < <(rc_files)
|
||||
rm -rf "$TARGET" "$TARGET.prev" "$CONFIG/schema"
|
||||
[ "$purge" -eq 1 ] && rm -rf "$CONFIG" "$DATA" "$STATE"
|
||||
say "LLeMbas CLI is uninstalled. A project's own .agent/ directory is left as it is."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Bun, user-local, without its install script (which edits rc files): the release zip, checked
|
||||
# against the release's SHASUMS256.txt.
|
||||
fetch_bun() {
|
||||
local arch asset tmp base="https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION"
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch=x64; grep -qw avx2 /proc/cpuinfo 2>/dev/null || arch=x64-baseline ;;
|
||||
aarch64 | arm64) arch=aarch64 ;;
|
||||
*) echo "install.sh: no Bun build for $(uname -m)" >&2; return 1 ;;
|
||||
esac
|
||||
asset="bun-linux-$arch"
|
||||
for tool in curl sha256sum unzip; do
|
||||
command -v "$tool" >/dev/null 2>&1 || { echo "install.sh: fetching Bun needs $tool (sudo apt install $tool)" >&2; return 1; }
|
||||
done
|
||||
tmp="$(mktemp -d)"
|
||||
say " fetching Bun $BUN_VERSION ($asset)…"
|
||||
if ! curl -fsSL -o "$tmp/$asset.zip" "$base/$asset.zip" || ! curl -fsSL -o "$tmp/SHASUMS256.txt" "$base/SHASUMS256.txt"; then
|
||||
rm -rf "$tmp"
|
||||
echo "install.sh: could not download Bun" >&2
|
||||
return 1
|
||||
fi
|
||||
# Checked against the SHA-256 written here for this version, not only against the list that came
|
||||
# with the download (the same place could have changed both).
|
||||
local want
|
||||
case "$asset" in
|
||||
bun-linux-x64) want="$BUN_SHA256_X64" ;;
|
||||
bun-linux-x64-baseline) want="$BUN_SHA256_X64_BASELINE" ;;
|
||||
bun-linux-aarch64) want="$BUN_SHA256_AARCH64" ;;
|
||||
esac
|
||||
{ (cd "$tmp" && grep " $asset.zip\$" SHASUMS256.txt | sha256sum -c --quiet -) && [ "$(sha256sum "$tmp/$asset.zip" | cut -d' ' -f1)" = "$want" ]; } || { rm -rf "$tmp"; echo "install.sh: Bun's checksum did not match — not installed" >&2; return 1; }
|
||||
unzip -q "$tmp/$asset.zip" -d "$tmp"
|
||||
mkdir -p "$HOME/.bun/bin"
|
||||
install -m 0755 "$tmp/$asset/bun" "$HOME/.bun/bin/bun"
|
||||
rm -rf "$tmp"
|
||||
say " installed $HOME/.bun/bin/bun ($("$HOME/.bun/bin/bun" --version))"
|
||||
}
|
||||
|
||||
# ── the binary ─────────────────────────────────────────────────────────────────────────────
|
||||
if [ -z "$binary" ]; then
|
||||
binary="$HERE/dist/lembas"
|
||||
if [ -f "$HERE/package.json" ]; then
|
||||
bun="$(command -v bun || true)"
|
||||
[ -n "$bun" ] || { [ -x "$HOME/.bun/bin/bun" ] && bun="$HOME/.bun/bin/bun"; } || true
|
||||
if [ -z "$bun" ]; then
|
||||
say "LLeMbas CLI is built with Bun, which is not installed here."
|
||||
if confirm "Fetch Bun $BUN_VERSION into ~/.bun (checked against its published SHA-256)?"; then
|
||||
fetch_bun || exit 1
|
||||
bun="$HOME/.bun/bin/bun"
|
||||
else
|
||||
echo "install.sh: no Bun, so nothing to build. Install Bun (https://bun.sh) and run this again, or pass --binary PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
say "Building $binary…"
|
||||
log="$(mktemp)"
|
||||
if ! (cd "$HERE" && "$bun" install --frozen-lockfile && "$bun" run scripts/build.ts) >"$log" 2>&1; then
|
||||
cat "$log" >&2
|
||||
rm -f "$log"
|
||||
echo "install.sh: the build failed (output above)" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$log"
|
||||
fi
|
||||
fi
|
||||
[ -x "$binary" ] || { echo "install.sh: no binary at $binary (build it with: bun run build)" >&2; exit 1; }
|
||||
"$binary" --version >/dev/null 2>&1 || { echo "install.sh: $binary does not run" >&2; exit 1; }
|
||||
mkdir -p "$BIN_DIR"
|
||||
install -m 0755 "$binary" "$TARGET.new" && mv -f "$TARGET.new" "$TARGET"
|
||||
say " installed $TARGET ($("$TARGET" --version))"
|
||||
|
||||
# ── where the config is ────────────────────────────────────────────────────────────────────
|
||||
# Asked of the binary before anything is written there: where it says is where the starter files
|
||||
# go. A binary that does not know the question (a stand-in) leaves the default.
|
||||
if where="$("$TARGET" __paths 2>/dev/null)"; then
|
||||
said="$(printf '%s\n' "$where" | sed -n 's/^config=//p' | head -n1)"
|
||||
case "$said" in /*) CONFIG="$said" ;; esac
|
||||
fi
|
||||
|
||||
# ── starter config ─────────────────────────────────────────────────────────────────────────
|
||||
# Only where there is none: an existing file is the user's, whatever is in it.
|
||||
[ "$starter" -eq 1 ] && mkdir -p "$CONFIG"
|
||||
if [ "$starter" -eq 1 ] && [ ! -e "$CONFIG/config.yaml" ]; then
|
||||
cat >"$CONFIG/config.yaml" <<'YAML'
|
||||
# yaml-language-server: $schema=schema/config.schema.json
|
||||
# LLeMbas CLI settings. Every key is optional; `lembas config check` validates this file.
|
||||
# The full list: https://llembas.eu/docs/. An editor with a YAML language server
|
||||
# completes and checks keys as you type (the first line points it at the schema).
|
||||
|
||||
# The model to start with, as connection/model from connections.yaml.
|
||||
# model: local/qwen3
|
||||
|
||||
# manual (asks before changes) | edit (changes files in the project freely) | plan
|
||||
mode: manual
|
||||
|
||||
# personality: concise # concise | pragmatic | optimistic | funny | formal | socratic | custom
|
||||
# instructions: "Answer in Slovak; I know Python well." # how you want to be helped
|
||||
# icons: plain # one-column symbols, for a terminal font without emoji
|
||||
# mouse: false # leave the mouse to the terminal (its own selection and copy)
|
||||
YAML
|
||||
say " wrote $CONFIG/config.yaml"
|
||||
fi
|
||||
if [ "$starter" -eq 1 ] && [ ! -e "$CONFIG/connections.yaml" ]; then
|
||||
(umask 077 && cat >"$CONFIG/connections.yaml" <<'YAML'
|
||||
# yaml-language-server: $schema=schema/connections.schema.json
|
||||
# Where the models are. Global only: a project can never point LLeMbas CLI elsewhere.
|
||||
# Keys are never written here in plain text: {env:VAR}, {file:~/path}, or key_cmd.
|
||||
# Then set `model: <connection>/<model>` in config.yaml, and check with `lembas models`.
|
||||
connections: {}
|
||||
#
|
||||
# connections:
|
||||
# local: # llama.cpp / llama-swap / vLLM / LM Studio — OpenAI-compatible
|
||||
# dialect: openai-chat
|
||||
# base_url: http://127.0.0.1:8080/v1
|
||||
# models:
|
||||
# qwen3: { context: 131072, max_output: 65536 }
|
||||
# anthropic:
|
||||
# dialect: anthropic
|
||||
# base_url: https://api.anthropic.com
|
||||
# api_key: "{env:ANTHROPIC_API_KEY}"
|
||||
# models:
|
||||
# claude-sonnet-5: { context: 200000 }
|
||||
YAML
|
||||
)
|
||||
chmod 600 "$CONFIG/connections.yaml"
|
||||
say " wrote $CONFIG/connections.yaml (0600) — add a connection there"
|
||||
fi
|
||||
# The schemas for editors follow the binary, so they are rewritten on every install.
|
||||
"$TARGET" config schema >/dev/null && say " wrote $CONFIG/schema/ (JSON Schemas for editors)"
|
||||
|
||||
# ── aliases ────────────────────────────────────────────────────────────────────────────────
|
||||
# A name is free when no command, function or alias of that name exists already — checked in
|
||||
# this shell and in the rc files, since an alias defined there is invisible to this script.
|
||||
taken() {
|
||||
local name="$1" f
|
||||
command -v "$name" >/dev/null 2>&1 && return 0
|
||||
while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
# Our block is left out: the alias there is the one this replaces.
|
||||
awk -v b="$BEGIN" -v e="$END" '$0==b{skip=1;next} $0==e{skip=0;next} !skip' "$f" |
|
||||
grep -Eq "^[[:space:]]*(alias[[:space:]]+${name}=|${name}[[:space:]]*\(\)|function[[:space:]]+${name}\b)" && return 0
|
||||
done < <(rc_files)
|
||||
return 1
|
||||
}
|
||||
|
||||
want_alias() {
|
||||
local name="$1"
|
||||
if taken "$name"; then
|
||||
say " '$name' is already a command or alias here — not adding it"
|
||||
return 1
|
||||
fi
|
||||
case "$aliases" in
|
||||
yes) return 0 ;;
|
||||
no) return 1 ;;
|
||||
esac
|
||||
[ "$yes" -eq 1 ] && return 1
|
||||
[ -t 0 ] || return 1
|
||||
local answer
|
||||
read -r -p " Add the alias '$name' for lembas? [y/N] " answer
|
||||
[[ "$answer" =~ ^[Yy] ]]
|
||||
}
|
||||
|
||||
extra=""
|
||||
# One alias, `agent`.
|
||||
if want_alias agent; then extra+="alias agent='lembas'"$'\n'; fi
|
||||
|
||||
# ── rc files ───────────────────────────────────────────────────────────────────────────────
|
||||
block="$BEGIN
|
||||
# Added by LLeMbas CLI's install.sh; re-run it to change, or ./install.sh --uninstall.
|
||||
case \":\$PATH:\" in *\":\$HOME/.local/bin:\"*) ;; *) export PATH=\"\$HOME/.local/bin:\$PATH\" ;; esac
|
||||
${extra}$END"
|
||||
|
||||
while IFS= read -r f; do
|
||||
strip_block "$f"
|
||||
[ -f "$f" ] || : >"$f"
|
||||
# Keep a blank line between the user's content and ours.
|
||||
[ -s "$f" ] && [ -n "$(tail -c 1 "$f")" ] && printf '\n' >>"$f"
|
||||
printf '%s\n' "$block" >>"$f"
|
||||
say " updated $f"
|
||||
done < <(rc_files)
|
||||
|
||||
# ── another `lembas` first on PATH ─────────────────────────────────────────────────────────
|
||||
# A LLeMbas server installed with pip may bring a console script called `lembas` too (the server
|
||||
# itself runs as `lembas-server`). Whichever comes first on PATH is what `lembas` runs, so a server
|
||||
# installed into ~/.local or a venv on PATH would shadow this one. The PATH
|
||||
# a new shell gets: ours first when the rc block adds it, as it is when it is already there.
|
||||
eff="$PATH"
|
||||
case ":$PATH:" in *":$BIN_DIR:"*) ;; *) eff="$BIN_DIR:$PATH" ;; esac
|
||||
first="$(PATH="$eff"; hash -r; command -v lembas 2>/dev/null || true)"
|
||||
if [ -n "$first" ] && [ "$(readlink -f "$first")" != "$(readlink -f "$TARGET")" ]; then
|
||||
what="another program"
|
||||
if head -c 300 "$first" 2>/dev/null | head -n 1 | grep -q '^#!.*python'; then
|
||||
what="a Python console script, most likely the LLeMbas server's own \`lembas\`"
|
||||
fi
|
||||
say ""
|
||||
say "⚠ $first comes before $TARGET on PATH. It is $what,"
|
||||
say " so \`lembas\` would run it, not LLeMbas CLI. Fix: put $BIN_DIR before $(dirname "$first") in PATH,"
|
||||
say " or remove that one (the server itself runs as lembas-server). lembas config check says it too."
|
||||
fi
|
||||
|
||||
say ""
|
||||
say "Done. Open a new shell (or: source your rc file), then run: lembas"
|
||||
[ -n "$extra" ] && say "Aliases: $(printf '%s' "$extra" | sed -n "s/^alias \([a-z]*\)=.*/\1/p" | paste -sd' ')"
|
||||
if [ "$starter" -eq 1 ] && [ -z "$(sed -n 's/^model: *//p' "$CONFIG/config.yaml")" ]; then
|
||||
say "Next: add a connection to $CONFIG/connections.yaml and set model: in config.yaml."
|
||||
fi
|
||||
Reference in new issue
Block a user