Skip to content

Commit e6cd672

Browse files
authored
Merge pull request #22 from lemonade-sdk/snap_claude
Add claude and codex AI assistant support
2 parents 783e2ad + ab524c6 commit e6cd672

3 files changed

Lines changed: 255 additions & 3 deletions

File tree

scripts/claude

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/bash
2+
# Shim to launch the user-installed (or auto-downloaded) claude CLI.
3+
#
4+
# Problems this solves in strict confinement:
5+
# 1. lemonade-server sets HOME=$SNAP_COMMON, so claude would look for its
6+
# config in the wrong place. We derive the real home from SNAP_USER_COMMON
7+
# and restore it before exec-ing claude.
8+
# 2. If claude isn't installed yet, we download @anthropic-ai/claude-code from
9+
# the npm registry using curl, extract cli.js, and write a node wrapper that
10+
# uses $SNAP/usr/bin/node (always executable inside the snap).
11+
12+
CLAUDE_BIN="$SNAP_USER_COMMON/tools/bin/claude"
13+
MODULES_DIR="$SNAP_USER_COMMON/tools/lib/node_modules/@anthropic-ai/claude-code"
14+
15+
_install_claude() {
16+
echo "claude not found at $CLAUDE_BIN" >&2
17+
echo "Downloading @anthropic-ai/claude-code from the npm registry..." >&2
18+
19+
mkdir -p "$SNAP_USER_COMMON/tools/bin" "$MODULES_DIR"
20+
21+
# Resolve the latest version tag and associated tarball URL and checksum
22+
local metadata version tarball shasum
23+
metadata=$(curl -sf "https://registry.npmjs.org/@anthropic-ai/claude-code/latest")
24+
version=$(printf '%s\n' "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -1)
25+
tarball=$(printf '%s\n' "$metadata" | sed -n 's/.*"tarball":"\([^"]*\)".*/\1/p' | head -1)
26+
shasum=$(printf '%s\n' "$metadata" | sed -n 's/.*"shasum":"\([^"]*\)".*/\1/p' | head -1)
27+
28+
if [ -z "$version" ] || [ -z "$tarball" ] || [ -z "$shasum" ]; then
29+
echo "ERROR: could not resolve @anthropic-ai/claude-code metadata from npm registry." >&2
30+
echo "Check your network connection, or install manually:" >&2
31+
echo " npm install -g @anthropic-ai/claude-code --prefix '$SNAP_USER_COMMON/tools'" >&2
32+
exit 1
33+
fi
34+
35+
echo "Downloading claude v${version}..." >&2
36+
local tmp_tgz
37+
tmp_tgz=$(mktemp "${TMPDIR:-/tmp}/claude-code.XXXXXX.tgz")
38+
39+
if ! curl -fsSL "$tarball" -o "$tmp_tgz"; then
40+
echo "ERROR: download of @anthropic-ai/claude-code v${version} failed." >&2
41+
rm -f "$tmp_tgz"
42+
exit 1
43+
fi
44+
45+
# Verify the downloaded tarball against the npm-published shasum
46+
local downloaded_shasum
47+
downloaded_shasum=$(sha1sum "$tmp_tgz" | awk '{print $1}')
48+
if [ "$downloaded_shasum" != "$shasum" ]; then
49+
echo "ERROR: checksum verification failed for @anthropic-ai/claude-code v${version}." >&2
50+
rm -f "$tmp_tgz"
51+
exit 1
52+
fi
53+
54+
if ! tar -xzf "$tmp_tgz" --strip-components=1 -C "$MODULES_DIR"; then
55+
echo "ERROR: extraction of @anthropic-ai/claude-code v${version} failed." >&2
56+
rm -f "$tmp_tgz"
57+
exit 1
58+
fi
59+
60+
rm -f "$tmp_tgz"
61+
62+
# Make the bundled ripgrep executable so claude's search features work
63+
chmod +x "$MODULES_DIR/vendor/ripgrep/x64-linux/rg" 2>/dev/null || true
64+
65+
# Write a small wrapper that uses the node binary bundled with the snap.
66+
# $SNAP is set by snapd at runtime, so this always resolves correctly even
67+
# after snap refreshes.
68+
cat > "$CLAUDE_BIN" << 'WRAPPER'
69+
#!/bin/sh
70+
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
71+
exec "${SNAP}/usr/bin/node" \
72+
"$SCRIPT_DIR/../lib/node_modules/@anthropic-ai/claude-code/cli.js" "$@"
73+
WRAPPER
74+
chmod +x "$CLAUDE_BIN"
75+
echo "claude v${version} installed to $CLAUDE_BIN" >&2
76+
}
77+
78+
if [ ! -x "$CLAUDE_BIN" ]; then
79+
_install_claude
80+
fi
81+
82+
# Use SNAP_USER_COMMON as HOME so claude stores its config within the snap's
83+
# user data directory (~/.claude → $SNAP_USER_COMMON/.claude), which is
84+
# accessible under strict confinement without a personal-files plug.
85+
# Unset CLAUDECODE so claude doesn't refuse to start when the user is already
86+
# running a Claude Code session in the same shell environment.
87+
exec env -u CLAUDECODE HOME="$SNAP_USER_COMMON" "$CLAUDE_BIN" "$@"

scripts/codex

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/bin/bash
2+
# Shim to launch the user-installed (or auto-downloaded) codex CLI.
3+
#
4+
# Problems this solves in strict confinement:
5+
# 1. lemonade-server sets HOME=$SNAP_COMMON, so codex would look for its
6+
# config in the wrong place. We derive the real home from SNAP_USER_COMMON
7+
# and restore it before exec-ing codex.
8+
# 2. If codex isn't installed yet, we download the platform-specific
9+
# @openai/codex tarball from the npm registry using curl and extract the
10+
# statically-linked native binary directly — no Node.js required.
11+
12+
CODEX_BIN="$SNAP_USER_COMMON/tools/bin/codex"
13+
14+
_install_codex() {
15+
echo "codex not found at $CODEX_BIN" >&2
16+
echo "Downloading @openai/codex from the npm registry..." >&2
17+
18+
mkdir -p "$SNAP_USER_COMMON/tools/bin"
19+
20+
# Resolve the latest version tag and integrity from npm metadata
21+
local metadata version integrity
22+
metadata=$(curl -sf "https://registry.npmjs.org/@openai/codex/latest")
23+
version=$(printf '%s\n' "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -1)
24+
# Extract the dist.integrity field (e.g., "sha512-<base64>") for the resolved version
25+
integrity=$(printf '%s\n' "$metadata" | sed -n 's/.*"integrity":"\([^"]*\)".*/\1/p' | head -1)
26+
27+
if [ -z "$version" ] || [ -z "$integrity" ]; then
28+
echo "ERROR: could not resolve @openai/codex version and integrity from npm registry." >&2
29+
echo "Check your network connection, or install manually (after verifying checksum):" >&2
30+
echo " curl -fsSL https://registry.npmjs.org/@openai/codex/-/codex-VERSION-linux-x64.tgz \\" >&2
31+
echo " | tar -xzOf - package/vendor/x86_64-unknown-linux-musl/codex/codex \\" >&2
32+
echo " > '$CODEX_BIN' && chmod +x '$CODEX_BIN'" >&2
33+
exit 1
34+
fi
35+
36+
echo "Downloading codex v${version} (linux-x64 native binary)..." >&2
37+
local tarball="https://registry.npmjs.org/@openai/codex/-/codex-${version}-linux-x64.tgz"
38+
39+
# Download tarball to a temporary file so we can verify its integrity.
40+
local tmp_tgz
41+
tmp_tgz=$(mktemp "${TMPDIR:-/tmp}/codex-tarball.XXXXXX") || {
42+
echo "ERROR: failed to create temporary file for @openai/codex download." >&2
43+
exit 1
44+
}
45+
46+
if ! curl -fsSL "$tarball" -o "$tmp_tgz"; then
47+
echo "ERROR: download of @openai/codex v${version}-linux-x64 failed." >&2
48+
rm -f "$tmp_tgz"
49+
rm -f "$CODEX_BIN"
50+
exit 1
51+
fi
52+
53+
# Verify tarball integrity against the npm dist.integrity field (sha512-<base64>).
54+
local expected_prefix="sha512-"
55+
local expected actual
56+
expected="$integrity"
57+
if [ "${expected#"$expected_prefix"}" = "$expected" ]; then
58+
echo "ERROR: unsupported integrity format '$integrity' (expected sha512-*)." >&2
59+
rm -f "$tmp_tgz"
60+
rm -f "$CODEX_BIN"
61+
exit 1
62+
fi
63+
expected="${expected#"$expected_prefix"}"
64+
65+
# Compute base64-encoded SHA-512 of the downloaded tarball.
66+
if ! actual=$(openssl dgst -sha512 -binary "$tmp_tgz" 2>/dev/null | openssl base64 -A); then
67+
echo "ERROR: failed to compute SHA-512 checksum for @openai/codex tarball." >&2
68+
rm -f "$tmp_tgz"
69+
rm -f "$CODEX_BIN"
70+
exit 1
71+
fi
72+
73+
if [ "$actual" != "$expected" ]; then
74+
echo "ERROR: integrity check failed for @openai/codex v${version}-linux-x64 tarball." >&2
75+
echo "Expected (from npm dist.integrity): $expected" >&2
76+
echo "Actual (computed): $actual" >&2
77+
rm -f "$tmp_tgz"
78+
rm -f "$CODEX_BIN"
79+
exit 1
80+
fi
81+
82+
# The linux-x64 tarball ships a statically-linked musl binary; extract it directly.
83+
if ! tar -xzOf "$tmp_tgz" package/vendor/x86_64-unknown-linux-musl/codex/codex \
84+
> "$CODEX_BIN"; then
85+
echo "ERROR: failed to extract @openai/codex v${version}-linux-x64 binary." >&2
86+
rm -f "$tmp_tgz"
87+
rm -f "$CODEX_BIN"
88+
exit 1
89+
fi
90+
91+
rm -f "$tmp_tgz"
92+
chmod +x "$CODEX_BIN"
93+
echo "codex v${version} installed to $CODEX_BIN" >&2
94+
}
95+
96+
if [ ! -x "$CODEX_BIN" ]; then
97+
_install_codex
98+
fi
99+
100+
# OPENAI_BASE_URL is set by lemonade-server to the server's /v1/ endpoint.
101+
# Forward it as CODEX_OSS_BASE_URL so codex points at lemonade instead of
102+
# the default Ollama port, and inject -c oss_provider=ollama so the
103+
# LM Studio/Ollama picker is skipped.
104+
CODEX_OSS_BASE_URL="${OPENAI_BASE_URL%/}"
105+
export CODEX_OSS_BASE_URL
106+
107+
# Extract the model name passed via -m / --model so we can look up the actual
108+
# context window from the running lemonade server.
109+
MODEL_NAME=""
110+
for arg in "$@"; do
111+
if [ -n "$_NEXT_IS_MODEL" ]; then
112+
MODEL_NAME="$arg"
113+
break
114+
fi
115+
case "$arg" in
116+
-m|--model) _NEXT_IS_MODEL=1 ;;
117+
-m=*|--model=*) MODEL_NAME="${arg#*=}"; break ;;
118+
esac
119+
done
120+
121+
# Query /api/v1/health for the loaded model's actual ctx_size and derive a
122+
# sensible tool_output_token_limit (1/8 of context window, min 8192).
123+
CONTEXT_ARGS=()
124+
if [ -n "$MODEL_NAME" ] && [ -n "$OPENAI_BASE_URL" ]; then
125+
SERVER_BASE="${OPENAI_BASE_URL%%/v1*}"
126+
ctx_size=$(curl -sf "$SERVER_BASE/api/v1/health" 2>/dev/null \
127+
| jq -r --arg model "$MODEL_NAME" \
128+
'.all_models_loaded[]? | select(.model_name == $model) | .recipe_options.ctx_size // empty' \
129+
2>/dev/null | head -n1)
130+
if [ -n "$ctx_size" ] && [ "$ctx_size" -gt 0 ] 2>/dev/null; then
131+
tool_limit=$(( ctx_size / 8 ))
132+
[ "$tool_limit" -lt 8192 ] && tool_limit=8192
133+
CONTEXT_ARGS=(
134+
-c "model_context_window=$ctx_size"
135+
-c "model_auto_compact_token_limit=$tool_limit"
136+
)
137+
fi
138+
fi
139+
140+
# Use SNAP_USER_COMMON as HOME so codex stores its config within the snap's
141+
# user data directory (~/.codex → $SNAP_USER_COMMON/.codex), which is
142+
# accessible under strict confinement without a personal-files plug.
143+
exec env HOME="$SNAP_USER_COMMON" "$CODEX_BIN" \
144+
-c "oss_provider=\"ollama\"" "${CONTEXT_ARGS[@]}" "$@"

snap/snapcraft.yaml

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ platforms:
4949
layout:
5050
/usr/share/lemonade-server:
5151
symlink: $SNAP/usr/share/lemonade-server
52+
# Node.js externalizes some builtins to /usr/share/nodejs at a hardcoded path.
53+
# Bind the snap's copy so node can find them at runtime.
54+
/usr/share/nodejs:
55+
bind: $SNAP/usr/share/nodejs
5256
# Add layout entries so XRT's hardcoded absolute paths resolve to the snap's copies
5357
/usr/lib/x86_64-linux-gnu/libxrt_core.so.2:
5458
bind-file: $SNAP/usr/lib/x86_64-linux-gnu/libxrt_core.so.2.21.75
@@ -62,7 +66,6 @@ plugs:
6266
interface: content
6367
target: $SNAP/gpu-2404
6468
default-provider: mesa-2404
65-
6669
slots:
6770
# Adds a pseudo content interface to allow clients to express their
6871
# runtime dependency on lemonade-server
@@ -101,8 +104,8 @@ apps:
101104
- hardware-observe
102105
- system-observe
103106
environment:
104-
LD_LIBRARY_PATH: "$SNAP/usr/share/lemonade-server:$SNAP_COMMON/cache/lemonade/bin/llamacpp/vulka n:$SNAP_COMMON/cache/lemonade/bin/whispercpp/vulkan:$SNAP/usr/lib/x86_64-linux-gnu:$SNAP/lib/x86_64-linux-gnu:/var/lib/snapd/lib/gl${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
105-
PATH: "$SNAP/usr/bin:$PATH"
107+
LD_LIBRARY_PATH: "$SNAP/usr/share/lemonade-server:$SNAP_COMMON/cache/lemonade/bin/llamacpp/vulkan:$SNAP_COMMON/cache/lemonade/bin/whispercpp/vulkan:$SNAP/usr/lib/x86_64-linux-gnu:$SNAP/lib/x86_64-linux-gnu:/var/lib/snapd/lib/gl${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
108+
PATH: "$SNAP/usr/bin:$SNAP_USER_COMMON/tools/bin:$PATH"
106109
HOME: "$SNAP_COMMON"
107110
LEMONADE_CACHE_DIR: "$SNAP_COMMON/cache/lemonade"
108111
FLM_MODEL_PATH: $SNAP_COMMON/flm/models
@@ -169,6 +172,10 @@ parts:
169172
- libatomic1
170173
- libcurl4t64
171174
- curl # for metrics
175+
- nodejs # for running @anthropic-ai/claude-code cli.js when auto-downloaded
176+
- node-cjs-module-lexer # nodejs externalized builtin
177+
- node-undici # nodejs externalized builtin
178+
- node-acorn # nodejs externalized builtins (acorn + acorn-walk)
172179
- unzip # For extracting backends at runtime
173180
- libdrm-amdgpu1 # SD backends need this at runtime
174181
- libnuma1 # SD backends need this at runtime
@@ -447,7 +454,21 @@ parts:
447454
organize:
448455
lemonade-server-wrapper: bin/lemonade-server-wrapper
449456
metrics: bin/metrics
457+
claude: usr/bin/claude
458+
codex: usr/bin/codex
450459
override-prime: |
451460
craftctl default
452461
chmod +x $CRAFT_PRIME/bin/lemonade-server-wrapper
453462
chmod +x $CRAFT_PRIME/bin/metrics
463+
chmod +x $CRAFT_PRIME/usr/bin/claude
464+
chmod +x $CRAFT_PRIME/usr/bin/codex
465+
466+
tools:
467+
plugin: nil
468+
stage-packages:
469+
- gh
470+
- git
471+
- jq
472+
- make
473+
- patch
474+
- ripgrep

0 commit comments

Comments
 (0)