Skip to content
Merged
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
29 changes: 29 additions & 0 deletions plugins/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,35 @@
],
"lastUpdated": "2026-09-29",
"upstream": "https://github.com/vampireJSV/ryoku-plugin-kdeconnect-status"
},
{
"id": "strata",
"name": "Local LLM Manager",
"version": "0.4.0",
"path": "plugins/strata",
"author": "guyb <[email protected]>",
"upstream": "https://github.com/ryoku-dev/ryostore/tree/main/plugins/strata",
"summary": "Pick a local model and start or stop it from the bar.",
"description": "Pick a local model and start or stop it from the bar. Finds Strata, TabbyAPI and llama.cpp engines and their models, one port per model.",
"tags": [
"bar-widget"
],
"accent": "#8f8378",
"surface": "#101010",
"preview": "assets/preview-widget.png",
"screenshots": [
"assets/preview-widget.png",
"assets/preview-panel.png"
],
"manifest": "product-manifest.json",
"manifestSha256": "c0f9626812f2b8e155247286d14d53cfcbf9982f9897bc2a9134335971b1b923",
"official": false,
"tagline": "Pick a local model and start or stop it from the bar.",
"icon": "",
"hosts": [
"topbarGlyph"
],
"lastUpdated": "2026-10-06"
}
]
}
3 changes: 3 additions & 0 deletions plugins/strata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
*.log
*.tmp
84 changes: 84 additions & 0 deletions plugins/strata/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Agent guide for the strata plugin

This folder is a Ryoku shell plugin. If you are an AI agent editing it, follow
every rule below. They are the same rules `ryoku plugin validate` enforces, so
work that breaks one will not install. This file is self-contained: you do not
need any other document to work here.

## The rules

R1 Place. A plugin is one folder named after its id. Author it under
`$(xdg-user-dir DOCUMENTS)/ryoku-plugins/<id>/` (fallback `~/ryoku-plugins/<id>/`);
`ryoku plugin new <id>` creates it there. Never write into
`~/.local/share/ryoku/plugins/` (the store's install root, receipt-owned) or
`~/.config/quickshell/` (the shipped shell).

R2 Shape. `manifest.json` at the root; `service/Main.qml` (logic, no UI);
`content/Widget.qml` (the one view); optional `content/Panel.qml` (a bar panel);
`README.md`; `LICENSE`; `assets/preview-widget.png` (a real capture); scripts
only under `bin/` with a shebang and the exec bit. Every extra file listed in
manifest `files`.

R3 Id. Lowercase `[a-z0-9][a-z0-9-]*`, unique, not a built-in widget id.

R4 Imports. Only `QtQuick*`, `Quickshell*`, `Ryoku.PluginKit`,
`Ryoku.PluginKit.Singletons`, and files inside the plugin folder. Never
`shell.*`, `Ryoku.Ui*` internals, or a relative import that climbs out of the
folder.

R5 Settings. Declared in `metadata.settings` (types the bar renders: toggle,
choice, multi, int, text); read through `pluginApi.pluginSettings` behind a
default; written only through `pluginApi.saveSetting(key, value)`. Never edit
`shell.json` or `plugins.json` directly.

R6 Commands. Every external program the plugin runs is either in its own `bin/`
or listed in `dependencies.commands`. No `sudo`, `doas`, `su`. A privileged
action is allowed only through `pkexec`, only on an explicit click, only when
listed in manifest `capabilities.privileged` (exact command strings) and
explained in README.

R7 Network. Every host the plugin talks to is listed in
`capabilities.network`. No `curl … | sh`, no downloading and running code.

R8 Files. Write only under `pluginApi.stateDir`
(`$XDG_STATE_HOME/ryoku/plugins/<id>`), `$XDG_CACHE_HOME/ryoku/plugins/<id>`, or
a temp dir. Never touch `~/.ssh`, `/etc`, shell rc files, or another plugin's
folder.

R9 Shell. No `sh -c` with a string built from settings or program output
(injection). Pass argv arrays.

R10 Secrets and binaries. No tokens, keys or credentials in the tree; no
compiled binaries (ELF/Mach-O/.so); scripts only.

R11 Honesty. `official` is never true for a community plugin. `author` is
`Name <mail>`. README says what runs, what it reads, what it writes, and every
privileged/network capability.

## The workflow

1. `ryoku plugin new <id>` scaffolds this folder (already done).
2. Edit: `service/Main.qml` for logic, `content/Widget.qml` for the view, and
`content/Panel.qml` for the bar panel. Capture `assets/preview-widget.png`.
3. `ryoku plugin validate <dir>` and fix every blocking finding (warnings are
advisory but read them).
4. `ryoku plugin add <dir> --bar --yes` to install it, then verify it on the bar
and under **QS Bar Settings > Community**.
5. `ryoku plugin share <id>` ONLY when the user asks to publish. Never share a
plugin unless asked.

## What the host sets

Every entry point receives, from the host: `pluginApi` (see below), `s` (a scale
factor), `active`, `density`, and `widthBudget`. Read them; never assign them.

`pluginApi` exposes:

- `mainInstance`: the live `service/Main.qml` instance every view shares.
- `pluginSettings`: the resolved settings object; read behind a default.
- `pluginDir`: this plugin's folder.
- `stateDir`: `$XDG_STATE_HOME/ryoku/plugins/<id>`; the only place to write.
- `saveSetting(key, value)`: persist one setting.
- `panelOpen` (bool), `openPanel()`, `closePanel()`, `togglePanel()`: the bar
panel controls (present but inert off the bar). The sanctioned widget click is
`onClicked: pluginApi.togglePanel()`.
4 changes: 4 additions & 0 deletions plugins/strata/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# CLAUDE.md

Read `AGENTS.md` in this folder before editing anything here.
It holds every rule this plugin must follow and the full authoring workflow.
21 changes: 21 additions & 0 deletions plugins/strata/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 guyb

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
37 changes: 37 additions & 0 deletions plugins/strata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Local LLM Manager

Bar widget for local model servers. It finds every installed engine and its ready models by itself,
runs one model at a time, and gives each model its own port.

- **Glyph**: a chip icon (lit while a model runs) and the selected model's name. Click to open the panel.
- **Panel**: models grouped by engine, each with its port; the selected model's address; START / STOP.

## Engines and models (auto-detected)

| Engine | Found when | Models | Started as |
|---|---|---|---|
| Strata | `<Strata folder>/serve/server.py` exists | every `strata-<name>.json` in it (port from its `"port"`) | `<folder>/.venv/bin/python` (or `python3`) `serve/server.py --engine strata --config <json> --port <port> --lazy --idle-unload 900` |
| TabbyAPI | `<TabbyAPI folder>/run.sh` exists | every finished folder in its `models/` (EXL3) | `run.sh --port <port> --model-name <folder>` |
| llama.cpp | `llama-server` is on the PATH | every `*.gguf` in the GGUF folders (first shard; no mmproj/imatrix) | `llama-server -m <gguf> --host 127.0.0.1 --port <port> -c 16384` |

A model with no fixed port gets the next free one from 8081 (skipping ports in use) the first time it
is seen, kept in `ports` under the state dir so it never changes.

## Settings (QS Bar Settings > Community > Local LLM Manager, group "Paths")

| Setting | Default |
|---|---|
| Strata folder | `~/Strata` |
| TabbyAPI folder | `~/tabbyAPI` |
| GGUF folders (space-separated) | `~/models` |

## What it runs, reads, writes

- `bin/strata-models list` every 5 s (reads the folders above, `ss -ltn` when assigning a port) and
`systemctl --user is-active strata-model.service`.
- On click: `use` writes `current`; `start` stops `strata-model.service` if it runs, then launches the
selected model as that transient user unit with `systemd-run --user` (command lines above);
`stop` stops it. Model servers you started by hand are left alone.
- Nothing to install: the unit is transient, nothing is enabled, nothing starts at login.
- Writes only under `$XDG_STATE_HOME/ryoku/plugins/strata/`. No privileged commands. Network: none
itself; it only shows the local servers' `127.0.0.1` address.
Binary file added plugins/strata/assets/preview-panel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added plugins/strata/assets/preview-widget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions plugins/strata/bin/strata-models
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/bin/sh
# Finds every installed engine and its ready models; one model runs at a time, as the transient
# user unit strata-model.service (systemd-run; nothing to install, nothing starts at login).
#
# strata-models list <state> one "key<TAB>engine<TAB>label<TAB>port<TAB>unit<TAB>path" line per model
# strata-models current <state> print the selected model key ("engine:name"), or nothing
# strata-models use <key> <state> select a model; restarts the server if one is running
# strata-models start <state> start the selected model (stops the previous one)
# strata-models run <key> <state> select a model and start it
# strata-models stop stop the running model
#
# Engines: Strata ($STRATA_DIR, default ~/Strata), TabbyAPI ($TABBY_DIR, default ~/tabbyAPI),
# llama.cpp (llama-server on the PATH; *.gguf in $GGUF_DIRS, space-separated, default ~/models).
home() { case "$1" in "~") echo "$HOME" ;; "~/"*) echo "$HOME/${1#\~/}" ;; *) echo "$1" ;; esac; }
strata=$(home "${STRATA_DIR:-$HOME/Strata}")
tabby=$(home "${TABBY_DIR:-$HOME/tabbyAPI}")
gguf_dirs=$(for g in ${GGUF_DIRS:-$HOME/models}; do home "$g"; done)
unit=strata-model.service

# a stable port per model, kept in <state>/ports ("key port"); skips ports already taken or in use
port_for() { # key state [fixed]
[ -n "$3" ] && { echo "$3"; return; }
p=$(awk -v k="$1" '$1 == k { print $2 }' "$2/ports" 2>/dev/null)
[ -n "$p" ] && { echo "$p"; return; }
used=$( { cat "$strata"/strata-*.json 2>/dev/null | sed -n 's/.*"port": *\([0-9]*\).*/\1/p'
awk '{ print $2 }' "$2/ports" 2>/dev/null
ss -Hltn 2>/dev/null | awk '{ n = split($4, a, ":"); print a[n] }'; } | sort -u)
p=8081
while echo "$used" | grep -qx "$p"; do p=$((p + 1)); done
mkdir -p "$2" && echo "$1 $p" >> "$2/ports"
echo "$p"
}

row() { # key engine label port path
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$unit" "$5"
}

list() { # state
if [ -f "$strata/serve/server.py" ]; then
for f in "$strata"/strata-*.json; do
[ -e "$f" ] || continue
n=$(basename "$f" .json | sed 's/^strata-//')
fixed=$(sed -n 's/.*"port": *\([0-9]*\).*/\1/p' "$f" | head -n 1)
row "strata:$n" Strata "$n" "$(port_for "strata:$n" "$1" "${fixed:-8081}")" "$f"
done
fi
if [ -x "$tabby/run.sh" ]; then
for d in "$tabby"/models/*/; do
d=${d%/}
# a finished download: config and shards present, none still partial in hf's cache
[ -f "$d/config.json" ] && ls "$d"/*.safetensors >/dev/null 2>&1 || continue
ls "$d"/.cache/huggingface/download/*.incomplete >/dev/null 2>&1 && continue
n=$(basename "$d")
row "tabby:$n" TabbyAPI "$(echo "$n" | sed 's/[-_.]exl3.*//I')" "$(port_for "tabby:$n" "$1")" "$d"
done
fi
if command -v llama-server >/dev/null; then
for g in $gguf_dirs; do
for f in "$g"/*.gguf; do
[ -e "$f" ] || continue
n=$(basename "$f" .gguf)
case "$n" in *mmproj*|*imatrix*) continue ;; *-0000[2-9]-of-*|*-000[1-9][0-9]-of-*) continue ;; esac
row "llama:$n" llama.cpp "$(echo "$n" | sed 's/\.i1-.*//; s/-00001-of-.*//')" "$(port_for "llama:$n" "$1")" "$f"
done
done
fi
return 0
}

current() { # state
k=$(cat "$1/current" 2>/dev/null) || return 0
case "$k" in *:*) echo "$k" ;; ?*) echo "strata:$k" ;; esac # older files hold a bare Strata name
}
lookup() { list "$2" | awk -F '\t' -v k="$1" '$1 == k'; }

start() { # state
k=$(current "$1")
[ -n "$k" ] || { echo "no model selected" >&2; exit 2; }
r=$(lookup "$k" "$1")
[ -n "$r" ] || { echo "model not ready" >&2; exit 2; }
port=$(echo "$r" | cut -f4); path=$(echo "$r" | cut -f6)
systemctl --user stop "$unit" 2>/dev/null
systemctl --user reset-failed "$unit" 2>/dev/null
set -- systemd-run --user --unit="$unit" --collect --property=Restart=on-failure
case "$k" in
strata:*)
py="$strata/.venv/bin/python"; [ -x "$py" ] || py=python3
"$@" --working-directory="$strata" -- "$py" "$strata/serve/server.py" --engine strata \
--config "$path" --port "$port" --lazy --idle-unload 900 ;;
tabby:*)
"$@" --working-directory="$tabby" -- "$tabby/run.sh" --port "$port" --model-name "$(basename "$path")" ;;
llama:*)
"$@" --working-directory="$HOME" -- llama-server -m "$path" --host 127.0.0.1 --port "$port" -c 16384 ;;
esac
}

case "$1" in
list) list "$2" ;;
current) current "$2" ;;
use)
case "$2" in ''|*[!A-Za-z0-9._:-]*) echo "bad model key" >&2; exit 2 ;; esac
[ -n "$(lookup "$2" "$3")" ] || { echo "no such model" >&2; exit 2; }
mkdir -p "$3" && echo "$2" > "$3/current"
if systemctl --user is-active --quiet "$unit"; then start "$3"; fi ;;
start) start "$2" ;;
run)
case "$2" in ''|*[!A-Za-z0-9._:-]*) echo "bad model key" >&2; exit 2 ;; esac
[ -n "$(lookup "$2" "$3")" ] || { echo "no such model" >&2; exit 2; }
mkdir -p "$3" && echo "$2" > "$3/current"
start "$3" ;;
stop) systemctl --user stop "$unit" 2>/dev/null ;;
*) echo "usage: $0 list|current|use|start|run|stop (see the header)" >&2; exit 2 ;;
esac
Loading