Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 32 additions & 28 deletions skills/local-ai-use/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ The skill does three things:

> **Requires modern Lemonade (v10.1.0 or newer).** Modern Lemonade unified
> everything under one `lemonade` CLI (`lemonade status`, `lemonade pull`, ...)
> driving an always-on `lemond` service. This skill targets that and installs
> Lemonade only from the official installers (see below) — never via
> `pip install lemonade-sdk`, which is a separate, older release line. If an
> older/incompatible `lemonade` is already on the PATH (from any channel — an
> old `.msi`/`.deb` or a pip install), it will shadow the modern CLI; uninstall
> it first (see the removal commands in Step 1a) before running this skill.
> driving an always-on `lemond` service. `lemonade` is the only valid CLI, and
> this skill installs Lemonade only from the [official install
> paths](https://lemonade-server.ai/docs/guide/install/) listed in Step 1a. If
> an older `lemonade` is already on the `PATH` it will shadow the modern CLI;
> uninstall it first (see the removal commands in Step 1a) before running this
> skill.

Models are **not** downloaded during setup. Each default model is pulled
lazily, on first use, by the routing rule (e.g. the first image request pulls
Expand All @@ -69,12 +69,12 @@ instead.
## Prerequisites

- **OS:** Windows 11 x64, Ubuntu/Debian x64, or macOS (beta).
- **Lemonade:** the setup script installs it if missing. It downloads and
silently installs the latest version (Windows `lemonade.msi`, the
Ubuntu/Debian `ppa:lemonade-team/stable` PPA, or the macOS `.pkg`). The
`lemond` service auto-starts after install; the script waits for it rather
than launching it. On Linux/macOS the install needs `sudo`. Pass
`--no-install` if the user wants to install it themselves instead.
- **Lemonade:** the setup script installs the latest version if missing, using
`winget` on Windows, the `ppa:lemonade-team/stable` PPA on Ubuntu/Debian, and
the Homebrew cask on macOS (see Step 1a for the fallbacks). The `lemond`
service auto-starts after install; the script waits for it rather than
launching it. On Linux the install needs `sudo`. Pass `--no-install` if the
user wants to install it themselves instead.
- **Disk:** ~8 GB free for the three default models (SD-Turbo + Whisper-Tiny
+ kokoro-v1), plus ~0.1 GB for the installer itself.
- **Network:** required for the install download and the first `lemonade pull`
Expand Down Expand Up @@ -119,28 +119,32 @@ so you can do it by hand or debug it:
**1a. Is a modern `lemonade` CLI installed?** Run `lemonade status`. The check
is by *capability*, not by name: modern Lemonade prints `Server is running...`
or `Server is not running`. If instead you get an "invalid choice" / usage
error, the `lemonade` on `PATH` is an old, incompatible build that predates the
unified CLI (v10.1.0) — do **not** use it. It could have come from any install
channel, so remove it based on how it was installed, then re-run this skill (or
install Lemonade manually):
error, the `lemonade` on `PATH` is an old build that predates the unified CLI
(v10.1.0) — do **not** use it. Remove it, then re-run this skill:

| Installed via | Uninstall with |
| OS | Uninstall the old build with |
|---|---|
| Windows `.msi` | `winget uninstall -e --id AMD.LemonadeServer`, or Settings > Apps > Installed apps > Lemonade Server > Uninstall |
| Ubuntu/Debian apt/PPA | `sudo apt remove lemonade-server` |
| pip | `pip uninstall lemonade-sdk` |
| macOS `.pkg` | Delete the installed `Lemonade.app` / remove the package receipt |
| Windows | `winget uninstall -e --id AMD.LemonadeServer`, or Settings > Apps > Installed apps > Lemonade Server > Uninstall |
| Ubuntu/Debian | `sudo apt remove lemonade-server` |
| macOS | `brew uninstall --cask lemonade-server`, or delete the installed `Lemonade.app` and its `.pkg` receipt |

Never try to drive or auto-remove it for the user.

If no `lemonade` is found at all, install the latest version on the user's
behalf:
behalf. Use the package manager first; the download is the fallback when the
package manager is absent. Full matrix, including Arch, Fedora, Debian, Snap,
and Docker, is in the [install docs](https://lemonade-server.ai/docs/guide/install/).

| OS | Install |
|---|---|
| Windows | Download `lemonade.msi` from the [latest release](https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi) and run `msiexec /i lemonade.msi /qn` (silent, per-user, no elevation). |
| Ubuntu/Debian | `sudo add-apt-repository -y ppa:lemonade-team/stable && sudo apt-get update && sudo apt-get install -y lemonade-server` (the apt package is `lemonade-server`; the CLI you then run is `lemonade`) |
| macOS (beta) | Download the `Lemonade-<ver>-Darwin.pkg` from the latest release and run `sudo installer -pkg Lemonade-<ver>-Darwin.pkg -target /`. |
| OS | Install | Fallback |
|---|---|---|
| Windows | `winget install -e --id AMD.LemonadeServer` | Download [`lemonade.msi`](https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi) and run `msiexec /i lemonade.msi /qn` (silent, per-user, no elevation). |
| Ubuntu | `sudo add-apt-repository -y ppa:lemonade-team/stable && sudo apt-get update && sudo apt-get install -y lemonade-server` | `sudo snap install lemonade-server` |
| macOS | `brew install --cask lemonade-server` | Download `Lemonade-<ver>-Darwin.pkg` from the [latest release](https://github.com/lemonade-sdk/lemonade/releases/latest) and run `sudo installer -pkg Lemonade-<ver>-Darwin.pkg -target /`. |

The Ubuntu apt package is named `lemonade-server`, but the CLI it installs is
`lemonade`. The browser UI is served at `http://localhost:13305` with no extra
package; add `sudo apt install lemonade-desktop` only if the user wants the
desktop frontend.

After a Windows install the CLI lands in `%LOCALAPPDATA%\lemonade_server` and
is added to the *user* PATH (new shells only); the setup script probes that
Expand Down Expand Up @@ -250,7 +254,7 @@ machine.
| Symptom | Cause | Recovery |
|---|---|---|
| `lemonade: command not found` | CLI not installed | Re-run `python scripts/setup_local_ai.py` (auto-installs the latest version). If it just installed on Windows, open a new shell so the user PATH refreshes, or the script will find it under `%LOCALAPPDATA%\lemonade_server`. |
| `status` gives an "invalid choice" / usage error | An old, incompatible `lemonade` (pre-v10.1.0, from any install channel) is shadowing the modern CLI | Uninstall it the way it was installed (see the Step 1a table: `winget uninstall -e --id AMD.LemonadeServer` / `sudo apt remove lemonade-server` / `pip uninstall lemonade-sdk`), then re-run the setup script or install Lemonade from the docs link. |
| `status` gives an "invalid choice" / usage error | An old `lemonade` (pre-v10.1.0) is shadowing the modern CLI | Uninstall it (see the Step 1a table: `winget uninstall -e --id AMD.LemonadeServer` / `sudo apt remove lemonade-server` / `brew uninstall --cask lemonade-server`), then re-run the setup script. |
| `Server is not running` | `lemond` service stopped | Start it via the OS service manager — `sudo systemctl start lemond` / `systemctl --user start lemond` (Linux), `launchctl load /Library/LaunchDaemons/com.lemonade.server.plist` (macOS), or the tray app / `Start-Service lemond` (Windows). There is no `lemonade serve`. |
| `POST /v1/images/generations` returns 404 model not found | Image model not downloaded | `lemonade pull SD-Turbo` and retry. |
| `lemonade pull` keeps printing `Progress: NN%` but never finishes | Download target is a bad path (out of space, no write permission, quota, read-only mount). The write error may surface only in the server log while the console keeps showing progress | Check the target and free space first: `GET /api/v1/system-info` reports `models_dir` and `model_storage.free_bytes`. If a pull stalls, read the recent lines of the server log (typically `lemonade-server.log` in the OS temp dir) for the real error (e.g. a download/write failure like `CURL code 23`, or an out-of-space message), then point the download at a writable disk with room. |
Expand Down
120 changes: 68 additions & 52 deletions skills/local-ai-use/scripts/setup_local_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@
<workspace>/AGENTS.md, between stable BEGIN/END markers so re-runs
replace the block in place rather than appending.

Modern Lemonade (v10.1.0+) unified everything under the single `lemonade`
CLI (subcommands `status`, `pull`, `run`, ...) driving an always-on `lemond`
service. Older/incompatible builds of `lemonade` -- whichever way they were
installed (an old .msi/.deb, or the pip `lemonade-sdk` package) -- lack the
service-control subcommands and are NOT supported here. The CLI check below
therefore probes *capability* (does `lemonade status` work?) rather than
trusting the binary name, so an old incompatible CLI on PATH is reported
clearly instead of being driven into a 90-second dead end.
Modern Lemonade unified everything under a single `lemonade` CLI (subcommands
`status`, `pull`, `run`, ...) driving an always-on `lemond` service. That is
the only CLI this skill supports. An older build left on PATH lacks those
subcommands, so the check below probes *capability* (does `lemonade status`
work?) rather than trusting the binary name, and reports a stale CLI clearly
instead of driving it into a 90-second dead end.

Setup never downloads models: the default image/TTS/STT models are pulled
on first use, by the installed AGENTS.md rule (see its failure
Expand Down Expand Up @@ -82,10 +80,14 @@
SKILL_DIR = Path(__file__).resolve().parent.parent
RULE_TEMPLATE = SKILL_DIR / "templates" / "local-ai-rule.md"

# The *full* Windows installer: Lemonade plus the desktop app and the
# always-on `lemond` service. `releases/latest/download/<asset>` always
# resolves to the newest published asset of that exact name, so we never have
# to pin a version.
# Windows: winget is the current recommended install and keeps Lemonade
# upgradable in place, so we try it first.
WINDOWS_WINGET_ID = "AMD.LemonadeServer"

# Fallback when winget is unavailable: the *full* Windows installer, Lemonade
# plus the desktop app and the always-on `lemond` service.
# `releases/latest/download/<asset>` always resolves to the newest published
# asset of that exact name, so we never have to pin a version.
WINDOWS_MSI_URL = (
"https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi"
)
Expand All @@ -96,6 +98,11 @@
os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
) / "lemonade_server"

# macOS: the Homebrew cask is the current recommended install. It also avoids
# the unauthenticated GitHub API call the .pkg fallback needs, which is rate
# limited per source IP.
MACOS_BREW_CASK = "lemonade-server"

# GitHub release metadata, used to resolve the versioned macOS .pkg asset
# (its filename embeds the version, so there is no stable latest/download URL).
GITHUB_LATEST_RELEASE_API = (
Expand All @@ -112,10 +119,9 @@
"sudo apt-get install -y lemonade-server"
)

# Modern Lemonade exposes a single `lemonade` CLI (it drives the always-on
# `lemond` service). We deliberately do NOT fall back to the deprecated
# `lemonade-server` CLI or the pip "eval" build; instead we verify the CLI we
# find is actually the modern one via a capability probe (see find_cli).
# Modern Lemonade exposes exactly one CLI, `lemonade`, driving the always-on
# `lemond` service. There is no second binary to fall back to, so instead of
# trying other names we verify this one is the modern build (see find_cli).
CLI_NAME = "lemonade"

# Docs URL to point users at when they must install/upgrade Lemonade by hand.
Expand Down Expand Up @@ -171,13 +177,11 @@ def is_modern_cli(cli: str) -> bool:
"""True if `cli` is the modern Lemonade CLI (drives the `lemond` service).

Capability probe, not a name check: the modern `lemonade` CLI exposes a
`status` subcommand that reports on the service. An older or otherwise
incompatible `lemonade` on PATH -- regardless of how it was installed (an
old .msi/.deb, or the pip `lemonade-sdk` build) -- does not recognise the
subcommand and errors with an argparse "invalid choice" instead. Running
`lemonade status` is cheap, does not mutate anything, and tells us both
that the CLI is modern AND whether the service is already up, so we reuse
it as the single discriminator.
`status` subcommand that reports on the service. An older `lemonade` on
PATH does not recognise the subcommand and errors with an argparse
"invalid choice" instead. Running `lemonade status` is cheap, does not
mutate anything, and tells us both that the CLI is modern AND whether the
service is already up, so we reuse it as the single discriminator.

A modern CLI prints "Server is running..." or "Server is not running"
(exiting 0 or 1 accordingly). We key off that phrasing rather than the
Expand Down Expand Up @@ -206,8 +210,7 @@ def find_cli() -> tuple[str | None, str | None]:
Returns ``(modern_cli, stale_cli)``:

- ``(path, None)`` a modern, capable `lemonade` CLI was found -> use it.
- ``(None, path)`` a `lemonade` executable exists but is NOT the modern
CLI (old pip "eval" build, or otherwise incompatible)
- ``(None, path)`` a `lemonade` executable exists but is an older build
-> the caller should guide the user to upgrade.
- ``(None, None)`` no `lemonade` executable found at all -> install it.
"""
Expand Down Expand Up @@ -257,7 +260,25 @@ def _run(cmd: list[str] | str, *, shell: bool = False) -> None:


def _install_windows() -> None:
"""Silently install the full lemonade.msi (server + desktop app)."""
"""Install with winget, falling back to the signed full MSI."""
if shutil.which("winget") is not None:
try:
_run(
[
"winget",
"install",
"-e",
"--id",
WINDOWS_WINGET_ID,
"--accept-package-agreements",
"--accept-source-agreements",
"--disable-interactivity",
]
)
_print("Lemonade installed.")
return
except RuntimeError as exc:
_print(f"winget install did not complete ({exc}); trying the MSI.")
msi = Path(tempfile.gettempdir()) / "lemonade.msi"
_download(WINDOWS_MSI_URL, msi)
# /qn = silent, per-user (no elevation needed). The MSI registers the CLI
Expand All @@ -267,7 +288,7 @@ def _install_windows() -> None:


def _install_linux() -> None:
"""Install the stable PPA server plus the desktop frontend on apt distros."""
"""Install from the stable PPA on apt distros."""
if shutil.which("apt-get") is None:
raise RuntimeError(
"Automatic install only supports apt-based distros (Ubuntu/Debian). "
Expand All @@ -283,7 +304,14 @@ def _install_linux() -> None:


def _install_macos() -> None:
"""Download the latest signed .pkg and install it system-wide."""
"""Install the Homebrew cask, falling back to the signed .pkg."""
if shutil.which("brew") is not None:
try:
_run(["brew", "install", "--cask", MACOS_BREW_CASK])
_print("Lemonade installed.")
return
except RuntimeError as exc:
_print(f"Homebrew install did not complete ({exc}); trying the .pkg.")
pkg_url = _resolve_macos_pkg_url()
pkg = Path(tempfile.gettempdir()) / "Lemonade.pkg"
_download(pkg_url, pkg)
Expand Down Expand Up @@ -336,32 +364,21 @@ def service_start_hint() -> str:


def uninstall_hint() -> str:
"""OS-specific ways to remove an old/incompatible Lemonade.

An incompatible `lemonade` could come from any install channel, so we do
not assume pip. We list the removal path for each channel and let the user
apply whichever one matches how they installed it.
"""
"""OS-specific ways to remove an old Lemonade that shadows the modern CLI."""
system = platform.system()
if system == "Linux":
return (
"remove it however it was installed: `sudo apt remove lemonade-server` "
"(apt/PPA) or `pip uninstall lemonade-sdk` (pip)"
)
return "`sudo apt remove lemonade-server`, or the package manager it came from"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make the agent run sudo commands? Sounds risky? Should this prompt a question to the user.

if system == "Darwin": # macOS
return (
"remove it however it was installed: delete the installed "
"`Lemonade.app`/receipt from a .pkg install, or "
"`pip uninstall lemonade-sdk` (pip)"
f"`brew uninstall --cask {MACOS_BREW_CASK}`, or delete the installed "
"`Lemonade.app` and its .pkg receipt"
)
if system == "Windows":
return (
"remove it however it was installed: for an .msi install, run "
"`winget uninstall -e --id AMD.LemonadeServer` or uninstall Lemonade "
"Server from Settings > Apps > Installed apps; or, if it came from "
"pip, `pip uninstall lemonade-sdk`"
f"`winget uninstall -e --id {WINDOWS_WINGET_ID}`, or uninstall "
"Lemonade Server from Settings > Apps > Installed apps"
)
return "remove the old Lemonade using your platform's package manager or `pip uninstall lemonade-sdk`"
return f"remove it with your platform's package manager; see {INSTALL_DOCS_URL}"


def wait_for_server(host: str, port: int, timeout_s: float = 90.0) -> bool:
Expand Down Expand Up @@ -449,6 +466,7 @@ def upsert_agents_md(
)

if not target.exists():
workspace.mkdir(parents=True, exist_ok=True)
target.write_text(
"# Agent instructions\n\n"
"Project-scoped rules picked up automatically by Cursor, Claude Code,\n"
Expand Down Expand Up @@ -524,13 +542,11 @@ def main(argv: list[str] | None = None) -> int:

cli, stale = find_cli()

# An old/incompatible `lemonade` on PATH shadows the modern CLI. It may
# have come from any install channel (old .msi/.deb or pip), so we never
# assume one. We also never drive or auto-remove it -- we guide the user,
# because a shadowing binary will keep hiding a freshly installed modern
# CLI until it is removed.
# An old `lemonade` on PATH shadows the modern CLI. We never drive or
# auto-remove it -- we guide the user, because a shadowing binary will keep
# hiding a freshly installed modern CLI until it is removed.
if cli is None and stale is not None:
_print(f"FAIL: found an old/incompatible Lemonade CLI at {stale}.")
_print(f"FAIL: found an old Lemonade CLI at {stale}.")
_print(
"It is missing the modern `lemonade status` command, so it predates "
"the unified CLI (v10.1.0) and cannot be used by this skill."
Expand Down
Loading